Fix session nonce re-use

Use a new secure-random nonce for each session encryption.

Many thanks to @Trundle for writing [this gist](https://gist.github.com/Trundle/76c0d8f80999617f4d8373c03f86391a)
highlighting the severity of this issue.
This commit is contained in:
Bob Farrell 2024-05-01 20:51:39 +01:00
parent 3c88d845a8
commit 1ea3f6c4ff
7 changed files with 95 additions and 102 deletions

View File

@ -38,11 +38,11 @@ If you are interested in _Jetzig_ you will probably find these tools interesting
* :white_check_mark: Static content parameter definitions.
* :white_check_mark: Middleware interface.
* :white_check_mark: MIME type inference.
* :white_check_mark: Email delivery.
* :white_check_mark: Background jobs.
* :x: Environment configurations (develompent/production/etc.)
* :x: Email delivery.
* :x: Custom/non-conventional routes.
* :x: General-purpose cache.
* :x: Background jobs.
* :x: Testing helpers for testing HTTP requests/responses.
* :x: Development server auto-reload.
* :x: Database integration.

View File

@ -0,0 +1,25 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
var root = try data.object();
if (try request.session.get("message")) |message| {
try root.put("message", message);
} else {
try root.put("message", data.string("No message saved yet"));
}
return request.render(.ok);
}
pub fn post(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
_ = data;
const params = try request.params();
if (params.get("message")) |message| {
try request.session.put("message", message);
}
return request.redirect("/session", .moved_permanently);
}

View File

@ -0,0 +1,10 @@
<div>
<span>Saved message in session: {{.message}}</span>
</div>
<hr/>
<form action="/session" method="POST">
<input type="text" name="message" placeholder="Enter a message here" />
<input type="submit" />
</form>

View File

