Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(wallet): set Argon2 derived bytes for AES IV #1703

Merged
merged 3 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
71 changes: 50 additions & 21 deletions wallet/encrypter/encrypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
iterations uint32
memory uint32
parallelism uint8
keyLen uint32
}

type Option func(p *argon2dParameters)
Expand All @@ -47,11 +48,20 @@
nameParamIterations = "iterations"
nameParamMemory = "memory"
nameParamParallelism = "parallelism"
nameParamKeyLen = "keylen"

nameFuncNope = ""
nameFuncArgon2ID = "ARGON2ID"
nameFuncAES256CTR = "AES_256_CTR"
nameFuncAES256CBC = "AES_256_CBC"
nameFuncMACv1 = "MACV1"

// Parameter Choice
// https://www.rfc-editor.org/rfc/rfc9106.html#section-4
defaultIterations = 3
defaultMemory = 65536 // 2 ^ 16
defaultParallelism = 4
defaultKeyLen = 48
Copy link
Contributor

Choose a reason for hiding this comment

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

I can't find recommended value for KeyLen and what is it keylen?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Check above explanation

)

// ErrNotSupported describes an error in which the encrypted method is no
Expand All @@ -75,17 +85,17 @@
}
}

// DefaultEncrypter creates a default encrypter instance.
// DefaultEncrypter creates a new encrypter instance.
// If no option sets it uses the default parameters.
//
// The default encrypter uses Argon2ID as password hasher and AES_256_CTR as
// encryption algorithm.
func DefaultEncrypter(opts ...Option) Encrypter {
// Parameter Choice
// https://www.rfc-editor.org/rfc/rfc9106.html#section-4
argon2dParameters := &argon2dParameters{
iterations: uint32(3),
memory: uint32(65536), // 2 ^ 16
parallelism: uint8(4),
iterations: defaultIterations,
memory: defaultMemory,
parallelism: defaultParallelism,
keyLen: defaultKeyLen,
}
for _, opt := range opts {
opt(argon2dParameters)
Expand All @@ -98,6 +108,7 @@
encParams.SetUint32(nameParamIterations, argon2dParameters.iterations)
encParams.SetUint32(nameParamMemory, argon2dParameters.memory)
encParams.SetUint8(nameParamParallelism, argon2dParameters.parallelism)
encParams.SetUint32(nameParamKeyLen, argon2dParameters.keyLen)

return Encrypter{
Method: method,
Expand Down Expand Up @@ -140,22 +151,23 @@
return "", err
}

iterations := e.Params.GetUint32(nameParamIterations)
memory := e.Params.GetUint32(nameParamMemory)
parallelism := e.Params.GetUint8(nameParamParallelism)
iterations := e.Params.GetUint32(nameParamIterations, defaultIterations)
memory := e.Params.GetUint32(nameParamMemory, defaultMemory)
parallelism := e.Params.GetUint8(nameParamParallelism, defaultParallelism)
keyLen := e.Params.GetUint32(nameParamKeyLen, defaultKeyLen)

// Argon2 currently has three modes:
// - data-dependent Argon2d,
// - data-independent Argon2i,
// - a mix of the two, Argon2id.
cipherKey := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, 32)
derivedBytes := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, keyLen)

