From 220a3458d7828d240b817626c50a04884a966bee Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 12:49:59 +0000 Subject: [PATCH] feat(trix): add Argon2id key derivation alongside legacy SHA-256 Co-Authored-By: Claude Opus 4.6 --- go.mod | 8 ++--- go.sum | 16 ++++----- pkg/trix/trix.go | 45 +++++++++++++++++++++++ pkg/trix/trix_test.go | 84 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 2ded154..dc29b24 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,9 @@ require ( github.com/spf13/cobra v1.10.1 github.com/ulikunitz/xz v0.5.15 github.com/wailsapp/wails/v2 v2.11.0 - golang.org/x/mod v0.30.0 - golang.org/x/net v0.47.0 + golang.org/x/crypto v0.48.0 + golang.org/x/mod v0.32.0 + golang.org/x/net v0.49.0 golang.org/x/oauth2 v0.33.0 ) @@ -60,9 +61,8 @@ require ( github.com/wailsapp/go-webview2 v1.0.22 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.45.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/text v0.34.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 92da1f3..5b315c2 100644 --- a/go.sum +++ b/go.sum @@ -155,18 +155,18 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= @@ -190,8 +190,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/pkg/trix/trix.go b/pkg/trix/trix.go index 2d0122e..364b90f 100644 --- a/pkg/trix/trix.go +++ b/pkg/trix/trix.go @@ -2,9 +2,12 @@ package trix import ( "crypto/sha256" + "encoding/binary" "errors" "fmt" + "golang.org/x/crypto/argon2" + "github.com/Snider/Borg/pkg/datanode" "github.com/Snider/Enchantrix/pkg/crypt" "github.com/Snider/Enchantrix/pkg/enchantrix" @@ -61,11 +64,53 @@ func FromTrix(data []byte, password string) (*datanode.DataNode, error) { // DeriveKey derives a 32-byte key from a password using SHA-256. // This is used for ChaCha20-Poly1305 encryption which requires a 32-byte key. +// Deprecated: Use DeriveKeyArgon2 for new code; this remains for backward compatibility. func DeriveKey(password string) []byte { hash := sha256.Sum256([]byte(password)) return hash[:] } +// Argon2Params holds the tunable parameters for Argon2id key derivation. +type Argon2Params struct { + Time uint32 + Memory uint32 // in KiB + Threads uint32 +} + +// DefaultArgon2Params returns sensible default parameters for Argon2id. +func DefaultArgon2Params() Argon2Params { + return Argon2Params{ + Time: 3, + Memory: 64 * 1024, + Threads: 4, + } +} + +// Encode serialises the Argon2Params as 12 bytes (3 x uint32 little-endian). +func (p Argon2Params) Encode() []byte { + buf := make([]byte, 12) + binary.LittleEndian.PutUint32(buf[0:4], p.Time) + binary.LittleEndian.PutUint32(buf[4:8], p.Memory) + binary.LittleEndian.PutUint32(buf[8:12], p.Threads) + return buf +} + +// DecodeArgon2Params reads 12 bytes (3 x uint32 little-endian) into Argon2Params. +func DecodeArgon2Params(data []byte) Argon2Params { + return Argon2Params{ + Time: binary.LittleEndian.Uint32(data[0:4]), + Memory: binary.LittleEndian.Uint32(data[4:8]), + Threads: binary.LittleEndian.Uint32(data[8:12]), + } +} + +// DeriveKeyArgon2 derives a 32-byte key from a password and salt using Argon2id +// with DefaultArgon2Params. This is the recommended key derivation for new code. +func DeriveKeyArgon2(password string, salt []byte) []byte { + p := DefaultArgon2Params() + return argon2.IDKey([]byte(password), salt, p.Time, p.Memory, uint8(p.Threads), 32) +} + // ToTrixChaCha converts a DataNode to encrypted Trix format using ChaCha20-Poly1305. func ToTrixChaCha(dn *datanode.DataNode, password string) ([]byte, error) { if password == "" { diff --git a/pkg/trix/trix_test.go b/pkg/trix/trix_test.go index 7329fbc..2b1eb84 100644 --- a/pkg/trix/trix_test.go +++ b/pkg/trix/trix_test.go @@ -1,6 +1,8 @@ package trix import ( + "bytes" + "crypto/rand" "testing" "github.com/Snider/Borg/pkg/datanode" @@ -236,3 +238,85 @@ func TestToTrixChaChaWithLargeData(t *testing.T) { t.Fatalf("Failed to open large.bin: %v", err) } } + +// --- Argon2id key derivation tests --- + +func TestDeriveKeyArgon2_Good(t *testing.T) { + salt := make([]byte, 16) + if _, err := rand.Read(salt); err != nil { + t.Fatalf("failed to generate salt: %v", err) + } + + key := DeriveKeyArgon2("test-password", salt) + if len(key) != 32 { + t.Fatalf("expected 32-byte key, got %d bytes", len(key)) + } +} + +func TestDeriveKeyArgon2_Deterministic_Good(t *testing.T) { + salt := []byte("fixed-salt-value") + + key1 := DeriveKeyArgon2("same-password", salt) + key2 := DeriveKeyArgon2("same-password", salt) + + if !bytes.Equal(key1, key2) { + t.Fatal("same password and salt must produce the same key") + } +} + +func TestDeriveKeyArgon2_DifferentSalt_Good(t *testing.T) { + salt1 := []byte("salt-one-value!!") + salt2 := []byte("salt-two-value!!") + + key1 := DeriveKeyArgon2("same-password", salt1) + key2 := DeriveKeyArgon2("same-password", salt2) + + if bytes.Equal(key1, key2) { + t.Fatal("different salts must produce different keys") + } +} + +func TestDeriveKeyLegacy_Good(t *testing.T) { + key1 := DeriveKey("backward-compat") + key2 := DeriveKey("backward-compat") + + if len(key1) != 32 { + t.Fatalf("expected 32-byte key, got %d bytes", len(key1)) + } + if !bytes.Equal(key1, key2) { + t.Fatal("legacy DeriveKey must be deterministic") + } +} + +func TestArgon2Params_Good(t *testing.T) { + params := DefaultArgon2Params() + + // Non-zero values + if params.Time == 0 { + t.Fatal("Time must be non-zero") + } + if params.Memory == 0 { + t.Fatal("Memory must be non-zero") + } + if params.Threads == 0 { + t.Fatal("Threads must be non-zero") + } + + // Encode produces 12 bytes (3 x uint32 LE) + encoded := params.Encode() + if len(encoded) != 12 { + t.Fatalf("expected 12-byte encoding, got %d bytes", len(encoded)) + } + + // Round-trip: Decode must recover original values + decoded := DecodeArgon2Params(encoded) + if decoded.Time != params.Time { + t.Fatalf("Time mismatch: got %d, want %d", decoded.Time, params.Time) + } + if decoded.Memory != params.Memory { + t.Fatalf("Memory mismatch: got %d, want %d", decoded.Memory, params.Memory) + } + if decoded.Threads != params.Threads { + t.Fatalf("Threads mismatch: got %d, want %d", decoded.Threads, params.Threads) + } +}