Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
355794b
Add a Cubyz protection api and integrate it
Mabeeck Aug 11, 2026
7aa8be7
Lots of things
Mabeeck Aug 12, 2026
5e6f73c
Format
Mabeeck Aug 12, 2026
5201c5c
All hail The Linter!
Mabeeck Aug 12, 2026
c47ab58
Microslop
Mabeeck Aug 12, 2026
9185e3e
Add tests and improve unprotect error detection
Mabeeck Aug 12, 2026
8fc9cc6
Improve Tests and errordetection
Mabeeck Aug 12, 2026
a1b7390
Improve based on received criticism
Mabeeck Aug 14, 2026
acbe6a6
Format
Mabeeck Aug 14, 2026
9a2d5e5
Move protect struct into authentication.zig
Mabeeck Aug 16, 2026
c0e64ea
Use Impl structs
Mabeeck Aug 16, 2026
549329d
Couple simple changes
Mabeeck Aug 16, 2026
2542404
Get rid of constCast in tests
Mabeeck Aug 16, 2026
2cc59af
Change Test
Mabeeck Aug 16, 2026
872f803
No more quiet fallback on abnormal failure
Mabeeck Aug 16, 2026
4125aad
Use allocator.dupe instead of @memcpy
Mabeeck Aug 16, 2026
3efe858
Get rid of complex defers in decryptFromPassword
Mabeeck Aug 16, 2026
bb85a85
Initialize blob directly
Mabeeck Aug 16, 2026
aec7238
Add checkbox
Mabeeck Aug 17, 2026
7b6f395
Move protections struct to separate file
Mabeeck Aug 21, 2026
b129ee7
Make canProtect a const bool
Mabeeck Aug 21, 2026
f4e4036
Change names to fit naming convention
Mabeeck Aug 21, 2026
a9cb849
Get rid of unnecessary pub
Mabeeck Aug 24, 2026
ba419a5
Update authentication.zig
Mabeeck Aug 24, 2026
6d245a9
Update authentication.zig
Mabeeck Aug 24, 2026
b0f5648
Update encrypt_with_password.zig
Mabeeck Aug 24, 2026
354ea9e
Remove error.Unsupported
Mabeeck Aug 24, 2026
3717831
Mini naming change
Mabeeck Aug 24, 2026
728c5a7
Remove "Please report to maintainers" notice
Mabeeck Aug 30, 2026
fd469e5
Increase window hight
Mabeeck Aug 30, 2026
b01321c
Move defer
Mabeeck Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/c.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@
#include <errno.h>
#endif

// Used for platform-speciffic keystorage
#ifdef _WIN32
#pragma comment(lib, "crypt32.lib")
#include <wincrypt.h>
#include <errhandlingapi.h>
#endif

// used for audio
#include <miniaudio.h>
#define STB_VORBIS_HEADER_ONLY
Expand Down
25 changes: 22 additions & 3 deletions src/gui/windows/authentication/encrypt_with_password.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Label = GuiComponent.Label;
const HorizontalList = GuiComponent.HorizontalList;
const TextInput = GuiComponent.TextInput;
const VerticalList = GuiComponent.VerticalList;
const PasswordEncodedAccountCode = main.network.authentication.PasswordEncodedAccountCode;

pub var window = GuiWindow{
.contentSize = Vec2f{128, 256},
Expand All @@ -22,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;

Expand All @@ -40,10 +43,16 @@ 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 = 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 = .initUnencoded(main.globalAllocator, accountCode);
settings.storedAccount = PasswordEncodedAccountCode.initUnencoded(main.globalAllocator, accountCode, protectAccountCode) catch |err| {
std.log.err("Could not protect: {}", .{err});
return;
};
}
settings.save();

Expand All @@ -56,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());
}
Expand All @@ -75,7 +90,11 @@ 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is too generic. Protection from theft can mean a lot of things.

I'd suggest to find a non-dev on the discord server to work together on this. Or maybe @ikabod-kee could help here.

@Mabeeck Mabeeck Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Does this work? Or maybe "Prevent loading on different devices"? I have also asked on discord. Will change once I have further improvement ideas.

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();
Expand Down
46 changes: 40 additions & 6 deletions src/network/authentication.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const BinaryReader = main.utils.BinaryReader;
const NeverFailingAllocator = main.heap.NeverFailingAllocator;
const ZonElement = main.ZonElement;

pub const protection = @import("protection.zig");

var wordlist: ?[2048][]const u8 = null;

fn wordToIndex(word: []const u8) ?u11 {
Expand Down Expand Up @@ -288,14 +290,15 @@ 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) 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));
Expand All @@ -305,27 +308,44 @@ 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);
std.crypto.aead.aes_gcm.Aes256Gcm.encrypt(encryptedBuffer, &authenticationTag, accountCode.text, &.{}, nonce, key);

const protected = shouldProtect and protection.canProtect;
var data: []u8 = undefined;
if (protected) {
data = try protection.protect(allocator, encryptedBuffer);
} else {
data = allocator.dupe(u8, encryptedBuffer);
}
return .{
.typ = .argon2_aes_gcm,
.protected = protected,
.salt = saltBase64,
.data = encryptedBuffer,
.data = data,
.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{SystemError}!PasswordEncodedAccountCode {
const protected = shouldProtect and protection.canProtect;
var data: []u8 = undefined;
if (protected) {
data = try protection.protect(allocator, accountCode.text);
} else {
data = allocator.dupe(u8, accountCode.text);
}
return .{
.typ = .none,
.protected = protected,
.salt = &.{},
.nonce = &.{},
.data = allocator.dupe(u8, accountCode.text),
.data = data,
.authenticationTag = &.{},
};
}
Expand All @@ -338,6 +358,18 @@ 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;

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);
}
return decryptFromPassword(copy, password, failureText);
}
if (self.typ == .none) {
return AccountCode.initFromUserInput(self.data, failureText);
}
Expand Down Expand Up @@ -379,6 +411,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;
Expand Down Expand Up @@ -407,6 +440,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));
Expand Down
113 changes: 113 additions & 0 deletions src/network/protection.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 => windows_impl,
else => no_impl,
};

pub const canProtect: bool = impl.canProtect;

pub fn protect(allocator: NeverFailingAllocator, data: []const u8) error{SystemError}![]u8 {
return impl.protect(allocator, data);
}

pub fn unprotect(allocator: NeverFailingAllocator, data: []const u8) error{ SystemError, Invalid }![]u8 {
Comment thread
Mabeeck marked this conversation as resolved.
return impl.unprotect(allocator, data);
}

const no_impl = struct {
const canProtect = false;

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 {
return error.Invalid;
}
};

const windows_impl = struct {
const canProtect = true;

fn protect(allocator: NeverFailingAllocator, data: []const u8) error{SystemError}![]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.", .{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.", .{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.", .{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 "Unprotect fails on unsupported platforms" {
const slice = "Test";
if (!canProtect) {
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;
}
}
Loading