@ -95,8 +95,7 @@ pub fn getServerOptions(self: Environment) !jetzig.http.Server.ServerOptions {
std.process.exit(1);
}
// TODO: Generate nonce per session - do research to confirm correct best practice.
const secret_len = jetzig.http.Session.Cipher.key_length + jetzig.http.Session.Cipher.nonce_length;
const secret_len = jetzig.http.Session.Cipher.key_length;
const secret = (try self.getSecret(&logger, secret_len, environment))[0..secret_len];
if (secret.len != secret_len) {

View File

@ -3,32 +3,26 @@ const std = @import("std");
const jetzig = @import("../../jetzig.zig");
pub const cookie_name = "_jetzig-session";
pub const Cipher = std.crypto.aead.aes_gcm.Aes256Gcm;
pub const Cipher = std.crypto.aead.chacha_poly.XChaCha20Poly1305;
allocator: std.mem.Allocator,
encryption_key: ?[]const u8,
encryption_key: []const u8,
cookies: *jetzig.http.Cookies,
hashmap: std.StringHashMap(jetzig.data.Value),
cookie: ?jetzig.http.Cookies.Cookie = null,
initialized: bool = false,
data: jetzig.data.Data = undefined,
data: jetzig.data.Data,
state: enum { parsed, pending } = .pending,
encrypted: ?[]const u8 = null,
decrypted: ?[]const u8 = null,
encoded: ?[]const u8 = null,
const Self = @This();
pub fn init(
allocator: std.mem.Allocator,
cookies: *jetzig.http.Cookies,
encryption_key: ?[]const u8,
encryption_key: []const u8,
) Self {
return .{
.allocator = allocator,
.hashmap = std.StringHashMap(jetzig.data.Value).init(allocator),
.data = jetzig.data.Data.init(allocator),
.cookies = cookies,
.encryption_key = encryption_key,
};
@ -43,7 +37,7 @@ pub fn parse(self: *Self) !void {
}
pub fn reset(self: *Self) !void {
self.data = jetzig.data.Data.init(self.allocator);
self.data.reset();
_ = try self.data.object();
self.state = .parsed;
try self.save();
@ -52,17 +46,7 @@ pub fn reset(self: *Self) !void {
pub fn deinit(self: *Self) void {
if (self.state != .parsed) return;
var it = self.hashmap.iterator();
while (it.next()) |item| {
self.allocator.destroy(item.key_ptr);
self.allocator.destroy(item.value_ptr);
}
self.hashmap.deinit();
if (self.encoded) |*ptr| self.allocator.free(ptr.*);
if (self.decrypted) |*ptr| self.allocator.free(ptr.*);
if (self.encrypted) |*ptr| self.allocator.free(ptr.*);
if (self.cookie) |*ptr| self.allocator.free(ptr.*.value);
self.data.deinit();
}
pub fn get(self: *Self, key: []const u8) !?*jetzig.data.Value {
@ -91,97 +75,70 @@ fn save(self: *Self) !void {
if (self.state != .parsed) return error.UnparsedSessionCookie;
const json = try self.data.toJson();
defer self.allocator.free(json);
if (self.encrypted) |*ptr| {
self.allocator.free(ptr.*);
self.encrypted = null;
}
self.encrypted = try self.encrypt(json);
const encoded = try jetzig.util.base64Encode(self.allocator, self.encrypted.?);
const encrypted = try self.encrypt(json);
defer self.allocator.free(encrypted);
const encoded = try jetzig.util.base64Encode(self.allocator, encrypted);
defer self.allocator.free(encoded);
if (self.cookie) |*ptr| self.allocator.free(ptr.*.value);
self.cookie = .{ .value = try self.allocator.dupe(u8, encoded) };
try self.cookies.put(
cookie_name,
self.cookie.?,
);
std.debug.print("encoded: {s}\n", .{encoded});
try self.cookies.put(cookie_name, .{ .value = encoded });
}
fn parseSessionCookie(self: *Self, cookie_value: []const u8) !void {
self.data = jetzig.data.Data.init(self.allocator);
const decoded = try jetzig.util.base64Decode(self.allocator, cookie_value);
defer self.allocator.free(decoded);
const buf = self.decrypt(decoded) catch |err| {
const decrypted = self.decrypt(decoded) catch |err| {
switch (err) {
error.AuthenticationFailed => return error.JetzigInvalidSessionCookie,
else => return err,
}
};
defer self.allocator.free(buf);
if (self.decrypted) |*ptr| self.allocator.free(ptr.*);
self.decrypted = try self.allocator.dupe(u8, buf);
defer self.allocator.free(decrypted);
try self.data.fromJson(self.decrypted.?);
try self.data.fromJson(decrypted);
self.state = .parsed;
}
fn decrypt(self: *Self, data: []const u8) ![]const u8 {
if (self.encryption_key) |secret| {
const encrypted = data[0 .. data.len - Cipher.tag_length];
const secret_bytes = std.mem.sliceAsBytes(secret);
const key: [Cipher.key_length]u8 = secret_bytes[0..Cipher.key_length].*;
const nonce: [Cipher.nonce_length]u8 = secret_bytes[Cipher.key_length .. Cipher.key_length + Cipher.nonce_length].*;
const buf = try self.allocator.alloc(u8, data.len - Cipher.tag_length);
const additional_data = "";
fn decrypt(self: *Self, data: []u8) ![]u8 {
const secret_bytes = std.mem.sliceAsBytes(self.encryption_key);
const key = secret_bytes[0..Cipher.key_length];
const nonce = data[0..Cipher.nonce_length];
const buf = try self.allocator.alloc(u8, data.len - Cipher.tag_length - Cipher.nonce_length);
errdefer self.allocator.free(buf);
const associated_data = "";
var tag: [Cipher.tag_length]u8 = undefined;
std.mem.copyForwards(u8, &tag, data[data.len - Cipher.tag_length ..]);
@memcpy(&tag, data[data.len - Cipher.tag_length ..]);
try Cipher.decrypt(
buf,
encrypted,
data[Cipher.nonce_length .. data.len - Cipher.tag_length],
tag,
additional_data,
nonce,
key,
associated_data,
nonce.*,
key.*,
);
return buf[0..];
} else {
return self.allocator.dupe(u8, "hello");
}
return buf;
}
fn encrypt(self: *Self, value: []const u8) ![]const u8 {
if (self.encryption_key) |secret| {
const secret_bytes = std.mem.sliceAsBytes(secret);
const secret_bytes = std.mem.sliceAsBytes(self.encryption_key);
const key: [Cipher.key_length]u8 = secret_bytes[0..Cipher.key_length].*;
const nonce: [Cipher.nonce_length]u8 = secret_bytes[Cipher.key_length .. Cipher.key_length + Cipher.nonce_length].*;
var nonce: [Cipher.nonce_length]u8 = undefined;
for (0..Cipher.nonce_length) |index| nonce[index] = std.crypto.random.int(u8);
const associated_data = "";
if (self.encrypted) |*val| {
self.allocator.free(val.*);
self.encrypted = null;
}
const buf = try self.allocator.alloc(u8, value.len);
defer self.allocator.free(buf);
var tag: [Cipher.tag_length]u8 = undefined;
Cipher.encrypt(buf, &tag, value, associated_data, nonce, key);
if (self.encrypted) |*ptr| self.allocator.free(ptr.*);
self.encrypted = try std.mem.concat(
const encrypted = try std.mem.concat(
self.allocator,
u8,
&[_][]const u8{ buf, tag[0..] },
&[_][]const u8{ &nonce, buf, tag[0..] },
);
return self.encrypted.?;
} else {
return value;
}
return encrypted;
}
test "put and get session key/value" {
@ -190,10 +147,9 @@ test "put and get session key/value" {
defer cookies.deinit();
try cookies.parse();
const secret: [Cipher.key_length + Cipher.nonce_length]u8 = [_]u8{0x69} ** (Cipher.key_length + Cipher.nonce_length);
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
var session = Self.init(allocator, &cookies, &secret);
defer session.deinit();
defer session.data.deinit();
var data = jetzig.data.Data.init(allocator);
defer data.deinit();
@ -206,14 +162,16 @@ test "put and get session key/value" {
test "get value from parsed/decrypted cookie" {
const allocator = std.testing.allocator;
var cookies = jetzig.http.Cookies.init(allocator, "_jetzig-session=GIRI22v4C9EwU_mY02_obbnX2QkdnEZenlQz2xs");
var cookies = jetzig.http.Cookies.init(
allocator,
"_jetzig-session=fPCFwZHvPDT-XCVcsQUSspDLchS3tRuJDqPpB2v3127VXpRP_bPcPLgpHK6RiVkfcP1bMtU",
);
defer cookies.deinit();
try cookies.parse();
const secret: [Cipher.key_length + Cipher.nonce_length]u8 = [_]u8{0x69} ** (Cipher.key_length + Cipher.nonce_length);
const secret: [Cipher.key_length]u8 = [_]u8{0x69} ** Cipher.key_length;
var session = Self.init(allocator, &cookies, &secret);
defer session.deinit();
defer session.data.deinit();
try session.parse();
var value = (try session.get("foo")).?;

View File

@ -10,7 +10,7 @@ pub fn equalStringsCaseInsensitive(expected: []const u8, actual: []const u8) boo
}
/// Encode arbitrary input to Base64.
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]u8 {
const encoder = std.base64.Base64Encoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,
@ -22,7 +22,7 @@ pub fn base64Encode(allocator: std.mem.Allocator, string: []const u8) ![]const u
}
/// Decode arbitrary input from Base64.
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]const u8 {
pub fn base64Decode(allocator: std.mem.Allocator, string: []const u8) ![]u8 {
const decoder = std.base64.Base64Decoder.init(
std.base64.url_safe_no_pad.alphabet_chars,
std.base64.url_safe_no_pad.pad_char,

View File

@ -2,6 +2,7 @@ test {
_ = @import("jetzig/http/Query.zig");
_ = @import("jetzig/http/Headers.zig");
_ = @import("jetzig/http/Cookies.zig");
_ = @import("jetzig/http/Session.zig");
_ = @import("jetzig/http/Path.zig");
_ = @import("jetzig/jobs/Job.zig");
_ = @import("jetzig/mail/Mail.zig");