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: Static content parameter definitions.
* :white_check_mark: Middleware interface. * :white_check_mark: Middleware interface.
* :white_check_mark: MIME type inference. * :white_check_mark: MIME type inference.
* :white_check_mark: Email delivery.
* :white_check_mark: Background jobs.
* :x: Environment configurations (develompent/production/etc.) * :x: Environment configurations (develompent/production/etc.)
* :x: Email delivery.
* :x: Custom/non-conventional routes. * :x: Custom/non-conventional routes.
* :x: General-purpose cache. * :x: General-purpose cache.
* :x: Background jobs.
* :x: Testing helpers for testing HTTP requests/responses. * :x: Testing helpers for testing HTTP requests/responses.
* :x: Development server auto-reload. * :x: Development server auto-reload.
* :x: Database integration. * :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); 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;
const secret_len = jetzig.http.Session.Cipher.key_length + jetzig.http.Session.Cipher.nonce_length;
const secret = (try self.getSecret(&logger, secret_len, environment))[0..secret_len]; const secret = (try self.getSecret(&logger, secret_len, environment))[0..secret_len];
if (secret.len != secret_len) { if (secret.len != secret_len) {

View File

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

View File

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