fix: unlock legacy vault kdf files
This commit is contained in:
parent
0be1e93bb7
commit
257ce79d42
|
|
@ -61,6 +61,11 @@ type secretRecord struct {
|
||||||
plaintext []byte
|
plaintext []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type derivedKey struct {
|
||||||
|
key []byte
|
||||||
|
legacy bool
|
||||||
|
}
|
||||||
|
|
||||||
type SecretMeta struct {
|
type SecretMeta struct {
|
||||||
ID string
|
ID string
|
||||||
Alias string
|
Alias string
|
||||||
|
|
@ -164,22 +169,19 @@ func (v *Vault) Unlock(masterPassword string) error {
|
||||||
return fmt.Errorf("decode salt: %w", err)
|
return fmt.Errorf("decode salt: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
key := argon2.IDKey([]byte(masterPassword), salt, uint32(vf.KDF.Iterations), uint32(vf.KDF.MemoryKiB), uint8(vf.KDF.Parallelism), keyLen)
|
candidate, err := deriveValidKey([]byte(masterPassword), salt, vf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key := candidate.key
|
||||||
|
|
||||||
if vf.Verifier != nil {
|
if len(vf.Records) == 0 && vf.Verifier == nil {
|
||||||
if err := verifyRecord(key, *vf.Verifier); err != nil {
|
|
||||||
return fmt.Errorf("invalid master password")
|
|
||||||
}
|
|
||||||
} else if len(vf.Records) > 0 {
|
|
||||||
if _, err := decryptRecord(key, vf.Records[0]); err != nil {
|
|
||||||
return fmt.Errorf("invalid master password")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("vault cannot verify master password; recreate empty vault")
|
return fmt.Errorf("vault cannot verify master password; recreate empty vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
v.masterKey = key
|
v.masterKey = key
|
||||||
v.records = make(map[string]secretRecord)
|
v.records = make(map[string]secretRecord)
|
||||||
|
v.modified = candidate.legacy
|
||||||
|
|
||||||
for _, rec := range vf.Records {
|
for _, rec := range vf.Records {
|
||||||
plaintext, err := decryptRecord(key, rec)
|
plaintext, err := decryptRecord(key, rec)
|
||||||
|
|
@ -531,6 +533,44 @@ func decryptRecord(key []byte, rec Record) ([]byte, error) {
|
||||||
return plaintext, nil
|
return plaintext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deriveValidKey(masterPassword, salt []byte, vf VaultFile) (derivedKey, error) {
|
||||||
|
key := deriveKey(masterPassword, salt, vf.KDF.MemoryKiB, vf.KDF.Iterations, vf.KDF.Parallelism)
|
||||||
|
if canDecryptVaultFile(key, vf) {
|
||||||
|
return derivedKey{key: key}, nil
|
||||||
|
}
|
||||||
|
clearBytes(key)
|
||||||
|
|
||||||
|
if shouldTryLegacyKDF(vf) {
|
||||||
|
legacyMemoryKiB := vf.KDF.MemoryKiB * 1024
|
||||||
|
key = deriveKey(masterPassword, salt, legacyMemoryKiB, vf.KDF.Iterations, vf.KDF.Parallelism)
|
||||||
|
if canDecryptVaultFile(key, vf) {
|
||||||
|
return derivedKey{key: key, legacy: true}, nil
|
||||||
|
}
|
||||||
|
clearBytes(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return derivedKey{}, fmt.Errorf("invalid master password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldTryLegacyKDF(vf VaultFile) bool {
|
||||||
|
return vf.Verifier == nil && vf.KDF.MemoryKiB > 0 && vf.KDF.MemoryKiB <= 4096
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveKey(masterPassword, salt []byte, memoryKiB, iterations, parallelism int) []byte {
|
||||||
|
return argon2.IDKey(masterPassword, salt, uint32(iterations), uint32(memoryKiB), uint8(parallelism), keyLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canDecryptVaultFile(key []byte, vf VaultFile) bool {
|
||||||
|
if vf.Verifier != nil {
|
||||||
|
return verifyRecord(key, *vf.Verifier) == nil
|
||||||
|
}
|
||||||
|
if len(vf.Records) > 0 {
|
||||||
|
_, err := decryptRecord(key, vf.Records[0])
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func newVerifierRecord(key []byte) (Record, error) {
|
func newVerifierRecord(key []byte) (Record, error) {
|
||||||
rec, err := encryptRecord(key, verifierID, []byte(verifierPlaintext))
|
rec, err := encryptRecord(key, verifierID, []byte(verifierPlaintext))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -568,22 +608,22 @@ func VerifyPassword(path string, masterPassword string) (bool, error) {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
key := argon2.IDKey([]byte(masterPassword), salt, uint32(vf.KDF.Iterations), uint32(vf.KDF.MemoryKiB), uint8(vf.KDF.Parallelism), keyLen)
|
candidate, err := deriveValidKey([]byte(masterPassword), salt, vf)
|
||||||
defer func() {
|
if err != nil {
|
||||||
for i := range key {
|
|
||||||
key[i] = 0
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if vf.Verifier != nil {
|
|
||||||
return verifyRecord(key, *vf.Verifier) == nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(vf.Records) == 0 {
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
_, err = decryptRecord(key, vf.Records[0])
|
key := candidate.key
|
||||||
return err == nil, nil
|
defer func() {
|
||||||
|
clearBytes(key)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearBytes(data []byte) {
|
||||||
|
for i := range data {
|
||||||
|
data[i] = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constant-time comparison to prevent timing attacks
|
// Constant-time comparison to prevent timing attacks
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,56 @@ func TestLegacyVaultWithRecordsStillVerifiesByFirstRecord(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLegacyVaultWithPreReductionKDFStillUnlocks(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "vault.bin")
|
||||||
|
salt := []byte("12345678901234567890123456789012")
|
||||||
|
key := argon2.IDKey([]byte("correct horse"), salt, 2, 1024, 1, keyLen)
|
||||||
|
|
||||||
|
rec, err := encryptRecord(key, "server:test:ssh_password", []byte("secret"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt legacy record: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(VaultFile{
|
||||||
|
Version: currentVersion,
|
||||||
|
KDF: KDFMeta{
|
||||||
|
Name: "argon2id",
|
||||||
|
MemoryKiB: 1,
|
||||||
|
Iterations: 2,
|
||||||
|
Parallelism: 1,
|
||||||
|
Salt: base64.StdEncoding.EncodeToString(salt),
|
||||||
|
},
|
||||||
|
Records: []Record{rec},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal legacy vault: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||||
|
t.Fatalf("write legacy vault: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := VerifyPassword(path, "correct horse")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("verify legacy vault: %v", err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected legacy vault using pre-reduction KDF to accept correct password")
|
||||||
|
}
|
||||||
|
|
||||||
|
v := New(path)
|
||||||
|
if err := v.Unlock("correct horse"); err != nil {
|
||||||
|
t.Fatalf("unlock legacy vault using pre-reduction KDF: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := v.Get("server:test:ssh_password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get legacy secret: %v", err)
|
||||||
|
}
|
||||||
|
if string(secret) != "secret" {
|
||||||
|
t.Fatalf("unexpected legacy secret: %q", secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLegacyEmptyVaultWithoutVerifierCannotUnlock(t *testing.T) {
|
func TestLegacyEmptyVaultWithoutVerifierCannotUnlock(t *testing.T) {
|
||||||
path := filepath.Join(t.TempDir(), "vault.bin")
|
path := filepath.Join(t.TempDir(), "vault.bin")
|
||||||
salt := []byte("12345678901234567890123456789012")
|
salt := []byte("12345678901234567890123456789012")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue