check / check (push) Successful in 39s
The passphrase protecting the keychain unlocker's age key was a plain string passed through encoding/json, leaving copies in ordinary memory when an unlocker was created and each time one was used. It is now generated into a locked buffer, and KeychainData, moved to keychaindata.go, which is not darwin-only so its tests run on Linux, writes and reads the keychain JSON itself: encode copies the parts straight into a locked buffer, and decodeKeychainData takes the passphrase from a json.RawMessage that it wipes. The JSON field names are unchanged. keychainunlocker.go only calls this code and stores the item from the locked buffer without a string copy. Model: opus-5-5
119 lines
2.7 KiB
Go
119 lines
2.7 KiB
Go
//nolint:testpackage // white-box test of unexported internals
|
|
package secret
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/awnumar/memguard"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGenerateRandomPassphrase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
first, err := generateRandomPassphrase(64)
|
|
require.NoError(t, err)
|
|
|
|
defer first.Destroy()
|
|
|
|
second, err := generateRandomPassphrase(64)
|
|
require.NoError(t, err)
|
|
|
|
defer second.Destroy()
|
|
|
|
assert.Regexp(t, `^[0-9a-f]{64}$`, first.String())
|
|
assert.NotEqual(t, first.String(), second.String())
|
|
assert.False(t, first.IsMutable())
|
|
|
|
for _, length := range []int{0, -2, 63} {
|
|
_, err := generateRandomPassphrase(length)
|
|
require.ErrorIs(t, err, errPassphraseLength, "length %d", length)
|
|
}
|
|
}
|
|
|
|
func TestKeychainDataEncodeDecode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
passphrase := memguard.NewBufferFromBytes([]byte("0a1b2c3d"))
|
|
defer passphrase.Destroy()
|
|
|
|
data := KeychainData{
|
|
AgePublicKey: "age1example",
|
|
AgePrivKeyPassphrase: passphrase,
|
|
EncryptedLongtermKey: "beef",
|
|
}
|
|
|
|
encoded, err := data.encode()
|
|
require.NoError(t, err)
|
|
|
|
defer encoded.Destroy()
|
|
|
|
assert.JSONEq(t,
|
|
`{"agePublicKey":"age1example",`+
|
|
`"agePrivKeyPassphrase":"0a1b2c3d",`+
|
|
`"encryptedLongtermKey":"beef"}`,
|
|
encoded.String())
|
|
assert.False(t, encoded.IsMutable())
|
|
|
|
decoded, err := decodeKeychainData(encoded)
|
|
require.NoError(t, err)
|
|
|
|
defer decoded.AgePrivKeyPassphrase.Destroy()
|
|
|
|
assert.Equal(t, "age1example", decoded.AgePublicKey)
|
|
assert.Equal(t, "0a1b2c3d", decoded.AgePrivKeyPassphrase.String())
|
|
assert.Equal(t, "beef", decoded.EncryptedLongtermKey)
|
|
}
|
|
|
|
func TestKeychainDataEncodeRejectsBadPassphrase(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
passphrase *memguard.LockedBuffer
|
|
wantErr error
|
|
}{
|
|
{"nil", nil, errNilPassphraseBuffer},
|
|
{"empty", memguard.NewBuffer(0), errEmptyPassphrase},
|
|
{
|
|
"not hex",
|
|
memguard.NewBufferFromBytes([]byte(`abc"def`)),
|
|
errPassphraseNotHex,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
data := KeychainData{AgePrivKeyPassphrase: tt.passphrase}
|
|
_, err := data.encode()
|
|
require.ErrorIs(t, err, tt.wantErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecodeKeychainDataRejectsBadData(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, text := range []string{
|
|
`{"agePublicKey":"age1example"}`,
|
|
`{"agePrivKeyPassphrase":42}`,
|
|
} {
|
|
data := memguard.NewBufferFromBytes([]byte(text))
|
|
_, err := decodeKeychainData(data)
|
|
data.Destroy()
|
|
require.ErrorIs(t, err, errNoKeychainPassphrase, text)
|
|
}
|
|
|
|
notJSON := memguard.NewBufferFromBytes([]byte(`{"agePrivKeyPassphrase":`))
|
|
defer notJSON.Destroy()
|
|
|
|
_, err := decodeKeychainData(notJSON)
|
|
|
|
var syntaxError *json.SyntaxError
|
|
require.ErrorAs(t, err, &syntaxError)
|
|
}
|