// Encrypter method
switch funcs[1] {
case nameFuncAES256CTR:
// Using salt for Initialization Vector (IV)
iv := salt
cipher := aesCrypt([]byte(message), iv, cipherKey)
cipherKey := derivedBytes[:32]
iv := derivedBytes[32:]
cipher := aesCTRCrypt([]byte(message), iv, cipherKey)

// MAC method
switch funcs[2] {
Expand Down Expand Up @@ -215,18 +227,35 @@
case nameFuncArgon2ID:
salt := data[0:16]

iterations := e.Params.GetUint32(nameParamIterations)
memory := e.Params.GetUint32(nameParamMemory)
parallelism := e.Params.GetUint8(nameParamParallelism)
iterations := e.Params.GetUint32(nameParamIterations, defaultIterations)
memory := e.Params.GetUint32(nameParamMemory, defaultMemory)
parallelism := e.Params.GetUint8(nameParamParallelism, defaultParallelism)
keyLen := e.Params.GetUint32(nameParamKeyLen, defaultKeyLen)

cipherKey := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, 32)
derivedByte := argon2.IDKey([]byte(password), salt, iterations, memory, parallelism, keyLen)

// Encrypter method
switch funcs[1] {
case nameFuncAES256CTR:
iv := salt
var initVec, cipherKey []byte

switch keyLen {
case 0:
// This case supports legacy encryption methods where the same salt is reused as the IV.
cipherKey = derivedByte
initVec = salt

Check warning on line 246 in wallet/encrypter/encrypter.go

View check run for this annotation

Codecov / codecov/patch

wallet/encrypter/encrypter.go#L243-L246

Added lines #L243 - L246 were not covered by tests

case 48:
// The first 32 bytes are used as the encryption key, and the last 16 bytes are used as the IV.
cipherKey = derivedByte[:32]
initVec = derivedByte[32:]

default:
return "", ErrInvalidParam

Check warning on line 254 in wallet/encrypter/encrypter.go

View check run for this annotation

Codecov / codecov/patch

wallet/encrypter/encrypter.go#L253-L254

Added lines #L253 - L254 were not covered by tests
}

enc := data[16 : len(data)-4]
text = string(aesCrypt(enc, iv, cipherKey))
text = string(aesCTRCrypt(enc, initVec, cipherKey))

// MAC method
switch funcs[2] {
Expand All @@ -249,9 +278,9 @@
return text, nil
}

// aesCrypt encrypts/decrypts a message using AES-256-CTR and
// aesCTRCrypt encrypts/decrypts a message using AES-256-CTR and
// returns the encoded/decoded bytes.
func aesCrypt(message, initVec, cipherKey []byte) []byte {
func aesCTRCrypt(message, initVec, cipherKey []byte) []byte {
// Generate the cipher message
cipherMsg := make([]byte, len(message))
aesCipher, err := aes.NewCipher(cipherKey)
Expand Down
31 changes: 30 additions & 1 deletion wallet/encrypter/encrypter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ func TestDefaultEncrypter(t *testing.T) {
assert.Equal(t, "3", enc.Params["iterations"])
assert.Equal(t, "4", enc.Params["memory"])
assert.Equal(t, "5", enc.Params["parallelism"])
assert.Equal(t, "48", enc.Params["keylen"])
assert.True(t, enc.IsEncrypted())
}

func TestEncrypter(t *testing.T) {
func TestEncrypterV2(t *testing.T) {
enc := &Encrypter{
Method: "ARGON2ID-AES_256_CTR-MACV1",
Params: params{
Expand All @@ -66,3 +67,31 @@ func TestEncrypter(t *testing.T) {
_, err = enc.Decrypt(cipher, "invalid-password")
assert.ErrorIs(t, err, ErrInvalidPassword)
}

func TestEncrypterV3(t *testing.T) {
enc := &Encrypter{
Method: "ARGON2ID-AES_256_CTR-MACV1",
Params: params{
nameParamIterations: "1",
nameParamMemory: "1",
nameParamParallelism: "1",
nameParamKeyLen: "48",
},
}

msg := "foo"

_, err := enc.Encrypt(msg, "")
assert.ErrorIs(t, err, ErrInvalidPassword)

password := "cowboy"
cipher, err := enc.Encrypt(msg, password)
assert.NoError(t, err)

dec, err := enc.Decrypt(cipher, password)
assert.NoError(t, err)
assert.Equal(t, msg, dec)

_, err = enc.Decrypt(cipher, "invalid-password")
assert.ErrorIs(t, err, ErrInvalidPassword)
}
3 changes: 3 additions & 0 deletions wallet/encrypter/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
// ErrInvalidPassword describes an error in which the password is invalid.
var ErrInvalidPassword = errors.New("invalid password")

// ErrInvalidParam describes an error in which the encryption parameter is invalid.
var ErrInvalidParam = errors.New("invalid param")

// ErrInvalidCipher describes an error in which the cipher message is invalid.
var ErrInvalidCipher = errors.New("invalid cipher message")

Expand Down
14 changes: 8 additions & 6 deletions wallet/encrypter/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,19 @@ func (p params) SetString(key, val string) {
p[key] = val
}

func (p params) GetUint8(key string) uint8 {
return uint8(p.GetUint64(key))
func (p params) GetUint8(key string, defaultValue uint64) uint8 {
return uint8(p.GetUint64(key, defaultValue))
}

func (p params) GetUint32(key string) uint32 {
return uint32(p.GetUint64(key))
func (p params) GetUint32(key string, defaultValue uint64) uint32 {
return uint32(p.GetUint64(key, defaultValue))
}

func (p params) GetUint64(key string) uint64 {
func (p params) GetUint64(key string, defaultValue uint64) uint64 {
val, err := strconv.ParseUint(p[key], 10, 64)
exitOnErr(err)
if err != nil {
return defaultValue
}

return val
}
Expand Down
27 changes: 18 additions & 9 deletions wallet/encrypter/params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func TestParamsUint8(t *testing.T) {
p := params{}
for _, tt := range tests {
p.SetUint8(tt.key, tt.val)
assert.Equal(t, tt.val, p.GetUint8(tt.key))
assert.Equal(t, tt.val, p.GetUint8(tt.key, 0))
}
}

Expand All @@ -34,7 +34,7 @@ func TestParamsUint32(t *testing.T) {
p := params{}
for _, tt := range tests {
p.SetUint32(tt.key, tt.val)
assert.Equal(t, tt.val, p.GetUint32(tt.key))
assert.Equal(t, tt.val, p.GetUint32(tt.key, 0))
}
}

Expand All @@ -50,24 +50,33 @@ func TestParamsUint64(t *testing.T) {
p := params{}
for _, tt := range tests {
p.SetUint64(tt.key, tt.val)
assert.Equal(t, tt.val, p.GetUint64(tt.key))
assert.Equal(t, tt.val, p.GetUint64(tt.key, 0))
}
}

func TestParamsDefaultValue(t *testing.T) {
p := params{}
assert.Equal(t, uint64(24), p.GetUint64("not-exist", 24))
assert.Equal(t, uint32(24), p.GetUint32("not-exist", 24))
assert.Equal(t, uint8(24), p.GetUint8("not-exist", 24))
}

func TestParamsBytes(t *testing.T) {
tests := []struct {
key string
val []byte
key string
val []byte
base64 string
}{
{"k1", []byte{0, 0}},
{"k2", []byte{0xff, 0xff}},
{"k2", []byte{}},
{"k1", []byte{0, 0}, "AAA="},
{"k2", []byte{0xff, 0xff}, "//8="},
{"k2", []byte{}, ""},
}

p := params{}
for _, tt := range tests {
p.SetBytes(tt.key, tt.val)
assert.Equal(t, tt.val, p.GetBytes(tt.key))
assert.Equal(t, tt.base64, p.GetString(tt.key))
}
}

Expand All @@ -78,7 +87,7 @@ func TestParamsString(t *testing.T) {
}{
{"k1", "foo"},
{"k2", "bar"},
{"k3", "bar"},
{"k3", ""},
}

p := params{}
Expand Down
10 changes: 5 additions & 5 deletions wallet/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import (
)

const (
Version1 = 1 // initial version
Version2 = 2 // supporting Ed25519
Version1 = 1 // Initial version
Version2 = 2 // Supporting Ed25519
Version3 = 3 // USe AEC-256-CBC for default encryption

VersionLatest = Version2
VersionLatest = Version3
)

type Store struct {
Expand Down Expand Up @@ -84,8 +85,7 @@ func (s *Store) UpgradeWallet(walletPath string) error {
return err
}

case Version2:
// Current version
case Version2, Version3:
return nil

default:
Expand Down
37 changes: 24 additions & 13 deletions wallet/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,40 @@ import (

func TestUpgradeWallet(t *testing.T) {
// password is: "password"
data, err := util.ReadFile("./testdata/wallet_version_1")
require.NoError(t, err)
tests := []struct {
walletPath string
upgradedVersion int
}{
{"./testdata/wallet_version_1", 2},
{"./testdata/wallet_version_2", 2},
{"./testdata/wallet_version_3", 3},
}

for _, tt := range tests {
data, err := util.ReadFile(tt.walletPath)
require.NoError(t, err)

tempPath := util.TempFilePath()
err = util.WriteFile(tempPath, data)
require.NoError(t, err)
tempPath := util.TempFilePath()
err = util.WriteFile(tempPath, data)
require.NoError(t, err)

wlt, err := Open(tempPath, true)
require.NoError(t, err)
wlt, err := Open(tempPath, true)
require.NoError(t, err)

assert.Equal(t, 4, wlt.AddressCount())
assert.Equal(t, VersionLatest, wlt.store.Version)
assert.Equal(t, 4, wlt.AddressCount())
assert.Equal(t, tt.upgradedVersion, wlt.store.Version)

infos := wlt.AddressInfos()
for _, info := range infos {
assert.NotEmpty(t, info.PublicKey)
infos := wlt.AddressInfos()
for _, info := range infos {
assert.NotEmpty(t, info.PublicKey)
}
}
}

func TestUnsupportedWallet(t *testing.T) {
_, err := Open("./testdata/unsupported_wallet", true)
require.ErrorIs(t, err, UnsupportedVersionError{
WalletVersion: 3,
WalletVersion: 4,
SupportedVersion: VersionLatest,
})
}
2 changes: 1 addition & 1 deletion wallet/testdata/unsupported_wallet
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": 3
"version": 4
}
Loading
Loading