From 355794bf35c9a55f8e12321adb37c4d9b671c940 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:19:38 +0200 Subject: [PATCH 01/31] Add a Cubyz protection api and integrate it Due to line 411 in authentication.zig and some other reasons I have decided to rework this implementation to make use of a `protected` boolean attribute instead of creating tons of extra EncodingTypes. --- src/c.h | 7 ++ .../authentication/encrypt_with_password.zig | 5 +- src/main.zig | 1 + src/network/authentication.zig | 45 +++++++++---- src/protect.zig | 65 +++++++++++++++++++ 5 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 src/protect.zig diff --git a/src/c.h b/src/c.h index 33f794d155..4d42662db3 100644 --- a/src/c.h +++ b/src/c.h @@ -55,6 +55,13 @@ #include #endif +// Used for platform-speciffic keystorage +#ifdef _WIN32 +#pragma comment(lib, "crypt32.lib") +#include +#include +#endif + // used for audio #include #define STB_VORBIS_HEADER_ONLY diff --git a/src/gui/windows/authentication/encrypt_with_password.zig b/src/gui/windows/authentication/encrypt_with_password.zig index d7545917ab..016050a60e 100644 --- a/src/gui/windows/authentication/encrypt_with_password.zig +++ b/src/gui/windows/authentication/encrypt_with_password.zig @@ -13,6 +13,7 @@ const Label = GuiComponent.Label; const HorizontalList = GuiComponent.HorizontalList; const TextInput = GuiComponent.TextInput; const VerticalList = GuiComponent.VerticalList; +const PEAC = main.network.authentication.PasswordEncodedAccountCode; pub var window = GuiWindow{ .contentSize = Vec2f{128, 256}, @@ -40,10 +41,10 @@ pub fn setAccountCode(accountCode_: main.network.authentication.AccountCode) voi fn confirm() void { if (encryptAccountCode) { settings.storedAccount.deinit(main.globalAllocator); - settings.storedAccount = .initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items); + settings.storedAccount = PEAC.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, true) catch PEAC.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, false) catch unreachable; } else { settings.storedAccount.deinit(main.globalAllocator); - settings.storedAccount = .initUnencoded(main.globalAllocator, accountCode); + settings.storedAccount = PEAC.initUnencoded(main.globalAllocator, accountCode, true) catch PEAC.initUnencoded(main.globalAllocator, accountCode, false) catch unreachable; } settings.save(); diff --git a/src/main.zig b/src/main.zig index d7e75925d4..3340ca0bfa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -41,6 +41,7 @@ pub const utils = @import("utils.zig"); pub const vec = @import("vec.zig"); const zon = @import("zon.zig"); pub const ZonElement = zon.ZonElement; +pub const protect = @import("protect.zig"); const file_monitor = utils.file_monitor; diff --git a/src/network/authentication.zig b/src/network/authentication.zig index c2cd3f9599..d25146ec95 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -5,6 +5,7 @@ const BinaryWriter = main.utils.BinaryWriter; const BinaryReader = main.utils.BinaryReader; const NeverFailingAllocator = main.heap.NeverFailingAllocator; const ZonElement = main.ZonElement; +const protect = main.protect; var wordlist: ?[2048][]const u8 = null; @@ -284,7 +285,7 @@ pub const AccountCode = struct { } }; -const EncodingType = enum { none, argon2_aes_gcm }; +pub const EncodingType = enum { none, winProtect, argon2_aes_gcm, winProtect_argon2_aes_gcm }; pub const PasswordEncodedAccountCode = struct { typ: EncodingType, @@ -295,7 +296,7 @@ pub const PasswordEncodedAccountCode = struct { pub const empty: PasswordEncodedAccountCode = .{.typ = .none, .salt = &.{}, .nonce = &.{}, .data = &.{}, .authenticationTag = &.{}}; - pub fn initFromPassword(allocator: NeverFailingAllocator, accountCode: AccountCode, password: []const u8) PasswordEncodedAccountCode { + pub fn initFromPassword(allocator: NeverFailingAllocator, accountCode: AccountCode, password: []const u8, shouldProtect: bool) error{syserr}!PasswordEncodedAccountCode { var salt: [32]u8 = undefined; main.io.random(&salt); const saltBase64 = allocator.alloc(u8, std.base64.standard.Encoder.calcSize(salt.len)); @@ -306,26 +307,27 @@ pub const PasswordEncodedAccountCode = struct { keyFromPassword(.argon2_aes_gcm, saltBase64, password, &key); const encryptedBuffer = allocator.alloc(u8, accountCode.text.len); + defer if (shouldProtect) allocator.free(encryptedBuffer); var authenticationTag: [std.crypto.aead.aes_gcm.Aes256Gcm.tag_length]u8 = undefined; var nonce: [std.crypto.aead.aes_gcm.Aes256Gcm.nonce_length]u8 = undefined; main.io.random(&nonce); std.crypto.aead.aes_gcm.Aes256Gcm.encrypt(encryptedBuffer, &authenticationTag, accountCode.text, &.{}, nonce, key); return .{ - .typ = .argon2_aes_gcm, + .typ = if (shouldProtect) protect.getRecommendedEncoding(true) else .argon2_aes_gcm, .salt = saltBase64, - .data = encryptedBuffer, + .data = if (shouldProtect) try protect.protect(allocator, encryptedBuffer) else encryptedBuffer, .nonce = allocator.dupe(u8, &nonce), .authenticationTag = allocator.dupe(u8, &authenticationTag), }; } - pub fn initUnencoded(allocator: NeverFailingAllocator, accountCode: AccountCode) PasswordEncodedAccountCode { + pub fn initUnencoded(allocator: NeverFailingAllocator, accountCode: AccountCode, shouldProtect: bool) error{syserr}!PasswordEncodedAccountCode { return .{ - .typ = .none, + .typ = if (shouldProtect) protect.getRecommendedEncoding(false) else .none, .salt = &.{}, .nonce = &.{}, - .data = allocator.dupe(u8, accountCode.text), + .data = if (shouldProtect) try protect.protect(allocator, accountCode.text) else allocator.dupe(u8, accountCode.text), .authenticationTag = &.{}, }; } @@ -341,30 +343,49 @@ pub const PasswordEncodedAccountCode = struct { if (self.typ == .none) { return AccountCode.initFromUserInput(self.data, failureText); } + if (self.typ == protect.getRecommendedEncoding(false)) { + const data = try protect.unprotect(main.stackAllocator, self.data); + defer { + std.crypto.secureZero(u8, data); + main.stackAllocator.free(data); + } + + return AccountCode.initFromUserInput(data, failureText); + } var key: [32]u8 = undefined; defer std.crypto.secureZero(u8, &key); keyFromPassword(self.typ, self.salt, password, &key); switch (self.typ) { .none => unreachable, - .argon2_aes_gcm => { + protect.getRecommendedEncoding(true), .argon2_aes_gcm => { + var data = self.data; + if (self.typ == protect.getRecommendedEncoding(true)) { + data = try protect.unprotect(main.stackAllocator, data); + } + defer if (self.typ == protect.getRecommendedEncoding(true)) { + std.crypto.secureZero(u8, data); + main.stackAllocator.free(data); + }; + if (self.authenticationTag.len != std.crypto.aead.aes_gcm.Aes256Gcm.tag_length) return error.Invalid; if (self.nonce.len != std.crypto.aead.aes_gcm.Aes256Gcm.nonce_length) return error.Invalid; const authenticationTag = self.authenticationTag[0..std.crypto.aead.aes_gcm.Aes256Gcm.tag_length]; const nonce = self.nonce[0..std.crypto.aead.aes_gcm.Aes256Gcm.nonce_length]; - const decryptedBuffer = main.stackAllocator.alloc(u8, self.data.len); + const decryptedBuffer = main.stackAllocator.alloc(u8, data.len); defer main.stackAllocator.free(decryptedBuffer); defer std.crypto.secureZero(u8, decryptedBuffer); - try std.crypto.aead.aes_gcm.Aes256Gcm.decrypt(decryptedBuffer, self.data, authenticationTag.*, &.{}, nonce.*, key); + try std.crypto.aead.aes_gcm.Aes256Gcm.decrypt(decryptedBuffer, data, authenticationTag.*, &.{}, nonce.*, key); return AccountCode.initFromUserInput(decryptedBuffer, failureText); }, + else => return error.platformIncompatibleWithProtectionEncoding, } } fn keyFromPassword(typ: EncodingType, salt: []const u8, password: []const u8, key: *[32]u8) void { switch (typ) { - .none => unreachable, - .argon2_aes_gcm => { + .none, .winProtect => unreachable, + .winProtect_argon2_aes_gcm, .argon2_aes_gcm => { std.crypto.pwhash.argon2.kdf(main.globalAllocator.allocator, key, password, salt, .{ .t = 10, .m = 32000, diff --git a/src/protect.zig b/src/protect.zig new file mode 100644 index 0000000000..0337cb36da --- /dev/null +++ b/src/protect.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const main = @import("main"); +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const EncodingType = main.network.authentication.EncodingType; +const c = @import("c"); + +pub fn canProtect() bool { + switch (builtin.os.tag) { + .windows => return true, + else => return false, + } +} + +pub inline fn getRecommendedEncoding(comptime encrypted: bool) EncodingType { + switch (builtin.os.tag) { + .windows => if (encrypted) return .winProtect_argon2_aes_gcm else return .winProtect, + else => if (encrypted) return .argon2_aes_gcm else return .none, + } +} + +pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{syserr}![]u8 { + if (builtin.os.tag == .windows) { + var plainblob: c.DATA_BLOB = undefined; + var cipherblob: c.DATA_BLOB = undefined; + plainblob.cbData = @intCast(data.len); + plainblob.pbData = @as([*c]u8, data.ptr); // Does this need to be secureZeroed? + if (c.CryptProtectData(&plainblob, @as([*c]const c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &cipherblob) == 0) { + std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + return error.syserr; + } + defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + const out: []u8 = allocator.alloc(u8, @intCast(cipherblob.cbData)); + @memcpy(out, cipherblob.pbData); + return out; + } else { + return allocator.dupe(u8, data); + } +} + +pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Invalid }![]u8 { + if (builtin.os.tag == .windows) { + var plainblob: c.DATA_BLOB = undefined; + var cipherblob: c.DATA_BLOB = undefined; + cipherblob.cbData = @intCast(data.len); + cipherblob.pbData = @as([*c]u8, data.ptr); + if (c.CryptUnprotectData(&cipherblob, @as([*c][*c]c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &plainblob) == 0) { + std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{c.GetLastError()}); + return error.Invalid; // Will assume the error to be caused by wrong input + } + var pbDataSlice: []u8 = undefined; + pbDataSlice.len = plainblob.cbData; + pbDataSlice.ptr = plainblob.pbData; + defer { + std.crypto.secureZero(u8, pbDataSlice); + if (c.LocalFree(plainblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + } + const out: []u8 = allocator.alloc(u8, @intCast(plainblob.cbData)); + @memcpy(out, plainblob.pbData); + return out; + } else { + return allocator.dupe(u8, data); + } +} From 7aa8be7ae17f13a6bcc5b16df28b33c4a434039c Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:19:56 +0200 Subject: [PATCH 02/31] Lots of things Obey linter and migrate to protected attribute. protect() and unprotect() now error on unsupported platforms. I felt that it would be bad, if someone assumed that a call to unprotect would always error on bad input or that a call to protect would always encrypt the data. These previously false assumptions are now true. --- .../authentication/encrypt_with_password.zig | 6 +-- src/network/authentication.zig | 44 +++++++++++-------- src/protect.zig | 15 ++----- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/gui/windows/authentication/encrypt_with_password.zig b/src/gui/windows/authentication/encrypt_with_password.zig index 016050a60e..b65d967500 100644 --- a/src/gui/windows/authentication/encrypt_with_password.zig +++ b/src/gui/windows/authentication/encrypt_with_password.zig @@ -13,7 +13,7 @@ const Label = GuiComponent.Label; const HorizontalList = GuiComponent.HorizontalList; const TextInput = GuiComponent.TextInput; const VerticalList = GuiComponent.VerticalList; -const PEAC = main.network.authentication.PasswordEncodedAccountCode; +const PasswordEncodedAccountCode = main.network.authentication.PasswordEncodedAccountCode; pub var window = GuiWindow{ .contentSize = Vec2f{128, 256}, @@ -41,10 +41,10 @@ pub fn setAccountCode(accountCode_: main.network.authentication.AccountCode) voi fn confirm() void { if (encryptAccountCode) { settings.storedAccount.deinit(main.globalAllocator); - settings.storedAccount = PEAC.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, true) catch PEAC.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, false) catch unreachable; + settings.storedAccount = PasswordEncodedAccountCode.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, true) catch PasswordEncodedAccountCode.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, false) catch unreachable; } else { settings.storedAccount.deinit(main.globalAllocator); - settings.storedAccount = PEAC.initUnencoded(main.globalAllocator, accountCode, true) catch PEAC.initUnencoded(main.globalAllocator, accountCode, false) catch unreachable; + settings.storedAccount = PasswordEncodedAccountCode.initUnencoded(main.globalAllocator, accountCode, true) catch PasswordEncodedAccountCode.initUnencoded(main.globalAllocator, accountCode, false) catch unreachable; } settings.save(); diff --git a/src/network/authentication.zig b/src/network/authentication.zig index d25146ec95..72303bdde9 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -285,16 +285,17 @@ pub const AccountCode = struct { } }; -pub const EncodingType = enum { none, winProtect, argon2_aes_gcm, winProtect_argon2_aes_gcm }; +pub const EncodingType = enum { none, argon2_aes_gcm }; pub const PasswordEncodedAccountCode = struct { typ: EncodingType, + protected: bool, salt: []u8, nonce: []u8, data: []u8, authenticationTag: []u8, - pub const empty: PasswordEncodedAccountCode = .{.typ = .none, .salt = &.{}, .nonce = &.{}, .data = &.{}, .authenticationTag = &.{}}; + pub const empty: PasswordEncodedAccountCode = .{.typ = .none, .protected = false, .salt = &.{}, .nonce = &.{}, .data = &.{}, .authenticationTag = &.{}}; pub fn initFromPassword(allocator: NeverFailingAllocator, accountCode: AccountCode, password: []const u8, shouldProtect: bool) error{syserr}!PasswordEncodedAccountCode { var salt: [32]u8 = undefined; @@ -313,21 +314,25 @@ pub const PasswordEncodedAccountCode = struct { main.io.random(&nonce); std.crypto.aead.aes_gcm.Aes256Gcm.encrypt(encryptedBuffer, &authenticationTag, accountCode.text, &.{}, nonce, key); + const protected = shouldProtect and protect.canProtect(); return .{ - .typ = if (shouldProtect) protect.getRecommendedEncoding(true) else .argon2_aes_gcm, + .typ = .argon2_aes_gcm, + .protected = protected, .salt = saltBase64, - .data = if (shouldProtect) try protect.protect(allocator, encryptedBuffer) else encryptedBuffer, + .data = if (protected) protect.protect(allocator, encryptedBuffer) catch |err| {if (err==error.syserr) return error.syserr else unreachable;} else encryptedBuffer, .nonce = allocator.dupe(u8, &nonce), .authenticationTag = allocator.dupe(u8, &authenticationTag), }; } pub fn initUnencoded(allocator: NeverFailingAllocator, accountCode: AccountCode, shouldProtect: bool) error{syserr}!PasswordEncodedAccountCode { + const protected = shouldProtect and protect.canProtect(); return .{ - .typ = if (shouldProtect) protect.getRecommendedEncoding(false) else .none, + .typ = .none, + .protected = protected, .salt = &.{}, .nonce = &.{}, - .data = if (shouldProtect) try protect.protect(allocator, accountCode.text) else allocator.dupe(u8, accountCode.text), + .data = if (protected) protect.protect(allocator, accountCode.text) catch |err| {if (err == error.syserr) return error.syserr else unreachable;} else allocator.dupe(u8, accountCode.text), .authenticationTag = &.{}, }; } @@ -340,16 +345,16 @@ pub const PasswordEncodedAccountCode = struct { } pub fn decryptFromPassword(self: PasswordEncodedAccountCode, password: []const u8, failureText: *main.ListManaged(u8)) !AccountCode { + if (self.protected and !protect.canProtect()) return error.Invalid; if (self.typ == .none) { - return AccountCode.initFromUserInput(self.data, failureText); - } - if (self.typ == protect.getRecommendedEncoding(false)) { - const data = try protect.unprotect(main.stackAllocator, self.data); - defer { + var data = self.data; + if (self.protected) { + data = try protect.unprotect(main.stackAllocator, data); + } + defer if (self.protected) { std.crypto.secureZero(u8, data); main.stackAllocator.free(data); - } - + }; return AccountCode.initFromUserInput(data, failureText); } var key: [32]u8 = undefined; @@ -358,12 +363,12 @@ pub const PasswordEncodedAccountCode = struct { switch (self.typ) { .none => unreachable, - protect.getRecommendedEncoding(true), .argon2_aes_gcm => { + .argon2_aes_gcm => { var data = self.data; - if (self.typ == protect.getRecommendedEncoding(true)) { + if (self.protected) { data = try protect.unprotect(main.stackAllocator, data); } - defer if (self.typ == protect.getRecommendedEncoding(true)) { + defer if (self.protected) { std.crypto.secureZero(u8, data); main.stackAllocator.free(data); }; @@ -378,14 +383,13 @@ pub const PasswordEncodedAccountCode = struct { try std.crypto.aead.aes_gcm.Aes256Gcm.decrypt(decryptedBuffer, data, authenticationTag.*, &.{}, nonce.*, key); return AccountCode.initFromUserInput(decryptedBuffer, failureText); }, - else => return error.platformIncompatibleWithProtectionEncoding, } } fn keyFromPassword(typ: EncodingType, salt: []const u8, password: []const u8, key: *[32]u8) void { switch (typ) { - .none, .winProtect => unreachable, - .winProtect_argon2_aes_gcm, .argon2_aes_gcm => { + .none => unreachable, + .argon2_aes_gcm => { std.crypto.pwhash.argon2.kdf(main.globalAllocator.allocator, key, password, salt, .{ .t = 10, .m = 32000, @@ -400,6 +404,7 @@ pub const PasswordEncodedAccountCode = struct { var self: PasswordEncodedAccountCode = undefined; self.typ = std.meta.stringToEnum(EncodingType, zon.get([]const u8, "type") orelse return error.Invalid) orelse return error.Invalid; + self.protected = zon.get(bool, "protected") orelse false; self.salt = allocator.dupe(u8, zon.get([]const u8, "salt") orelse ""); errdefer allocator.free(self.salt); if (self.salt.len < 32 and self.typ != .none) return error.Invalid; @@ -428,6 +433,7 @@ pub const PasswordEncodedAccountCode = struct { pub fn toZon(self: PasswordEncodedAccountCode, allocator: NeverFailingAllocator) ZonElement { const zon = ZonElement.initObject(allocator); zon.put("type", @tagName(self.typ)); + zon.put("protected", self.protected); zon.putOwnedString("salt", self.salt); const base64EncodedData = main.stackAllocator.alloc(u8, std.base64.standard.Encoder.calcSize(self.data.len)); diff --git a/src/protect.zig b/src/protect.zig index 0337cb36da..997075552b 100644 --- a/src/protect.zig +++ b/src/protect.zig @@ -6,21 +6,14 @@ const NeverFailingAllocator = main.heap.NeverFailingAllocator; const EncodingType = main.network.authentication.EncodingType; const c = @import("c"); -pub fn canProtect() bool { +pub inline fn canProtect() bool { switch (builtin.os.tag) { .windows => return true, else => return false, } } -pub inline fn getRecommendedEncoding(comptime encrypted: bool) EncodingType { - switch (builtin.os.tag) { - .windows => if (encrypted) return .winProtect_argon2_aes_gcm else return .winProtect, - else => if (encrypted) return .argon2_aes_gcm else return .none, - } -} - -pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{syserr}![]u8 { +pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Unsupported }![]u8 { if (builtin.os.tag == .windows) { var plainblob: c.DATA_BLOB = undefined; var cipherblob: c.DATA_BLOB = undefined; @@ -35,7 +28,7 @@ pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{syserr}![]u8 @memcpy(out, cipherblob.pbData); return out; } else { - return allocator.dupe(u8, data); + return error.Unsupported; } } @@ -60,6 +53,6 @@ pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, In @memcpy(out, plainblob.pbData); return out; } else { - return allocator.dupe(u8, data); + return error.Invalid; } } From 5e6f73cc3b1fe043a12dbd5e5798440d1ece4c58 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:23:09 +0200 Subject: [PATCH 03/31] Format --- src/network/authentication.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index 72303bdde9..39bb4f2366 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -319,7 +319,9 @@ pub const PasswordEncodedAccountCode = struct { .typ = .argon2_aes_gcm, .protected = protected, .salt = saltBase64, - .data = if (protected) protect.protect(allocator, encryptedBuffer) catch |err| {if (err==error.syserr) return error.syserr else unreachable;} else encryptedBuffer, + .data = if (protected) protect.protect(allocator, encryptedBuffer) catch |err| { + if (err == error.syserr) return error.syserr else unreachable; + } else encryptedBuffer, .nonce = allocator.dupe(u8, &nonce), .authenticationTag = allocator.dupe(u8, &authenticationTag), }; @@ -332,7 +334,9 @@ pub const PasswordEncodedAccountCode = struct { .protected = protected, .salt = &.{}, .nonce = &.{}, - .data = if (protected) protect.protect(allocator, accountCode.text) catch |err| {if (err == error.syserr) return error.syserr else unreachable;} else allocator.dupe(u8, accountCode.text), + .data = if (protected) protect.protect(allocator, accountCode.text) catch |err| { + if (err == error.syserr) return error.syserr else unreachable; + } else allocator.dupe(u8, accountCode.text), .authenticationTag = &.{}, }; } From 5201c5ce76d7cf93c106439486859a560aa83607 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:58:59 +0200 Subject: [PATCH 04/31] All hail The Linter! --- src/network/authentication.zig | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index 39bb4f2366..8a2bc2a9be 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -308,20 +308,26 @@ pub const PasswordEncodedAccountCode = struct { keyFromPassword(.argon2_aes_gcm, saltBase64, password, &key); const encryptedBuffer = allocator.alloc(u8, accountCode.text.len); - defer if (shouldProtect) allocator.free(encryptedBuffer); var authenticationTag: [std.crypto.aead.aes_gcm.Aes256Gcm.tag_length]u8 = undefined; var nonce: [std.crypto.aead.aes_gcm.Aes256Gcm.nonce_length]u8 = undefined; main.io.random(&nonce); std.crypto.aead.aes_gcm.Aes256Gcm.encrypt(encryptedBuffer, &authenticationTag, accountCode.text, &.{}, nonce, key); const protected = shouldProtect and protect.canProtect(); + var data: []u8 = undefined; + if (protected) { + data = protect.protect(allocator, encryptedBuffer) catch |err| { + if (err == error.syserr) return error.syserr else unreachable; + }; + defer allocator.free(encryptedBuffer); // Deferred, because that way even if a syserr is thrown it will still get freed + } else { + data = encryptedBuffer; + } return .{ .typ = .argon2_aes_gcm, .protected = protected, .salt = saltBase64, - .data = if (protected) protect.protect(allocator, encryptedBuffer) catch |err| { - if (err == error.syserr) return error.syserr else unreachable; - } else encryptedBuffer, + .data = data, .nonce = allocator.dupe(u8, &nonce), .authenticationTag = allocator.dupe(u8, &authenticationTag), }; @@ -329,14 +335,20 @@ pub const PasswordEncodedAccountCode = struct { pub fn initUnencoded(allocator: NeverFailingAllocator, accountCode: AccountCode, shouldProtect: bool) error{syserr}!PasswordEncodedAccountCode { const protected = shouldProtect and protect.canProtect(); + var data: []u8 = undefined; + if (protected) { + data = protect.protect(allocator, accountCode.text) catch |err| { + if (err == error.syserr) return error.syserr else unreachable; + }; + } else { + data = allocator.dupe(u8, accountCode.text); + } return .{ .typ = .none, .protected = protected, .salt = &.{}, .nonce = &.{}, - .data = if (protected) protect.protect(allocator, accountCode.text) catch |err| { - if (err == error.syserr) return error.syserr else unreachable; - } else allocator.dupe(u8, accountCode.text), + .data = data, .authenticationTag = &.{}, }; } From c47ab58c3baa4ba9d39a22970c52d5dd5688e9c8 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:34:35 +0200 Subject: [PATCH 05/31] Microslop Yes, I blame microslop for using an uppercase in their docs. --- src/c.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c.h b/src/c.h index 4d42662db3..9d4580587a 100644 --- a/src/c.h +++ b/src/c.h @@ -58,7 +58,7 @@ // Used for platform-speciffic keystorage #ifdef _WIN32 #pragma comment(lib, "crypt32.lib") -#include +#include #include #endif From 9185e3e1bd1f30f0fc6305137e4b2ba628341293 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:19:05 +0200 Subject: [PATCH 06/31] Add tests and improve unprotect error detection --- src/protect.zig | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/protect.zig b/src/protect.zig index 997075552b..539de1001a 100644 --- a/src/protect.zig +++ b/src/protect.zig @@ -39,8 +39,12 @@ pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, In cipherblob.cbData = @intCast(data.len); cipherblob.pbData = @as([*c]u8, data.ptr); if (c.CryptUnprotectData(&cipherblob, @as([*c][*c]c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &plainblob) == 0) { - std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{c.GetLastError()}); - return error.Invalid; // Will assume the error to be caused by wrong input + const err = c.GetLastError(); + if (err == 13) { + return error.Invalid; + } + std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); + return error.syserr; } var pbDataSlice: []u8 = undefined; pbDataSlice.len = plainblob.cbData; @@ -56,3 +60,29 @@ pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, In return error.Invalid; } } + +test "slice==unprotect(protect(slice))" { + const slice: []u8 = @as([]u8, @constCast("Test")); + if (canProtect()) { + const protected = try protect(main.stackAllocator, slice); + defer main.stackAllocator.free(protected); + const unprotected = try unprotect(main.stackAllocator, protected); + defer main.stackAllocator.free(unprotected); + try std.testing.expectEqualSlices(u8, slice, unprotected); + } +} + +test "Protect fails on unsupported platforms" { + const slice: []u8 = @as([]u8, @constCast("Test")); + if (!canProtect()) { + try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); + try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + } +} + +test "Unprotect fails when supplied with garbage" { + const slice: []u8 = @as([]u8, @constCast("TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj")); + if (canProtect()) { + try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + } +} From 8fc9cc6c484ddbb7b5a7b1fa147910a158153964 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:48:19 +0200 Subject: [PATCH 07/31] Improve Tests and errordetection Apparently errorcode 13 is for strings too short and errorcode 87 is for otherwise gibberish. The real reason I made this commit is that some github service was down when my last test ran and the only way to rerun it is to make another commit. --- src/protect.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/protect.zig b/src/protect.zig index 539de1001a..3ccd63990b 100644 --- a/src/protect.zig +++ b/src/protect.zig @@ -40,7 +40,7 @@ pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, In cipherblob.pbData = @as([*c]u8, data.ptr); if (c.CryptUnprotectData(&cipherblob, @as([*c][*c]c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &plainblob) == 0) { const err = c.GetLastError(); - if (err == 13) { + if (err == 13 or err == 87) { return error.Invalid; } std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); @@ -81,8 +81,10 @@ test "Protect fails on unsupported platforms" { } test "Unprotect fails when supplied with garbage" { - const slice: []u8 = @as([]u8, @constCast("TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj")); - if (canProtect()) { - try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + const slices: [5][]u8 = .{@as([]u8, @constCast("TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj")), @as([]u8, @constCast("Test")), @as([]u8, @constCast("Testd")), @as([]u8, @constCast("")), @as([]u8, @constCast("WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"))}; + for (slices) |slice| { + if (canProtect()) { + try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + } } } From a1b739051d7e46763fc51dd2cfed23b005a05a34 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:20:23 +0200 Subject: [PATCH 08/31] Improve based on received criticism --- src/protect.zig | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/protect.zig b/src/protect.zig index 3ccd63990b..0bbc9a297d 100644 --- a/src/protect.zig +++ b/src/protect.zig @@ -40,11 +40,13 @@ pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, In cipherblob.pbData = @as([*c]u8, data.ptr); if (c.CryptUnprotectData(&cipherblob, @as([*c][*c]c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &plainblob) == 0) { const err = c.GetLastError(); - if (err == 13 or err == 87) { - return error.Invalid; + switch (err) { + c.ERROR_INVALID_DATA, c.ERROR_INVALID_PARAMETER => return error.Invalid, + else => { + std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); + return error.syserr; + } } - std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); - return error.syserr; } var pbDataSlice: []u8 = undefined; pbDataSlice.len = plainblob.cbData; @@ -69,6 +71,8 @@ test "slice==unprotect(protect(slice))" { const unprotected = try unprotect(main.stackAllocator, protected); defer main.stackAllocator.free(unprotected); try std.testing.expectEqualSlices(u8, slice, unprotected); + } else { + return error.SkipZigTest; } } @@ -77,14 +81,18 @@ test "Protect fails on unsupported platforms" { if (!canProtect()) { try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + } else { + return error.SkipZigTest; } } test "Unprotect fails when supplied with garbage" { - const slices: [5][]u8 = .{@as([]u8, @constCast("TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj")), @as([]u8, @constCast("Test")), @as([]u8, @constCast("Testd")), @as([]u8, @constCast("")), @as([]u8, @constCast("WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"))}; - for (slices) |slice| { - if (canProtect()) { + if (canProtect()) { + const slices: [5][]u8 = .{@as([]u8, @constCast("TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj")), @as([]u8, @constCast("Test")), @as([]u8, @constCast("Testd")), @as([]u8, @constCast("")), @as([]u8, @constCast("WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"))}; + for (slices) |slice| { try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); } + } else { + return error.SkipZigTest; } } From acbe6a6e05bd699c97cf6ae3e4fdf5b3b074f120 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Fri, 14 Aug 2026 19:24:17 +0200 Subject: [PATCH 09/31] Format --- src/protect.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protect.zig b/src/protect.zig index 0bbc9a297d..412bc41f6c 100644 --- a/src/protect.zig +++ b/src/protect.zig @@ -45,7 +45,7 @@ pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, In else => { std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); return error.syserr; - } + }, } } var pbDataSlice: []u8 = undefined; From 9a2d5e5caacf03f296d8a82fa1929587b961918a Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 19:44:39 +0200 Subject: [PATCH 10/31] Move protect struct into authentication.zig --- src/main.zig | 1 - src/network/authentication.zig | 111 ++++++++++++++++++++++++++++++--- src/protect.zig | 98 ----------------------------- 3 files changed, 103 insertions(+), 107 deletions(-) delete mode 100644 src/protect.zig diff --git a/src/main.zig b/src/main.zig index 3340ca0bfa..d7e75925d4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -41,7 +41,6 @@ pub const utils = @import("utils.zig"); pub const vec = @import("vec.zig"); const zon = @import("zon.zig"); pub const ZonElement = zon.ZonElement; -pub const protect = @import("protect.zig"); const file_monitor = utils.file_monitor; diff --git a/src/network/authentication.zig b/src/network/authentication.zig index 8a2bc2a9be..fb3ed0f20d 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -1,11 +1,13 @@ const std = @import("std"); const main = @import("main"); +const builtin = @import("builtin"); const BinaryWriter = main.utils.BinaryWriter; const BinaryReader = main.utils.BinaryReader; const NeverFailingAllocator = main.heap.NeverFailingAllocator; const ZonElement = main.ZonElement; -const protect = main.protect; + +const c = @import("c"); var wordlist: ?[2048][]const u8 = null; @@ -285,6 +287,99 @@ pub const AccountCode = struct { } }; +pub const protection = struct { + pub inline fn canProtect() bool { + switch (builtin.os.tag) { + .windows => return true, + else => return false, + } + } + + pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Unsupported }![]u8 { + if (builtin.os.tag == .windows) { + var plainblob: c.DATA_BLOB = undefined; + var cipherblob: c.DATA_BLOB = undefined; + plainblob.cbData = @intCast(data.len); + plainblob.pbData = @as([*c]u8, data.ptr); + if (c.CryptProtectData(&plainblob, @as([*c]const c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &cipherblob) == 0) { + std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + return error.syserr; + } + defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + const out: []u8 = allocator.alloc(u8, @intCast(cipherblob.cbData)); + @memcpy(out, cipherblob.pbData); + return out; + } else { + return error.Unsupported; + } + } + + pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Invalid }![]u8 { + if (builtin.os.tag == .windows) { + var plainblob: c.DATA_BLOB = undefined; + var cipherblob: c.DATA_BLOB = undefined; + cipherblob.cbData = @intCast(data.len); + cipherblob.pbData = @as([*c]u8, data.ptr); + if (c.CryptUnprotectData(&cipherblob, @as([*c][*c]c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &plainblob) == 0) { + const err = c.GetLastError(); + switch (err) { + c.ERROR_INVALID_DATA, c.ERROR_INVALID_PARAMETER => return error.Invalid, + else => { + std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); + return error.syserr; + }, + } + } + var pbDataSlice: []u8 = undefined; + pbDataSlice.len = plainblob.cbData; + pbDataSlice.ptr = plainblob.pbData; + defer { + std.crypto.secureZero(u8, pbDataSlice); + if (c.LocalFree(plainblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + } + const out: []u8 = allocator.alloc(u8, @intCast(plainblob.cbData)); + @memcpy(out, plainblob.pbData); + return out; + } else { + return error.Invalid; + } + } + + test "slice==unprotect(protect(slice))" { + const slice: []u8 = @as([]u8, @constCast("Test")); + if (canProtect()) { + const protected = try protect(main.stackAllocator, slice); + defer main.stackAllocator.free(protected); + const unprotected = try unprotect(main.stackAllocator, protected); + defer main.stackAllocator.free(unprotected); + try std.testing.expectEqualSlices(u8, slice, unprotected); + } else { + return error.SkipZigTest; + } + } + + test "Protect fails on unsupported platforms" { + const slice: []u8 = @as([]u8, @constCast("Test")); + if (!canProtect()) { + try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); + try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + } else { + return error.SkipZigTest; + } + } + + test "Unprotect fails when supplied with garbage" { + if (canProtect()) { + const slices: [5][]u8 = .{@as([]u8, @constCast("TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj")), @as([]u8, @constCast("Test")), @as([]u8, @constCast("Testd")), @as([]u8, @constCast("")), @as([]u8, @constCast("WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"))}; + for (slices) |slice| { + try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + } + } else { + return error.SkipZigTest; + } + } +}; + pub const EncodingType = enum { none, argon2_aes_gcm }; pub const PasswordEncodedAccountCode = struct { @@ -313,10 +408,10 @@ pub const PasswordEncodedAccountCode = struct { main.io.random(&nonce); std.crypto.aead.aes_gcm.Aes256Gcm.encrypt(encryptedBuffer, &authenticationTag, accountCode.text, &.{}, nonce, key); - const protected = shouldProtect and protect.canProtect(); + const protected = shouldProtect and protection.canProtect(); var data: []u8 = undefined; if (protected) { - data = protect.protect(allocator, encryptedBuffer) catch |err| { + data = protection.protect(allocator, encryptedBuffer) catch |err| { if (err == error.syserr) return error.syserr else unreachable; }; defer allocator.free(encryptedBuffer); // Deferred, because that way even if a syserr is thrown it will still get freed @@ -334,10 +429,10 @@ pub const PasswordEncodedAccountCode = struct { } pub fn initUnencoded(allocator: NeverFailingAllocator, accountCode: AccountCode, shouldProtect: bool) error{syserr}!PasswordEncodedAccountCode { - const protected = shouldProtect and protect.canProtect(); + const protected = shouldProtect and protection.canProtect(); var data: []u8 = undefined; if (protected) { - data = protect.protect(allocator, accountCode.text) catch |err| { + data = protection.protect(allocator, accountCode.text) catch |err| { if (err == error.syserr) return error.syserr else unreachable; }; } else { @@ -361,11 +456,11 @@ pub const PasswordEncodedAccountCode = struct { } pub fn decryptFromPassword(self: PasswordEncodedAccountCode, password: []const u8, failureText: *main.ListManaged(u8)) !AccountCode { - if (self.protected and !protect.canProtect()) return error.Invalid; + if (self.protected and !protection.canProtect()) return error.Invalid; if (self.typ == .none) { var data = self.data; if (self.protected) { - data = try protect.unprotect(main.stackAllocator, data); + data = try protection.unprotect(main.stackAllocator, data); } defer if (self.protected) { std.crypto.secureZero(u8, data); @@ -382,7 +477,7 @@ pub const PasswordEncodedAccountCode = struct { .argon2_aes_gcm => { var data = self.data; if (self.protected) { - data = try protect.unprotect(main.stackAllocator, data); + data = try protection.unprotect(main.stackAllocator, data); } defer if (self.protected) { std.crypto.secureZero(u8, data); diff --git a/src/protect.zig b/src/protect.zig deleted file mode 100644 index 412bc41f6c..0000000000 --- a/src/protect.zig +++ /dev/null @@ -1,98 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); - -const main = @import("main"); -const NeverFailingAllocator = main.heap.NeverFailingAllocator; -const EncodingType = main.network.authentication.EncodingType; -const c = @import("c"); - -pub inline fn canProtect() bool { - switch (builtin.os.tag) { - .windows => return true, - else => return false, - } -} - -pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Unsupported }![]u8 { - if (builtin.os.tag == .windows) { - var plainblob: c.DATA_BLOB = undefined; - var cipherblob: c.DATA_BLOB = undefined; - plainblob.cbData = @intCast(data.len); - plainblob.pbData = @as([*c]u8, data.ptr); // Does this need to be secureZeroed? - if (c.CryptProtectData(&plainblob, @as([*c]const c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &cipherblob) == 0) { - std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); - return error.syserr; - } - defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); - const out: []u8 = allocator.alloc(u8, @intCast(cipherblob.cbData)); - @memcpy(out, cipherblob.pbData); - return out; - } else { - return error.Unsupported; - } -} - -pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Invalid }![]u8 { - if (builtin.os.tag == .windows) { - var plainblob: c.DATA_BLOB = undefined; - var cipherblob: c.DATA_BLOB = undefined; - cipherblob.cbData = @intCast(data.len); - cipherblob.pbData = @as([*c]u8, data.ptr); - if (c.CryptUnprotectData(&cipherblob, @as([*c][*c]c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &plainblob) == 0) { - const err = c.GetLastError(); - switch (err) { - c.ERROR_INVALID_DATA, c.ERROR_INVALID_PARAMETER => return error.Invalid, - else => { - std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); - return error.syserr; - }, - } - } - var pbDataSlice: []u8 = undefined; - pbDataSlice.len = plainblob.cbData; - pbDataSlice.ptr = plainblob.pbData; - defer { - std.crypto.secureZero(u8, pbDataSlice); - if (c.LocalFree(plainblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); - } - const out: []u8 = allocator.alloc(u8, @intCast(plainblob.cbData)); - @memcpy(out, plainblob.pbData); - return out; - } else { - return error.Invalid; - } -} - -test "slice==unprotect(protect(slice))" { - const slice: []u8 = @as([]u8, @constCast("Test")); - if (canProtect()) { - const protected = try protect(main.stackAllocator, slice); - defer main.stackAllocator.free(protected); - const unprotected = try unprotect(main.stackAllocator, protected); - defer main.stackAllocator.free(unprotected); - try std.testing.expectEqualSlices(u8, slice, unprotected); - } else { - return error.SkipZigTest; - } -} - -test "Protect fails on unsupported platforms" { - const slice: []u8 = @as([]u8, @constCast("Test")); - if (!canProtect()) { - try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); - try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); - } else { - return error.SkipZigTest; - } -} - -test "Unprotect fails when supplied with garbage" { - if (canProtect()) { - const slices: [5][]u8 = .{@as([]u8, @constCast("TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj")), @as([]u8, @constCast("Test")), @as([]u8, @constCast("Testd")), @as([]u8, @constCast("")), @as([]u8, @constCast("WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"))}; - for (slices) |slice| { - try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); - } - } else { - return error.SkipZigTest; - } -} From c0e64ea2cc7c7a3b426d879cca836646e5017329 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:52:50 +0200 Subject: [PATCH 11/31] Use Impl structs --- src/network/authentication.zig | 44 ++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index fb3ed0f20d..eddb93b3a1 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -288,15 +288,39 @@ pub const AccountCode = struct { }; pub const protection = struct { + const Impl = switch (builtin.os.tag) { + .windows => WindowsImpl, + else => NoImpl, + }; + pub inline fn canProtect() bool { - switch (builtin.os.tag) { - .windows => return true, - else => return false, - } + return Impl.canProtect; } pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Unsupported }![]u8 { - if (builtin.os.tag == .windows) { + return Impl.protect(allocator, data); + } + + pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Invalid }![]u8 { + return Impl.unprotect(allocator, data); + } + + const NoImpl = struct { + const canProtect = false; + + fn protect(_: NeverFailingAllocator, _: []u8) error{ syserr, Unsupported }![]u8 { + return error.Unsupported; + } + + fn unprotect(_: NeverFailingAllocator, _: []u8) error{ syserr, Invalid }![]u8 { + return error.Invalid; + } + }; + + const WindowsImpl = struct { + const canProtect = true; + + fn protect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Unsupported }![]u8 { var plainblob: c.DATA_BLOB = undefined; var cipherblob: c.DATA_BLOB = undefined; plainblob.cbData = @intCast(data.len); @@ -309,13 +333,9 @@ pub const protection = struct { const out: []u8 = allocator.alloc(u8, @intCast(cipherblob.cbData)); @memcpy(out, cipherblob.pbData); return out; - } else { - return error.Unsupported; } - } - pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Invalid }![]u8 { - if (builtin.os.tag == .windows) { + fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Invalid }![]u8 { var plainblob: c.DATA_BLOB = undefined; var cipherblob: c.DATA_BLOB = undefined; cipherblob.cbData = @intCast(data.len); @@ -340,10 +360,8 @@ pub const protection = struct { const out: []u8 = allocator.alloc(u8, @intCast(plainblob.cbData)); @memcpy(out, plainblob.pbData); return out; - } else { - return error.Invalid; } - } + }; test "slice==unprotect(protect(slice))" { const slice: []u8 = @as([]u8, @constCast("Test")); From 549329d55e9624dee340577302c6c4ee029ab12f Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:24:31 +0200 Subject: [PATCH 12/31] Couple simple changes --- src/network/authentication.zig | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index eddb93b3a1..f784668781 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -297,22 +297,22 @@ pub const protection = struct { return Impl.canProtect; } - pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Unsupported }![]u8 { + pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{ SystemError, Unsupported }![]u8 { return Impl.protect(allocator, data); } - pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Invalid }![]u8 { + pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ SystemError, Invalid }![]u8 { return Impl.unprotect(allocator, data); } const NoImpl = struct { const canProtect = false; - fn protect(_: NeverFailingAllocator, _: []u8) error{ syserr, Unsupported }![]u8 { + fn protect(_: NeverFailingAllocator, _: []u8) error{ SystemError, Unsupported }![]u8 { return error.Unsupported; } - fn unprotect(_: NeverFailingAllocator, _: []u8) error{ syserr, Invalid }![]u8 { + fn unprotect(_: NeverFailingAllocator, _: []u8) error{ SystemError, Invalid }![]u8 { return error.Invalid; } }; @@ -320,14 +320,14 @@ pub const protection = struct { const WindowsImpl = struct { const canProtect = true; - fn protect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Unsupported }![]u8 { + fn protect(allocator: NeverFailingAllocator, data: []u8) error{ SystemError, Unsupported }![]u8 { var plainblob: c.DATA_BLOB = undefined; var cipherblob: c.DATA_BLOB = undefined; plainblob.cbData = @intCast(data.len); - plainblob.pbData = @as([*c]u8, data.ptr); - if (c.CryptProtectData(&plainblob, @as([*c]const c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &cipherblob) == 0) { + plainblob.pbData = data.ptr; + if (c.CryptProtectData(&plainblob, null, null, null, null, 0, &cipherblob) == 0) { std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); - return error.syserr; + return error.SystemError; } defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); const out: []u8 = allocator.alloc(u8, @intCast(cipherblob.cbData)); @@ -335,7 +335,7 @@ pub const protection = struct { return out; } - fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ syserr, Invalid }![]u8 { + fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ SystemError, Invalid }![]u8 { var plainblob: c.DATA_BLOB = undefined; var cipherblob: c.DATA_BLOB = undefined; cipherblob.cbData = @intCast(data.len); @@ -346,7 +346,7 @@ pub const protection = struct { c.ERROR_INVALID_DATA, c.ERROR_INVALID_PARAMETER => return error.Invalid, else => { std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); - return error.syserr; + return error.SystemError; }, } } @@ -410,7 +410,7 @@ pub const PasswordEncodedAccountCode = struct { pub const empty: PasswordEncodedAccountCode = .{.typ = .none, .protected = false, .salt = &.{}, .nonce = &.{}, .data = &.{}, .authenticationTag = &.{}}; - pub fn initFromPassword(allocator: NeverFailingAllocator, accountCode: AccountCode, password: []const u8, shouldProtect: bool) error{syserr}!PasswordEncodedAccountCode { + pub fn initFromPassword(allocator: NeverFailingAllocator, accountCode: AccountCode, password: []const u8, shouldProtect: bool) error{SystemError}!PasswordEncodedAccountCode { var salt: [32]u8 = undefined; main.io.random(&salt); const saltBase64 = allocator.alloc(u8, std.base64.standard.Encoder.calcSize(salt.len)); @@ -430,9 +430,9 @@ pub const PasswordEncodedAccountCode = struct { var data: []u8 = undefined; if (protected) { data = protection.protect(allocator, encryptedBuffer) catch |err| { - if (err == error.syserr) return error.syserr else unreachable; + if (err == error.SystemError) return error.SystemError else unreachable; }; - defer allocator.free(encryptedBuffer); // Deferred, because that way even if a syserr is thrown it will still get freed + defer allocator.free(encryptedBuffer); // Deferred, because that way even if a SystemError is thrown it will still get freed } else { data = encryptedBuffer; } @@ -446,12 +446,12 @@ pub const PasswordEncodedAccountCode = struct { }; } - pub fn initUnencoded(allocator: NeverFailingAllocator, accountCode: AccountCode, shouldProtect: bool) error{syserr}!PasswordEncodedAccountCode { + pub fn initUnencoded(allocator: NeverFailingAllocator, accountCode: AccountCode, shouldProtect: bool) error{SystemError}!PasswordEncodedAccountCode { const protected = shouldProtect and protection.canProtect(); var data: []u8 = undefined; if (protected) { data = protection.protect(allocator, accountCode.text) catch |err| { - if (err == error.syserr) return error.syserr else unreachable; + if (err == error.SystemError) return error.SystemError else unreachable; }; } else { data = allocator.dupe(u8, accountCode.text); From 25424044aef7ca213011f33b72aaad24f8044ad0 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:39:39 +0200 Subject: [PATCH 13/31] Get rid of constCast in tests --- src/network/authentication.zig | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index f784668781..d57e9591f3 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -297,22 +297,22 @@ pub const protection = struct { return Impl.canProtect; } - pub fn protect(allocator: NeverFailingAllocator, data: []u8) error{ SystemError, Unsupported }![]u8 { + pub fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { return Impl.protect(allocator, data); } - pub fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ SystemError, Invalid }![]u8 { + pub fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { return Impl.unprotect(allocator, data); } const NoImpl = struct { const canProtect = false; - fn protect(_: NeverFailingAllocator, _: []u8) error{ SystemError, Unsupported }![]u8 { + fn protect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Unsupported }![]u8 { return error.Unsupported; } - fn unprotect(_: NeverFailingAllocator, _: []u8) error{ SystemError, Invalid }![]u8 { + fn unprotect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Invalid }![]u8 { return error.Invalid; } }; @@ -320,11 +320,11 @@ pub const protection = struct { const WindowsImpl = struct { const canProtect = true; - fn protect(allocator: NeverFailingAllocator, data: []u8) error{ SystemError, Unsupported }![]u8 { + fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { var plainblob: c.DATA_BLOB = undefined; var cipherblob: c.DATA_BLOB = undefined; plainblob.cbData = @intCast(data.len); - plainblob.pbData = data.ptr; + plainblob.pbData = @constCast(data.ptr); if (c.CryptProtectData(&plainblob, null, null, null, null, 0, &cipherblob) == 0) { std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); return error.SystemError; @@ -335,12 +335,12 @@ pub const protection = struct { return out; } - fn unprotect(allocator: NeverFailingAllocator, data: []u8) error{ SystemError, Invalid }![]u8 { + fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { var plainblob: c.DATA_BLOB = undefined; var cipherblob: c.DATA_BLOB = undefined; cipherblob.cbData = @intCast(data.len); - cipherblob.pbData = @as([*c]u8, data.ptr); - if (c.CryptUnprotectData(&cipherblob, @as([*c][*c]c_ushort, null), @as([*c]c.DATA_BLOB, null), null, @as([*c]c.CRYPTPROTECT_PROMPTSTRUCT, null), @as(c_ulong, 0), &plainblob) == 0) { + cipherblob.pbData = @constCast(data.ptr); + if (c.CryptUnprotectData(&cipherblob, null, null, null, null, 0, &plainblob) == 0) { const err = c.GetLastError(); switch (err) { c.ERROR_INVALID_DATA, c.ERROR_INVALID_PARAMETER => return error.Invalid, @@ -377,7 +377,7 @@ pub const protection = struct { } test "Protect fails on unsupported platforms" { - const slice: []u8 = @as([]u8, @constCast("Test")); + const slice = "Test"; if (!canProtect()) { try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); @@ -388,7 +388,7 @@ pub const protection = struct { test "Unprotect fails when supplied with garbage" { if (canProtect()) { - const slices: [5][]u8 = .{@as([]u8, @constCast("TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj")), @as([]u8, @constCast("Test")), @as([]u8, @constCast("Testd")), @as([]u8, @constCast("")), @as([]u8, @constCast("WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"))}; + const slices: [5][]const u8 = .{"TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj", "Test", "Testd", "", "WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"}; for (slices) |slice| { try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); } From 2cc59afac355fb145769debfdcf6903e2eefa9d5 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:40:33 +0200 Subject: [PATCH 14/31] Change Test Consistency. Oh, and better to be safe than sorry. --- src/network/authentication.zig | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index d57e9591f3..6e8f3e87ea 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -364,13 +364,15 @@ pub const protection = struct { }; test "slice==unprotect(protect(slice))" { - const slice: []u8 = @as([]u8, @constCast("Test")); if (canProtect()) { - const protected = try protect(main.stackAllocator, slice); - defer main.stackAllocator.free(protected); - const unprotected = try unprotect(main.stackAllocator, protected); - defer main.stackAllocator.free(unprotected); - try std.testing.expectEqualSlices(u8, slice, unprotected); + const slices: [5][]const u8 = .{"TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj", "Test", "Testd", "", "WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"}; + for (slices) |slice| { + const protected = try protect(main.stackAllocator, slice); + defer main.stackAllocator.free(protected); + const unprotected = try unprotect(main.stackAllocator, protected); + defer main.stackAllocator.free(unprotected); + try std.testing.expectEqualSlices(u8, slice, unprotected); + } } else { return error.SkipZigTest; } From 872f803d61a1eccc9f498614eab727c3f871103e Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 21:59:06 +0200 Subject: [PATCH 15/31] No more quiet fallback on abnormal failure --- .../windows/authentication/encrypt_with_password.zig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/gui/windows/authentication/encrypt_with_password.zig b/src/gui/windows/authentication/encrypt_with_password.zig index b65d967500..aaeb606165 100644 --- a/src/gui/windows/authentication/encrypt_with_password.zig +++ b/src/gui/windows/authentication/encrypt_with_password.zig @@ -41,10 +41,16 @@ pub fn setAccountCode(accountCode_: main.network.authentication.AccountCode) voi fn confirm() void { if (encryptAccountCode) { settings.storedAccount.deinit(main.globalAllocator); - settings.storedAccount = PasswordEncodedAccountCode.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, true) catch PasswordEncodedAccountCode.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, false) catch unreachable; + settings.storedAccount = PasswordEncodedAccountCode.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, true) catch |err| { + std.log.err("Could not protect: {}", .{err}); + return; + }; } else { settings.storedAccount.deinit(main.globalAllocator); - settings.storedAccount = PasswordEncodedAccountCode.initUnencoded(main.globalAllocator, accountCode, true) catch PasswordEncodedAccountCode.initUnencoded(main.globalAllocator, accountCode, false) catch unreachable; + settings.storedAccount = PasswordEncodedAccountCode.initUnencoded(main.globalAllocator, accountCode, true) catch |err| { + std.log.err("Could not protect: {}", .{err}); + return; + }; } settings.save(); From 4125aada0f0fe0468c3cb0d2d9910aab824af971 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:26:37 +0200 Subject: [PATCH 16/31] Use allocator.dupe instead of @memcpy --- src/network/authentication.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index 6e8f3e87ea..927065c594 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -330,9 +330,7 @@ pub const protection = struct { return error.SystemError; } defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); - const out: []u8 = allocator.alloc(u8, @intCast(cipherblob.cbData)); - @memcpy(out, cipherblob.pbData); - return out; + return allocator.dupe(u8, cipherblob.pbData[0..cipherblob.cbData]); } fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { From 3efe8581ea99f006b9c0273dd9bdc4b78bea61fe Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:47:39 +0200 Subject: [PATCH 17/31] Get rid of complex defers in decryptFromPassword --- src/network/authentication.zig | 36 ++++++++++++++-------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index 927065c594..cf6965bb6c 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -474,17 +474,20 @@ pub const PasswordEncodedAccountCode = struct { } pub fn decryptFromPassword(self: PasswordEncodedAccountCode, password: []const u8, failureText: *main.ListManaged(u8)) !AccountCode { - if (self.protected and !protection.canProtect()) return error.Invalid; - if (self.typ == .none) { - var data = self.data; - if (self.protected) { - data = try protection.unprotect(main.stackAllocator, data); + if (self.protected) { + if (!protection.canProtect()) return error.Invalid; + + var copy: PasswordEncodedAccountCode = self; + copy.protected = false; + copy.data = try protection.unprotect(main.stackAllocator, self.data); + defer { + std.crypto.secureZero(u8, copy.data); + main.stackAllocator.free(copy.data); } - defer if (self.protected) { - std.crypto.secureZero(u8, data); - main.stackAllocator.free(data); - }; - return AccountCode.initFromUserInput(data, failureText); + return decryptFromPassword(copy, password, failureText); + } + if (self.typ == .none) { + return AccountCode.initFromUserInput(self.data, failureText); } var key: [32]u8 = undefined; defer std.crypto.secureZero(u8, &key); @@ -493,23 +496,14 @@ pub const PasswordEncodedAccountCode = struct { switch (self.typ) { .none => unreachable, .argon2_aes_gcm => { - var data = self.data; - if (self.protected) { - data = try protection.unprotect(main.stackAllocator, data); - } - defer if (self.protected) { - std.crypto.secureZero(u8, data); - main.stackAllocator.free(data); - }; - if (self.authenticationTag.len != std.crypto.aead.aes_gcm.Aes256Gcm.tag_length) return error.Invalid; if (self.nonce.len != std.crypto.aead.aes_gcm.Aes256Gcm.nonce_length) return error.Invalid; const authenticationTag = self.authenticationTag[0..std.crypto.aead.aes_gcm.Aes256Gcm.tag_length]; const nonce = self.nonce[0..std.crypto.aead.aes_gcm.Aes256Gcm.nonce_length]; - const decryptedBuffer = main.stackAllocator.alloc(u8, data.len); + const decryptedBuffer = main.stackAllocator.alloc(u8, self.data.len); defer main.stackAllocator.free(decryptedBuffer); defer std.crypto.secureZero(u8, decryptedBuffer); - try std.crypto.aead.aes_gcm.Aes256Gcm.decrypt(decryptedBuffer, data, authenticationTag.*, &.{}, nonce.*, key); + try std.crypto.aead.aes_gcm.Aes256Gcm.decrypt(decryptedBuffer, self.data, authenticationTag.*, &.{}, nonce.*, key); return AccountCode.initFromUserInput(decryptedBuffer, failureText); }, } From bb85a8523a6400a2dc35cf35341f7613334d1ee1 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:58:09 +0200 Subject: [PATCH 18/31] Initialize blob directly --- src/network/authentication.zig | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index cf6965bb6c..bb09673714 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -321,10 +321,11 @@ pub const protection = struct { const canProtect = true; fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { - var plainblob: c.DATA_BLOB = undefined; + var plainblob: c.DATA_BLOB = .{ + .cbData = @intCast(data.len), + .pbData = @constCast(data.ptr), + }; var cipherblob: c.DATA_BLOB = undefined; - plainblob.cbData = @intCast(data.len); - plainblob.pbData = @constCast(data.ptr); if (c.CryptProtectData(&plainblob, null, null, null, null, 0, &cipherblob) == 0) { std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); return error.SystemError; @@ -335,9 +336,10 @@ pub const protection = struct { fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { var plainblob: c.DATA_BLOB = undefined; - var cipherblob: c.DATA_BLOB = undefined; - cipherblob.cbData = @intCast(data.len); - cipherblob.pbData = @constCast(data.ptr); + var cipherblob: c.DATA_BLOB = .{ + .cbData = @intCast(data.len), + .pbData = @constCast(data.ptr), + }; if (c.CryptUnprotectData(&cipherblob, null, null, null, null, 0, &plainblob) == 0) { const err = c.GetLastError(); switch (err) { @@ -355,9 +357,7 @@ pub const protection = struct { std.crypto.secureZero(u8, pbDataSlice); if (c.LocalFree(plainblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); } - const out: []u8 = allocator.alloc(u8, @intCast(plainblob.cbData)); - @memcpy(out, plainblob.pbData); - return out; + return allocator.dupe(u8, plainblob.pbData[0..plainblob.cbData]); } }; From aec72387cb9053268761738a9dc92a9b7d75ea09 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:11:48 +0200 Subject: [PATCH 19/31] Add checkbox Must test on linux --- .../authentication/encrypt_with_password.zig | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/gui/windows/authentication/encrypt_with_password.zig b/src/gui/windows/authentication/encrypt_with_password.zig index aaeb606165..19e9146537 100644 --- a/src/gui/windows/authentication/encrypt_with_password.zig +++ b/src/gui/windows/authentication/encrypt_with_password.zig @@ -23,12 +23,14 @@ pub var window = GuiWindow{ var innerList: *VerticalList = undefined; var encryptWithPasswordCheckbox: *CheckBox = undefined; +var protectCheckbox: *CheckBox = undefined; var passwordTextField: *TextInput = undefined; var passwordRow: *HorizontalList = undefined; var confirmButton: *Button = undefined; var encryptAccountCode: bool = true; +var protectAccountCode: bool = main.network.authentication.protection.canProtect(); const padding: f32 = 8; @@ -41,13 +43,13 @@ pub fn setAccountCode(accountCode_: main.network.authentication.AccountCode) voi fn confirm() void { if (encryptAccountCode) { settings.storedAccount.deinit(main.globalAllocator); - settings.storedAccount = PasswordEncodedAccountCode.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, true) catch |err| { + settings.storedAccount = PasswordEncodedAccountCode.initFromPassword(main.globalAllocator, accountCode, passwordTextField.currentString.items, protectAccountCode) catch |err| { std.log.err("Could not protect: {}", .{err}); return; }; } else { settings.storedAccount.deinit(main.globalAllocator); - settings.storedAccount = PasswordEncodedAccountCode.initUnencoded(main.globalAllocator, accountCode, true) catch |err| { + settings.storedAccount = PasswordEncodedAccountCode.initUnencoded(main.globalAllocator, accountCode, protectAccountCode) catch |err| { std.log.err("Could not protect: {}", .{err}); return; }; @@ -63,9 +65,15 @@ fn encryptAccountCodeCallback(encryptAccountCode_: bool) void { refreshInner(); } +fn protectAccountCodeCallback(protectAccountCode_: bool) void { + protectAccountCode = protectAccountCode_; + refreshInner(); +} + fn refreshInner() void { innerList.children.clearRetainingCapacity(); innerList.children.append(encryptWithPasswordCheckbox.toComponent()); + if (main.network.authentication.protection.canProtect()) innerList.children.append(protectCheckbox.toComponent()); if (encryptAccountCode) { innerList.children.append(passwordRow.toComponent()); } @@ -83,6 +91,10 @@ pub fn onOpen() void { const width = 480; list.add(Label.init(.{0, 0}, width, "Your Account Code will be stored in your settings to allow you to stay logged in. Please decide how we should store it:", .left)); innerList = VerticalList.init(.{0, 0}, 100, 16); + if (main.network.authentication.protection.canProtect()) { + protectCheckbox = CheckBox.init(.{0, 0}, width, "Protect with system api (recommended)", protectAccountCode, &protectAccountCodeCallback); + innerList.add(protectCheckbox); + } encryptWithPasswordCheckbox = CheckBox.init(.{0, 0}, width, "Encrypt it with a password (recommended)\n(The password needs to be entered every time)", encryptAccountCode, &encryptAccountCodeCallback); innerList.add(encryptWithPasswordCheckbox); passwordRow = HorizontalList.init(); From 7b6f395a27bc1c6926ddfe17215a06fb0ba92490 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:02:15 +0200 Subject: [PATCH 20/31] Move protections struct to separate file --- src/network/authentication.zig | 114 +------------------------------- src/network/protection.zig | 116 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 113 deletions(-) create mode 100644 src/network/protection.zig diff --git a/src/network/authentication.zig b/src/network/authentication.zig index bb09673714..792266ac05 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -1,13 +1,12 @@ const std = @import("std"); const main = @import("main"); -const builtin = @import("builtin"); const BinaryWriter = main.utils.BinaryWriter; const BinaryReader = main.utils.BinaryReader; const NeverFailingAllocator = main.heap.NeverFailingAllocator; const ZonElement = main.ZonElement; -const c = @import("c"); +pub const protection = @import("protection.zig"); var wordlist: ?[2048][]const u8 = null; @@ -287,117 +286,6 @@ pub const AccountCode = struct { } }; -pub const protection = struct { - const Impl = switch (builtin.os.tag) { - .windows => WindowsImpl, - else => NoImpl, - }; - - pub inline fn canProtect() bool { - return Impl.canProtect; - } - - pub fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { - return Impl.protect(allocator, data); - } - - pub fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { - return Impl.unprotect(allocator, data); - } - - const NoImpl = struct { - const canProtect = false; - - fn protect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Unsupported }![]u8 { - return error.Unsupported; - } - - fn unprotect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Invalid }![]u8 { - return error.Invalid; - } - }; - - const WindowsImpl = struct { - const canProtect = true; - - fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { - var plainblob: c.DATA_BLOB = .{ - .cbData = @intCast(data.len), - .pbData = @constCast(data.ptr), - }; - var cipherblob: c.DATA_BLOB = undefined; - if (c.CryptProtectData(&plainblob, null, null, null, null, 0, &cipherblob) == 0) { - std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); - return error.SystemError; - } - defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); - return allocator.dupe(u8, cipherblob.pbData[0..cipherblob.cbData]); - } - - fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { - var plainblob: c.DATA_BLOB = undefined; - var cipherblob: c.DATA_BLOB = .{ - .cbData = @intCast(data.len), - .pbData = @constCast(data.ptr), - }; - if (c.CryptUnprotectData(&cipherblob, null, null, null, null, 0, &plainblob) == 0) { - const err = c.GetLastError(); - switch (err) { - c.ERROR_INVALID_DATA, c.ERROR_INVALID_PARAMETER => return error.Invalid, - else => { - std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); - return error.SystemError; - }, - } - } - var pbDataSlice: []u8 = undefined; - pbDataSlice.len = plainblob.cbData; - pbDataSlice.ptr = plainblob.pbData; - defer { - std.crypto.secureZero(u8, pbDataSlice); - if (c.LocalFree(plainblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); - } - return allocator.dupe(u8, plainblob.pbData[0..plainblob.cbData]); - } - }; - - test "slice==unprotect(protect(slice))" { - if (canProtect()) { - const slices: [5][]const u8 = .{"TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj", "Test", "Testd", "", "WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"}; - for (slices) |slice| { - const protected = try protect(main.stackAllocator, slice); - defer main.stackAllocator.free(protected); - const unprotected = try unprotect(main.stackAllocator, protected); - defer main.stackAllocator.free(unprotected); - try std.testing.expectEqualSlices(u8, slice, unprotected); - } - } else { - return error.SkipZigTest; - } - } - - test "Protect fails on unsupported platforms" { - const slice = "Test"; - if (!canProtect()) { - try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); - try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); - } else { - return error.SkipZigTest; - } - } - - test "Unprotect fails when supplied with garbage" { - if (canProtect()) { - const slices: [5][]const u8 = .{"TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj", "Test", "Testd", "", "WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"}; - for (slices) |slice| { - try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); - } - } else { - return error.SkipZigTest; - } - } -}; - pub const EncodingType = enum { none, argon2_aes_gcm }; pub const PasswordEncodedAccountCode = struct { diff --git a/src/network/protection.zig b/src/network/protection.zig new file mode 100644 index 0000000000..f377c2a7de --- /dev/null +++ b/src/network/protection.zig @@ -0,0 +1,116 @@ +const std = @import("std"); + +const main = @import("main"); +const NeverFailingAllocator = main.heap.NeverFailingAllocator; +const builtin = @import("builtin"); + +const c = @import("c"); + +const Impl = switch (builtin.os.tag) { + .windows => WindowsImpl, + else => NoImpl, +}; + +pub inline fn canProtect() bool { + return Impl.canProtect; +} + +pub fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { + return Impl.protect(allocator, data); +} + +pub fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { + return Impl.unprotect(allocator, data); +} + +const NoImpl = struct { + const canProtect = false; + + fn protect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Unsupported }![]u8 { + return error.Unsupported; + } + + fn unprotect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Invalid }![]u8 { + return error.Invalid; + } +}; + +const WindowsImpl = struct { + const canProtect = true; + + fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { + var plainblob: c.DATA_BLOB = .{ + .cbData = @intCast(data.len), + .pbData = @constCast(data.ptr), + }; + var cipherblob: c.DATA_BLOB = undefined; + if (c.CryptProtectData(&plainblob, null, null, null, null, 0, &cipherblob) == 0) { + std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + return error.SystemError; + } + defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + return allocator.dupe(u8, cipherblob.pbData[0..cipherblob.cbData]); + } + + fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { + var plainblob: c.DATA_BLOB = undefined; + var cipherblob: c.DATA_BLOB = .{ + .cbData = @intCast(data.len), + .pbData = @constCast(data.ptr), + }; + if (c.CryptUnprotectData(&cipherblob, null, null, null, null, 0, &plainblob) == 0) { + const err = c.GetLastError(); + switch (err) { + c.ERROR_INVALID_DATA, c.ERROR_INVALID_PARAMETER => return error.Invalid, + else => { + std.log.err("CryptUnprotectData syscall failed. Errorcode: {}", .{err}); + return error.SystemError; + }, + } + } + var pbDataSlice: []u8 = undefined; + pbDataSlice.len = plainblob.cbData; + pbDataSlice.ptr = plainblob.pbData; + defer { + std.crypto.secureZero(u8, pbDataSlice); + if (c.LocalFree(plainblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + } + return allocator.dupe(u8, plainblob.pbData[0..plainblob.cbData]); + } +}; + +test "slice==unprotect(protect(slice))" { + if (canProtect()) { + const slices: [5][]const u8 = .{"TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj", "Test", "Testd", "", "WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"}; + for (slices) |slice| { + const protected = try protect(main.stackAllocator, slice); + defer main.stackAllocator.free(protected); + const unprotected = try unprotect(main.stackAllocator, protected); + defer main.stackAllocator.free(unprotected); + try std.testing.expectEqualSlices(u8, slice, unprotected); + } + } else { + return error.SkipZigTest; + } +} + +test "Protect fails on unsupported platforms" { + const slice = "Test"; + if (!canProtect()) { + try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); + try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + } else { + return error.SkipZigTest; + } +} + +test "Unprotect fails when supplied with garbage" { + if (canProtect()) { + const slices: [5][]const u8 = .{"TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj", "Test", "Testd", "", "WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"}; + for (slices) |slice| { + try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); + } + } else { + return error.SkipZigTest; + } +} From b129ee7bfac72868ea3dab29e96680ac2b443f28 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:17:52 +0200 Subject: [PATCH 21/31] Make canProtect a const bool --- .../windows/authentication/encrypt_with_password.zig | 6 +++--- src/network/authentication.zig | 6 +++--- src/network/protection.zig | 10 ++++------ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/gui/windows/authentication/encrypt_with_password.zig b/src/gui/windows/authentication/encrypt_with_password.zig index 19e9146537..73663f8016 100644 --- a/src/gui/windows/authentication/encrypt_with_password.zig +++ b/src/gui/windows/authentication/encrypt_with_password.zig @@ -30,7 +30,7 @@ var passwordRow: *HorizontalList = undefined; var confirmButton: *Button = undefined; var encryptAccountCode: bool = true; -var protectAccountCode: bool = main.network.authentication.protection.canProtect(); +var protectAccountCode: bool = main.network.authentication.protection.canProtect; const padding: f32 = 8; @@ -73,7 +73,7 @@ fn protectAccountCodeCallback(protectAccountCode_: bool) void { fn refreshInner() void { innerList.children.clearRetainingCapacity(); innerList.children.append(encryptWithPasswordCheckbox.toComponent()); - if (main.network.authentication.protection.canProtect()) innerList.children.append(protectCheckbox.toComponent()); + if (main.network.authentication.protection.canProtect) innerList.children.append(protectCheckbox.toComponent()); if (encryptAccountCode) { innerList.children.append(passwordRow.toComponent()); } @@ -91,7 +91,7 @@ pub fn onOpen() void { const width = 480; list.add(Label.init(.{0, 0}, width, "Your Account Code will be stored in your settings to allow you to stay logged in. Please decide how we should store it:", .left)); innerList = VerticalList.init(.{0, 0}, 100, 16); - if (main.network.authentication.protection.canProtect()) { + if (main.network.authentication.protection.canProtect) { protectCheckbox = CheckBox.init(.{0, 0}, width, "Protect with system api (recommended)", protectAccountCode, &protectAccountCodeCallback); innerList.add(protectCheckbox); } diff --git a/src/network/authentication.zig b/src/network/authentication.zig index 792266ac05..b6c23551ba 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -314,7 +314,7 @@ pub const PasswordEncodedAccountCode = struct { main.io.random(&nonce); std.crypto.aead.aes_gcm.Aes256Gcm.encrypt(encryptedBuffer, &authenticationTag, accountCode.text, &.{}, nonce, key); - const protected = shouldProtect and protection.canProtect(); + const protected = shouldProtect and protection.canProtect; var data: []u8 = undefined; if (protected) { data = protection.protect(allocator, encryptedBuffer) catch |err| { @@ -335,7 +335,7 @@ pub const PasswordEncodedAccountCode = struct { } pub fn initUnencoded(allocator: NeverFailingAllocator, accountCode: AccountCode, shouldProtect: bool) error{SystemError}!PasswordEncodedAccountCode { - const protected = shouldProtect and protection.canProtect(); + const protected = shouldProtect and protection.canProtect; var data: []u8 = undefined; if (protected) { data = protection.protect(allocator, accountCode.text) catch |err| { @@ -363,7 +363,7 @@ pub const PasswordEncodedAccountCode = struct { pub fn decryptFromPassword(self: PasswordEncodedAccountCode, password: []const u8, failureText: *main.ListManaged(u8)) !AccountCode { if (self.protected) { - if (!protection.canProtect()) return error.Invalid; + if (!protection.canProtect) return error.Invalid; var copy: PasswordEncodedAccountCode = self; copy.protected = false; diff --git a/src/network/protection.zig b/src/network/protection.zig index f377c2a7de..eae529455e 100644 --- a/src/network/protection.zig +++ b/src/network/protection.zig @@ -11,9 +11,7 @@ const Impl = switch (builtin.os.tag) { else => NoImpl, }; -pub inline fn canProtect() bool { - return Impl.canProtect; -} +pub const canProtect: bool = Impl.canProtect; pub fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { return Impl.protect(allocator, data); @@ -80,7 +78,7 @@ const WindowsImpl = struct { }; test "slice==unprotect(protect(slice))" { - if (canProtect()) { + if (canProtect) { const slices: [5][]const u8 = .{"TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj", "Test", "Testd", "", "WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"}; for (slices) |slice| { const protected = try protect(main.stackAllocator, slice); @@ -96,7 +94,7 @@ test "slice==unprotect(protect(slice))" { test "Protect fails on unsupported platforms" { const slice = "Test"; - if (!canProtect()) { + if (!canProtect) { try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); } else { @@ -105,7 +103,7 @@ test "Protect fails on unsupported platforms" { } test "Unprotect fails when supplied with garbage" { - if (canProtect()) { + if (canProtect) { const slices: [5][]const u8 = .{"TestdwadadÖOUWHdöouHIOSUdhöoUHNWLJDKNOÖPAHUIwdoöJKNSdlkjöwuHOÖIhso8zpo9IKj", "Test", "Testd", "", "WIJDp8iU)(du098UÜ=JHd0ü8hz=Ü(HJ0isidjowi8h=(Z\"ß08IJUISdhd0w98hdoi8uoIWUJDoikjsoIKHJOwiuhdOISHNdo9i8H(UIHNASUJhdnbiuJBWGiudjhbIAKUJHnbsiudjkhiWUAHNIUDshjliuAHELIUHFILUHNIUJBDIUHwiuHushoujhdiiuwhIUHsouhdUHwiuhdUAHLsuidhlHU)"}; for (slices) |slice| { try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); From f4e40364c672c0c054870f2501a23ea18e6f97bc Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Fri, 21 Aug 2026 22:27:46 +0200 Subject: [PATCH 22/31] Change names to fit naming convention --- src/network/protection.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/network/protection.zig b/src/network/protection.zig index eae529455e..8f7454309b 100644 --- a/src/network/protection.zig +++ b/src/network/protection.zig @@ -6,22 +6,22 @@ const builtin = @import("builtin"); const c = @import("c"); -const Impl = switch (builtin.os.tag) { - .windows => WindowsImpl, - else => NoImpl, +const impl = switch (builtin.os.tag) { + .windows => windows_impl, + else => no_impl, }; -pub const canProtect: bool = Impl.canProtect; +pub const canProtect: bool = impl.canProtect; pub fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { - return Impl.protect(allocator, data); + return impl.protect(allocator, data); } pub fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 { - return Impl.unprotect(allocator, data); + return impl.unprotect(allocator, data); } -const NoImpl = struct { +const no_impl = struct { const canProtect = false; fn protect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Unsupported }![]u8 { @@ -33,7 +33,7 @@ const NoImpl = struct { } }; -const WindowsImpl = struct { +const windows_impl = struct { const canProtect = true; fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { From a9cb8498c5385abbb7f5b96b76dca266bc2c0b33 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Mon, 24 Aug 2026 10:37:37 +0200 Subject: [PATCH 23/31] Get rid of unnecessary pub --- src/network/authentication.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index b6c23551ba..f39c7b3c32 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -286,7 +286,7 @@ pub const AccountCode = struct { } }; -pub const EncodingType = enum { none, argon2_aes_gcm }; +const EncodingType = enum { none, argon2_aes_gcm }; pub const PasswordEncodedAccountCode = struct { typ: EncodingType, From ba419a536dd9e58b727a18c9a951c470d87873c8 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:54:34 +0200 Subject: [PATCH 24/31] Update authentication.zig --- src/network/authentication.zig | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index f39c7b3c32..98ce37d212 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -317,10 +317,13 @@ pub const PasswordEncodedAccountCode = struct { const protected = shouldProtect and protection.canProtect; var data: []u8 = undefined; if (protected) { + defer allocator.free(encryptedBuffer); data = protection.protect(allocator, encryptedBuffer) catch |err| { - if (err == error.SystemError) return error.SystemError else unreachable; + switch (err) { + .SystemError => return error.SystemError, + .Unsupported => unreachable, + } }; - defer allocator.free(encryptedBuffer); // Deferred, because that way even if a SystemError is thrown it will still get freed } else { data = encryptedBuffer; } @@ -339,7 +342,10 @@ pub const PasswordEncodedAccountCode = struct { var data: []u8 = undefined; if (protected) { data = protection.protect(allocator, accountCode.text) catch |err| { - if (err == error.SystemError) return error.SystemError else unreachable; + switch (err) { + .SystemError => return error.SystemError, + .Unsupported => unreachable, + } }; } else { data = allocator.dupe(u8, accountCode.text); From 6d245a9dd7e1d38f4c773c646fe3f71473f03a8e Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:07:13 +0200 Subject: [PATCH 25/31] Update authentication.zig --- src/network/authentication.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index 98ce37d212..3513948296 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -320,8 +320,8 @@ pub const PasswordEncodedAccountCode = struct { defer allocator.free(encryptedBuffer); data = protection.protect(allocator, encryptedBuffer) catch |err| { switch (err) { - .SystemError => return error.SystemError, - .Unsupported => unreachable, + error.SystemError => return error.SystemError, + error.Unsupported => unreachable, } }; } else { @@ -343,8 +343,8 @@ pub const PasswordEncodedAccountCode = struct { if (protected) { data = protection.protect(allocator, accountCode.text) catch |err| { switch (err) { - .SystemError => return error.SystemError, - .Unsupported => unreachable, + error.SystemError => return error.SystemError, + error.Unsupported => unreachable, } }; } else { From b0f5648da936eaef0af8881596e518bf9cd11d6e Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:02:33 +0200 Subject: [PATCH 26/31] Update encrypt_with_password.zig --- src/gui/windows/authentication/encrypt_with_password.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/windows/authentication/encrypt_with_password.zig b/src/gui/windows/authentication/encrypt_with_password.zig index 73663f8016..b7b23c888b 100644 --- a/src/gui/windows/authentication/encrypt_with_password.zig +++ b/src/gui/windows/authentication/encrypt_with_password.zig @@ -92,7 +92,7 @@ pub fn onOpen() void { list.add(Label.init(.{0, 0}, width, "Your Account Code will be stored in your settings to allow you to stay logged in. Please decide how we should store it:", .left)); innerList = VerticalList.init(.{0, 0}, 100, 16); if (main.network.authentication.protection.canProtect) { - protectCheckbox = CheckBox.init(.{0, 0}, width, "Protect with system api (recommended)", protectAccountCode, &protectAccountCodeCallback); + protectCheckbox = CheckBox.init(.{0, 0}, width, "Protect from theft (recommended)\nForces re-authentication when device changes", protectAccountCode, &protectAccountCodeCallback); innerList.add(protectCheckbox); } encryptWithPasswordCheckbox = CheckBox.init(.{0, 0}, width, "Encrypt it with a password (recommended)\n(The password needs to be entered every time)", encryptAccountCode, &encryptAccountCodeCallback); From 354ea9e78a9e03bbdb6b229607ffaab8dd69b1ee Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:27:00 +0200 Subject: [PATCH 27/31] Remove error.Unsupported If error. Unsupported is raised, then a programmer is using the API incorrectly. Errors like these warrant a panic and do not need to be recoverable. --- src/network/authentication.zig | 14 ++------------ src/network/protection.zig | 11 +++++------ 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index 3513948296..d81d0d47dd 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -318,12 +318,7 @@ pub const PasswordEncodedAccountCode = struct { var data: []u8 = undefined; if (protected) { defer allocator.free(encryptedBuffer); - data = protection.protect(allocator, encryptedBuffer) catch |err| { - switch (err) { - error.SystemError => return error.SystemError, - error.Unsupported => unreachable, - } - }; + data = try protection.protect(allocator, encryptedBuffer); } else { data = encryptedBuffer; } @@ -341,12 +336,7 @@ pub const PasswordEncodedAccountCode = struct { const protected = shouldProtect and protection.canProtect; var data: []u8 = undefined; if (protected) { - data = protection.protect(allocator, accountCode.text) catch |err| { - switch (err) { - error.SystemError => return error.SystemError, - error.Unsupported => unreachable, - } - }; + data = try protection.protect(allocator, accountCode.text); } else { data = allocator.dupe(u8, accountCode.text); } diff --git a/src/network/protection.zig b/src/network/protection.zig index 8f7454309b..3d33e85b14 100644 --- a/src/network/protection.zig +++ b/src/network/protection.zig @@ -13,7 +13,7 @@ const impl = switch (builtin.os.tag) { pub const canProtect: bool = impl.canProtect; -pub fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { +pub fn protect(allocator: NeverFailingAllocator, data: []const u8) error{SystemError}![]u8 { return impl.protect(allocator, data); } @@ -24,8 +24,8 @@ pub fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ Syst const no_impl = struct { const canProtect = false; - fn protect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Unsupported }![]u8 { - return error.Unsupported; + fn protect(_: NeverFailingAllocator, _: []const u8) error{SystemError}![]u8 { + @panic("Protection API not implemented on this device. Always check protection.canProtect before trying to use this API."); } fn unprotect(_: NeverFailingAllocator, _: []const u8) error{ SystemError, Invalid }![]u8 { @@ -36,7 +36,7 @@ const no_impl = struct { const windows_impl = struct { const canProtect = true; - fn protect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Unsupported }![]u8 { + fn protect(allocator: NeverFailingAllocator, data: []const u8) error{SystemError}![]u8 { var plainblob: c.DATA_BLOB = .{ .cbData = @intCast(data.len), .pbData = @constCast(data.ptr), @@ -92,10 +92,9 @@ test "slice==unprotect(protect(slice))" { } } -test "Protect fails on unsupported platforms" { +test "unprotect fails on unsupported platforms" { const slice = "Test"; if (!canProtect) { - try std.testing.expectError(error.Unsupported, protect(main.stackAllocator, slice)); try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); } else { return error.SkipZigTest; From 37178319415fb7b57f70a1db18783bd048b8d4a1 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:41:14 +0200 Subject: [PATCH 28/31] Mini naming change --- src/network/protection.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/protection.zig b/src/network/protection.zig index 3d33e85b14..ab7999e627 100644 --- a/src/network/protection.zig +++ b/src/network/protection.zig @@ -92,7 +92,7 @@ test "slice==unprotect(protect(slice))" { } } -test "unprotect fails on unsupported platforms" { +test "Unprotect fails on unsupported platforms" { const slice = "Test"; if (!canProtect) { try std.testing.expectError(error.Invalid, unprotect(main.stackAllocator, slice)); From 728c5a74d10fd7bc7ca96fc879a13854ff47de85 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:41:08 +0200 Subject: [PATCH 29/31] Remove "Please report to maintainers" notice --- src/network/protection.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/network/protection.zig b/src/network/protection.zig index ab7999e627..847314350a 100644 --- a/src/network/protection.zig +++ b/src/network/protection.zig @@ -43,10 +43,10 @@ const windows_impl = struct { }; var cipherblob: c.DATA_BLOB = undefined; if (c.CryptProtectData(&plainblob, null, null, null, null, 0, &cipherblob) == 0) { - std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + std.log.err("CryptProtectData syscall failed. Errorcode: {}. This should never happen.", .{c.GetLastError()}); return error.SystemError; } - defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + defer if (c.LocalFree(cipherblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen.", .{c.GetLastError()}); return allocator.dupe(u8, cipherblob.pbData[0..cipherblob.cbData]); } @@ -71,7 +71,7 @@ const windows_impl = struct { pbDataSlice.ptr = plainblob.pbData; defer { std.crypto.secureZero(u8, pbDataSlice); - if (c.LocalFree(plainblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen. Please report it to the maintainers.", .{c.GetLastError()}); + if (c.LocalFree(plainblob.pbData) != null) std.log.err("LocalFree syscall failed to free previously allocated memory. Errorcode: {}. This should never happen.", .{c.GetLastError()}); } return allocator.dupe(u8, plainblob.pbData[0..plainblob.cbData]); } From fd469e5cf50effc3be58f9b3ba5850db11a191ef Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:51:21 +0200 Subject: [PATCH 30/31] Increase window hight --- src/gui/windows/authentication/encrypt_with_password.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/windows/authentication/encrypt_with_password.zig b/src/gui/windows/authentication/encrypt_with_password.zig index b7b23c888b..d4a1f607b4 100644 --- a/src/gui/windows/authentication/encrypt_with_password.zig +++ b/src/gui/windows/authentication/encrypt_with_password.zig @@ -90,7 +90,7 @@ pub fn onOpen() void { const list = VerticalList.init(.{padding, 16 + padding}, 320, 8); const width = 480; list.add(Label.init(.{0, 0}, width, "Your Account Code will be stored in your settings to allow you to stay logged in. Please decide how we should store it:", .left)); - innerList = VerticalList.init(.{0, 0}, 100, 16); + innerList = VerticalList.init(.{0, 0}, 120, 16); if (main.network.authentication.protection.canProtect) { protectCheckbox = CheckBox.init(.{0, 0}, width, "Protect from theft (recommended)\nForces re-authentication when device changes", protectAccountCode, &protectAccountCodeCallback); innerList.add(protectCheckbox); From b01321c9a7dad7caa0819a8ad9e65fb516c5f334 Mon Sep 17 00:00:00 2001 From: Mabeeck <84404489+Mabeeck@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:13:51 +0200 Subject: [PATCH 31/31] Move defer As I see it, this is the simplest way to have the defer directly below the recource creation. That makes one more allocation than necessary when shouldProtect is false, but if it improves readability, then that should be fine, since this is not performance critical code. --- src/network/authentication.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/network/authentication.zig b/src/network/authentication.zig index d81d0d47dd..57778915b6 100644 --- a/src/network/authentication.zig +++ b/src/network/authentication.zig @@ -308,7 +308,8 @@ pub const PasswordEncodedAccountCode = struct { defer std.crypto.secureZero(u8, &key); keyFromPassword(.argon2_aes_gcm, saltBase64, password, &key); - const encryptedBuffer = allocator.alloc(u8, accountCode.text.len); + const encryptedBuffer = main.stackAllocator.alloc(u8, accountCode.text.len); + defer main.stackAllocator.free(encryptedBuffer); var authenticationTag: [std.crypto.aead.aes_gcm.Aes256Gcm.tag_length]u8 = undefined; var nonce: [std.crypto.aead.aes_gcm.Aes256Gcm.nonce_length]u8 = undefined; main.io.random(&nonce); @@ -317,10 +318,9 @@ pub const PasswordEncodedAccountCode = struct { const protected = shouldProtect and protection.canProtect; var data: []u8 = undefined; if (protected) { - defer allocator.free(encryptedBuffer); data = try protection.protect(allocator, encryptedBuffer); } else { - data = encryptedBuffer; + data = allocator.dupe(u8, encryptedBuffer); } return .{ .typ = .argon2_aes_gcm,