refactor: naming cleanup and crypto package improvements (#551)

* refactor: naming cleanup across all packages
This commit is contained in:
Roger
2026-04-05 16:51:56 +08:00
committed by GitHub
parent 4af2ded428
commit 410bffe643
49 changed files with 716 additions and 510 deletions
+84 -88
View File
@@ -1,24 +1,30 @@
package crypto
import (
"crypto/aes"
"crypto/des"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/asn1"
"errors"
)
type ASN1PBE interface {
Decrypt(globalSalt []byte) ([]byte, error)
const des3KeySize = 24 // 3DES uses 24-byte (192-bit) keys
Encrypt(globalSalt, plaintext []byte) ([]byte, error)
// ASN1PBE represents a Password-Based Encryption structure from Firefox's NSS.
// The key parameter semantics vary by implementation:
// - privateKeyPBE / passwordCheckPBE: key is the global salt used for key derivation
// - credentialPBE: key is the already-derived master key
type ASN1PBE interface {
Decrypt(key []byte) ([]byte, error)
Encrypt(key, plaintext []byte) ([]byte, error)
}
func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
var (
nss nssPBE
meta metaPBE
login loginPBE
nss privateKeyPBE
meta passwordCheckPBE
login credentialPBE
)
if _, err := asn1.Unmarshal(b, &nss); err == nil {
return nss, nil
@@ -29,12 +35,10 @@ func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
if _, err := asn1.Unmarshal(b, &login); err == nil {
return login, nil
}
return nil, ErrDecodeASN1Failed
return nil, errDecodeASN1
}
var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed")
// nssPBE Struct
// privateKeyPBE Struct
//
// SEQUENCE (2 elem)
// OBJECT IDENTIFIER
@@ -42,52 +46,56 @@ var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed")
// OCTET STRING (20 byte)
// INTEGER 1
// OCTET STRING (16 byte)
type nssPBE struct {
type privateKeyPBE struct {
AlgoAttr struct {
asn1.ObjectIdentifier
SaltAttr struct {
EntrySalt []byte
Len int
KeyLen int
}
}
Encrypted []byte
}
// Decrypt decrypts the encrypted password with the global salt.
func (n nssPBE) Decrypt(globalSalt []byte) ([]byte, error) {
func (n privateKeyPBE) Decrypt(globalSalt []byte) ([]byte, error) {
key, iv := n.deriveKeyAndIV(globalSalt)
return DES3Decrypt(key, iv, n.Encrypted)
}
func (n nssPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
func (n privateKeyPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
key, iv := n.deriveKeyAndIV(globalSalt)
return DES3Encrypt(key, iv, plaintext)
}
// deriveKeyAndIV derives the key and initialization vector (IV)
// from the global salt and entry salt.
func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
salt := n.AlgoAttr.SaltAttr.EntrySalt
hashPrefix := sha1.Sum(globalSalt)
compositeHash := sha1.Sum(append(hashPrefix[:], salt...))
paddedEntrySalt := paddingZero(salt, 20)
// deriveKeyAndIV implements NSS PBE-SHA1-3DES key derivation.
// Reference: https://searchfox.org/mozilla-central/source/security/nss/lib/softoken/lowpbe.c
//
// Derivation steps:
//
// hp = SHA1(globalSalt)
// ck = SHA1(hp || entrySalt)
// hmac1 = HMAC-SHA1(ck, paddedSalt)
// k1 = HMAC-SHA1(ck, paddedSalt || entrySalt)
// k2 = HMAC-SHA1(ck, hmac1 || entrySalt)
// dk = k1 || k2 (40 bytes)
// key = dk[:24], iv = dk[32:]
func (n privateKeyPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
entrySalt := n.AlgoAttr.SaltAttr.EntrySalt
hp := sha1.Sum(globalSalt)
ck := sha1.Sum(append(hp[:], entrySalt...))
paddedSalt := paddingZero(entrySalt, 20)
hmacProcessor := hmac.New(sha1.New, compositeHash[:])
hmacProcessor.Write(paddedEntrySalt)
hmac1 := hmac.New(sha1.New, ck[:])
hmac1.Write(paddedSalt)
paddedEntrySalt = append(paddedEntrySalt, salt...)
keyComponent1 := hmac.New(sha1.New, compositeHash[:])
keyComponent1.Write(paddedEntrySalt)
k1 := hmac.New(sha1.New, ck[:])
k1.Write(append(paddedSalt, entrySalt...))
hmacWithSalt := append(hmacProcessor.Sum(nil), salt...)
keyComponent2 := hmac.New(sha1.New, compositeHash[:])
keyComponent2.Write(hmacWithSalt)
k2 := hmac.New(sha1.New, ck[:])
k2.Write(append(hmac1.Sum(nil), entrySalt...))
key := append(keyComponent1.Sum(nil), keyComponent2.Sum(nil)...)
iv := key[len(key)-8:]
return key[:24], iv
dk := append(k1.Sum(nil), k2.Sum(nil)...)
return dk[:24], dk[len(dk)-8:]
}
// MetaPBE Struct
@@ -107,17 +115,17 @@ func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
// OBJECT IDENTIFIER
// OCTET STRING (14 byte)
// OCTET STRING (16 byte)
type metaPBE struct {
type passwordCheckPBE struct {
AlgoAttr algoAttr
Encrypted []byte
}
type algoAttr struct {
asn1.ObjectIdentifier
Data struct {
Data struct {
KDFParams struct {
PBKDF2 struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
SaltAttr saltAttr
}
IVData ivAttr
}
@@ -128,7 +136,7 @@ type ivAttr struct {
IV []byte
}
type slatAttr struct {
type saltAttr struct {
EntrySalt []byte
IterationCount int
KeySize int
@@ -137,81 +145,69 @@ type slatAttr struct {
}
}
func (m metaPBE) Decrypt(globalSalt []byte) ([]byte, error) {
func (m passwordCheckPBE) Decrypt(globalSalt []byte) ([]byte, error) {
key, iv := m.deriveKeyAndIV(globalSalt)
return AES128CBCDecrypt(key, iv, m.Encrypted)
return AESCBCDecrypt(key, iv, m.Encrypted)
}
func (m metaPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
func (m passwordCheckPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
key, iv := m.deriveKeyAndIV(globalSalt)
return AES128CBCEncrypt(key, iv, plaintext)
return AESCBCEncrypt(key, iv, plaintext)
}
func (m metaPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
func (m passwordCheckPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
password := sha1.Sum(globalSalt)
salt := m.AlgoAttr.Data.Data.SlatAttr.EntrySalt
iter := m.AlgoAttr.Data.Data.SlatAttr.IterationCount
keyLen := m.AlgoAttr.Data.Data.SlatAttr.KeySize
params := m.AlgoAttr.KDFParams.PBKDF2.SaltAttr
key := PBKDF2Key(password[:], params.EntrySalt, params.IterationCount, params.KeySize, sha256.New)
key := PBKDF2Key(password[:], salt, iter, keyLen, sha256.New)
iv := append([]byte{4, 14}, m.AlgoAttr.Data.IVData.IV...)
// Firefox stores the IV with its ASN.1 OCTET STRING header (tag=0x04, length=0x0E).
// The full 16-byte IV = [0x04, 0x0E] + 14-byte IV value from the parsed structure.
iv := append([]byte{0x04, 0x0E}, m.AlgoAttr.KDFParams.IVData.IV...)
return key, iv
}
// loginPBE Struct
// credentialPBE Struct
//
// OCTET STRING (16 byte)
// SEQUENCE (2 elem)
// OBJECT IDENTIFIER
// OCTET STRING (8 byte)
// OCTET STRING (16 byte)
type loginPBE struct {
CipherText []byte
Data struct {
type credentialPBE struct {
KeyCheck []byte
Algo struct {
asn1.ObjectIdentifier
IV []byte
}
Encrypted []byte
}
func (l loginPBE) Decrypt(globalSalt []byte) ([]byte, error) {
key, iv := l.deriveKeyAndIV(globalSalt)
// The encryption algorithm can be reliably inferred from IV length:
// - 8 bytes : 3DES-CBC (legacy Firefox versions)
// - 16 bytes : AES-CBC (Firefox 144+)
if len(iv) == 8 {
// Use 3DES for old Firefox versions
return DES3Decrypt(key[:24], iv, l.Encrypted)
} else if len(iv) == 16 {
// Firefox 144+ uses 32-byte keys (AES-256-CBC)
// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths
return AES128CBCDecrypt(key, iv, l.Encrypted)
func (l credentialPBE) Decrypt(masterKey []byte) ([]byte, error) {
key, iv := l.deriveKeyAndIV(masterKey)
// The cipher is inferred from IV length (avoids fragile OID checks):
switch len(iv) {
case des.BlockSize: // 8: 3DES-CBC (legacy Firefox)
return DES3Decrypt(key[:des3KeySize], iv, l.Encrypted)
case aes.BlockSize: // 16: AES-256-CBC (Firefox 144+)
return AESCBCDecrypt(key, iv, l.Encrypted)
default:
return nil, errUnsupportedIVLen
}
return nil, errors.New("unsupported IV length for loginPBE decryption")
}
func (l loginPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
key, iv := l.deriveKeyAndIV(globalSalt)
// The encryption algorithm can be reliably inferred from IV length:
// - 8 bytes : 3DES-CBC (legacy Firefox versions)
// - 16 bytes : AES-CBC (Firefox 144+)
// This avoids relying on NSS-specific OIDs, which have changed historically.
if len(iv) == 8 {
// Use 3DES for old Firefox versions
return DES3Encrypt(key[:24], iv, plaintext)
} else if len(iv) == 16 {
// Firefox 144+ uses 32-byte keys (AES-256-CBC)
// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths
return AES128CBCEncrypt(key, iv, plaintext)
func (l credentialPBE) Encrypt(masterKey, plaintext []byte) ([]byte, error) {
key, iv := l.deriveKeyAndIV(masterKey)
switch len(iv) {
case des.BlockSize:
return DES3Encrypt(key[:des3KeySize], iv, plaintext)
case aes.BlockSize:
return AESCBCEncrypt(key, iv, plaintext)
default:
return nil, errUnsupportedIVLen
}
return nil, errors.New("unsupported IV length for loginPBE encryption")
}
func (l loginPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
return globalSalt, l.Data.IV
func (l credentialPBE) deriveKeyAndIV(masterKey []byte) ([]byte, []byte) {
return masterKey, l.Algo.IV
}
+132 -78
View File
@@ -11,18 +11,18 @@ import (
)
var (
pbeIV = []byte("01234567") // 8 bytes
pbePlaintext = []byte("Hello, World!")
pbeCipherText = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}
objWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3}
objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46}
objWithSHA1AndAES = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}
nssPBETestCases = []struct {
pbeIV = []byte("01234567") // 8 bytes
pbePlaintext = []byte("Hello, World!")
pbeKeyCheck = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}
objWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3}
objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46}
objWithSHA1AndAES = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}
privateKeyPBETestCases = []struct {
RawHexPBE string
GlobalSalt []byte
Encrypted []byte
IterationCount int
Len int
KeyLen int
Plaintext []byte
ObjectIdentifier asn1.ObjectIdentifier
}{
@@ -32,11 +32,11 @@ var (
Encrypted: []byte{0x95, 0x18, 0x3a, 0x14, 0xc7, 0x52, 0xe7, 0xb1, 0xd0, 0xaa, 0xa4, 0x7f, 0x53, 0xe0, 0x50, 0x97},
Plaintext: pbePlaintext,
IterationCount: 1,
Len: 32,
KeyLen: 32,
ObjectIdentifier: objWithSHA1AndAES,
},
}
metaPBETestCases = []struct {
passwordCheckPBETestCases = []struct {
RawHexPBE string
GlobalSalt []byte
Encrypted []byte
@@ -53,7 +53,7 @@ var (
ObjectIdentifier: objWithSHA256AndAES,
},
}
loginPBETestCases = []struct {
credentialPBETestCases = []struct {
RawHexPBE string
GlobalSalt []byte
Encrypted []byte
@@ -73,108 +73,108 @@ var (
)
func TestNewASN1PBE(t *testing.T) {
for _, tc := range nssPBETestCases {
for _, tc := range privateKeyPBETestCases {
nssRaw, err := hex.DecodeString(tc.RawHexPBE)
require.NoError(t, err)
pbe, err := NewASN1PBE(nssRaw)
require.NoError(t, err)
nssPBETC, ok := pbe.(nssPBE)
privateKeyPBETC, ok := pbe.(privateKeyPBE)
assert.True(t, ok)
assert.Equal(t, nssPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt)
assert.Equal(t, 20, nssPBETC.AlgoAttr.SaltAttr.Len)
assert.Equal(t, nssPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier)
assert.Equal(t, privateKeyPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, privateKeyPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt)
assert.Equal(t, 20, privateKeyPBETC.AlgoAttr.SaltAttr.KeyLen)
assert.Equal(t, privateKeyPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier)
}
}
func TestNssPBE_Encrypt(t *testing.T) {
for _, tc := range nssPBETestCases {
nssPBETC := nssPBE{
func TestPrivateKeyPBE_Encrypt(t *testing.T) {
for _, tc := range privateKeyPBETestCases {
privateKeyPBETC := privateKeyPBE{
Encrypted: tc.Encrypted,
AlgoAttr: struct {
asn1.ObjectIdentifier
SaltAttr struct {
EntrySalt []byte
Len int
KeyLen int
}
}{
ObjectIdentifier: tc.ObjectIdentifier,
SaltAttr: struct {
EntrySalt []byte
Len int
KeyLen int
}{
EntrySalt: tc.GlobalSalt,
Len: 20,
KeyLen: 20,
},
},
}
encrypted, err := nssPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
encrypted, err := privateKeyPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
require.NoError(t, err)
assert.NotEmpty(t, encrypted)
assert.Equal(t, nssPBETC.Encrypted, encrypted)
assert.Equal(t, privateKeyPBETC.Encrypted, encrypted)
}
}
func TestNssPBE_Decrypt(t *testing.T) {
for _, tc := range nssPBETestCases {
nssPBETC := nssPBE{
func TestPrivateKeyPBE_Decrypt(t *testing.T) {
for _, tc := range privateKeyPBETestCases {
privateKeyPBETC := privateKeyPBE{
Encrypted: tc.Encrypted,
AlgoAttr: struct {
asn1.ObjectIdentifier
SaltAttr struct {
EntrySalt []byte
Len int
KeyLen int
}
}{
ObjectIdentifier: tc.ObjectIdentifier,
SaltAttr: struct {
EntrySalt []byte
Len int
KeyLen int
}{
EntrySalt: tc.GlobalSalt,
Len: 20,
KeyLen: 20,
},
},
}
decrypted, err := nssPBETC.Decrypt(tc.GlobalSalt)
decrypted, err := privateKeyPBETC.Decrypt(tc.GlobalSalt)
require.NoError(t, err)
assert.NotEmpty(t, decrypted)
assert.Equal(t, pbePlaintext, decrypted)
}
}
func TestNewASN1PBE_MetaPBE(t *testing.T) {
for _, tc := range metaPBETestCases {
func TestNewASN1PBE_PasswordCheckPBE(t *testing.T) {
for _, tc := range passwordCheckPBETestCases {
metaRaw, err := hex.DecodeString(tc.RawHexPBE)
require.NoError(t, err)
pbe, err := NewASN1PBE(metaRaw)
require.NoError(t, err)
metaPBETC, ok := pbe.(metaPBE)
passwordCheckPBETC, ok := pbe.(passwordCheckPBE)
assert.True(t, ok)
assert.Equal(t, metaPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.IV, tc.IV)
assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.ObjectIdentifier, objWithSHA256AndAES)
assert.Equal(t, passwordCheckPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, passwordCheckPBETC.AlgoAttr.KDFParams.IVData.IV, tc.IV)
assert.Equal(t, passwordCheckPBETC.AlgoAttr.KDFParams.IVData.ObjectIdentifier, objWithSHA256AndAES)
}
}
func TestMetaPBE_Encrypt(t *testing.T) {
for _, tc := range metaPBETestCases {
metaPBETC := metaPBE{
func TestPasswordCheckPBE_Encrypt(t *testing.T) {
for _, tc := range passwordCheckPBETestCases {
passwordCheckPBETC := passwordCheckPBE{
AlgoAttr: algoAttr{
ObjectIdentifier: tc.ObjectIdentifier,
Data: struct {
Data struct {
KDFParams: struct {
PBKDF2 struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
SaltAttr saltAttr
}
IVData ivAttr
}{
Data: struct {
PBKDF2: struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
SaltAttr saltAttr
}{
ObjectIdentifier: tc.ObjectIdentifier,
SlatAttr: slatAttr{
SaltAttr: saltAttr{
EntrySalt: tc.GlobalSalt,
IterationCount: 1,
KeySize: 32,
@@ -193,31 +193,31 @@ func TestMetaPBE_Encrypt(t *testing.T) {
},
Encrypted: tc.Encrypted,
}
encrypted, err := metaPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
encrypted, err := passwordCheckPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
require.NoError(t, err)
assert.NotEmpty(t, encrypted)
assert.Equal(t, metaPBETC.Encrypted, encrypted)
assert.Equal(t, passwordCheckPBETC.Encrypted, encrypted)
}
}
func TestMetaPBE_Decrypt(t *testing.T) {
for _, tc := range metaPBETestCases {
metaPBETC := metaPBE{
func TestPasswordCheckPBE_Decrypt(t *testing.T) {
for _, tc := range passwordCheckPBETestCases {
passwordCheckPBETC := passwordCheckPBE{
AlgoAttr: algoAttr{
ObjectIdentifier: tc.ObjectIdentifier,
Data: struct {
Data struct {
KDFParams: struct {
PBKDF2 struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
SaltAttr saltAttr
}
IVData ivAttr
}{
Data: struct {
PBKDF2: struct {
asn1.ObjectIdentifier
SlatAttr slatAttr
SaltAttr saltAttr
}{
ObjectIdentifier: tc.ObjectIdentifier,
SlatAttr: slatAttr{
SaltAttr: saltAttr{
EntrySalt: tc.GlobalSalt,
IterationCount: 1,
KeySize: 32,
@@ -236,32 +236,32 @@ func TestMetaPBE_Decrypt(t *testing.T) {
},
Encrypted: tc.Encrypted,
}
decrypted, err := metaPBETC.Decrypt(tc.GlobalSalt)
decrypted, err := passwordCheckPBETC.Decrypt(tc.GlobalSalt)
require.NoError(t, err)
assert.NotEmpty(t, decrypted)
assert.Equal(t, pbePlaintext, decrypted)
}
}
func TestNewASN1PBE_LoginPBE(t *testing.T) {
for _, tc := range loginPBETestCases {
func TestNewASN1PBE_CredentialPBE(t *testing.T) {
for _, tc := range credentialPBETestCases {
loginRaw, err := hex.DecodeString(tc.RawHexPBE)
require.NoError(t, err)
pbe, err := NewASN1PBE(loginRaw)
require.NoError(t, err)
loginPBETC, ok := pbe.(loginPBE)
credentialPBETC, ok := pbe.(credentialPBE)
assert.True(t, ok)
assert.Equal(t, loginPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, loginPBETC.Data.IV, tc.IV)
assert.Equal(t, loginPBETC.Data.ObjectIdentifier, objWithMD5AndDESCBC)
assert.Equal(t, credentialPBETC.Encrypted, tc.Encrypted)
assert.Equal(t, credentialPBETC.Algo.IV, tc.IV)
assert.Equal(t, credentialPBETC.Algo.ObjectIdentifier, objWithMD5AndDESCBC)
}
}
func TestLoginPBE_Encrypt(t *testing.T) {
for _, tc := range loginPBETestCases {
loginPBETC := loginPBE{
CipherText: pbeCipherText,
Data: struct {
func TestCredentialPBE_Encrypt(t *testing.T) {
for _, tc := range credentialPBETestCases {
credentialPBETC := credentialPBE{
KeyCheck: pbeKeyCheck,
Algo: struct {
asn1.ObjectIdentifier
IV []byte
}{
@@ -270,18 +270,18 @@ func TestLoginPBE_Encrypt(t *testing.T) {
},
Encrypted: tc.Encrypted,
}
encrypted, err := loginPBETC.Encrypt(tc.GlobalSalt, plainText)
encrypted, err := credentialPBETC.Encrypt(tc.GlobalSalt, plainText)
require.NoError(t, err)
assert.NotEmpty(t, encrypted)
assert.Equal(t, loginPBETC.Encrypted, encrypted)
assert.Equal(t, credentialPBETC.Encrypted, encrypted)
}
}
func TestLoginPBE_Decrypt(t *testing.T) {
for _, tc := range loginPBETestCases {
loginPBETC := loginPBE{
CipherText: pbeCipherText,
Data: struct {
func TestCredentialPBE_Decrypt(t *testing.T) {
for _, tc := range credentialPBETestCases {
credentialPBETC := credentialPBE{
KeyCheck: pbeKeyCheck,
Algo: struct {
asn1.ObjectIdentifier
IV []byte
}{
@@ -290,9 +290,63 @@ func TestLoginPBE_Decrypt(t *testing.T) {
},
Encrypted: tc.Encrypted,
}
decrypted, err := loginPBETC.Decrypt(tc.GlobalSalt)
decrypted, err := credentialPBETC.Decrypt(tc.GlobalSalt)
require.NoError(t, err)
assert.NotEmpty(t, decrypted)
assert.Equal(t, pbePlaintext, decrypted)
}
}
func TestNewASN1PBE_InvalidData(t *testing.T) {
_, err := NewASN1PBE([]byte{0xFF, 0xFF})
assert.ErrorIs(t, err, errDecodeASN1)
}
func TestCredentialPBE_AES256CBC(t *testing.T) {
// Test the Firefox 144+ AES-256-CBC path (IV length = 16).
// Construct a credentialPBE with a 16-byte IV to exercise the AES branch.
masterKey := bytes.Repeat([]byte("k"), 32) // AES-256 key
iv := bytes.Repeat([]byte{0x01}, 16) // 16-byte IV → AES-CBC path
// Encrypt plaintext to get valid ciphertext for round-trip test.
encrypted, err := AESCBCEncrypt(masterKey, iv, pbePlaintext)
require.NoError(t, err)
pbe := credentialPBE{
KeyCheck: pbeKeyCheck,
Algo: struct {
asn1.ObjectIdentifier
IV []byte
}{
ObjectIdentifier: objWithSHA256AndAES,
IV: iv,
},
Encrypted: encrypted,
}
decrypted, err := pbe.Decrypt(masterKey)
require.NoError(t, err)
assert.Equal(t, pbePlaintext, decrypted)
// Verify encrypt round-trip
reEncrypted, err := pbe.Encrypt(masterKey, pbePlaintext)
require.NoError(t, err)
assert.Equal(t, encrypted, reEncrypted)
}
func TestCredentialPBE_UnsupportedIVLength(t *testing.T) {
pbe := credentialPBE{
Algo: struct {
asn1.ObjectIdentifier
IV []byte
}{
IV: []byte{1, 2, 3}, // 3-byte IV: neither 8 nor 16
},
Encrypted: []byte("data"),
}
_, err := pbe.Decrypt([]byte("key"))
require.ErrorIs(t, err, errUnsupportedIVLen)
_, err = pbe.Encrypt([]byte("key"), []byte("data"))
require.ErrorIs(t, err, errUnsupportedIVLen)
}
+95 -102
View File
@@ -1,161 +1,154 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/des"
"errors"
"fmt"
)
var ErrCiphertextLengthIsInvalid = errors.New("ciphertext length is invalid")
// AES128CBCDecrypt decrypts data using AES-CBC mode.
// Note: Despite the function name, this supports all AES key sizes.
// The Go standard library's aes.NewCipher automatically selects the AES variant
// based on the key length: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256).
// TODO: Rename to AESCBCDecrypt to avoid confusion about supported key lengths.
func AES128CBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) {
// AESCBCEncrypt encrypts data using AES-CBC mode with PKCS5 padding.
// Supports all AES key sizes: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256).
func AESCBCEncrypt(key, iv, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Check ciphertext length
if len(ciphertext) < aes.BlockSize {
return nil, errors.New("AES128CBCDecrypt: ciphertext too short")
}
if len(ciphertext)%aes.BlockSize != 0 {
return nil, errors.New("AES128CBCDecrypt: ciphertext is not a multiple of the block size")
}
decryptedData := make([]byte, len(ciphertext))
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(decryptedData, ciphertext)
// unpad the decrypted data and handle potential padding errors
decryptedData, err = pkcs5UnPadding(decryptedData)
if err != nil {
return nil, fmt.Errorf("AES128CBCDecrypt: %w", err)
}
return decryptedData, nil
return cbcEncrypt(block, iv, plaintext)
}
func AES128CBCEncrypt(key, iv, plaintext []byte) ([]byte, error) {
// AESCBCDecrypt decrypts data using AES-CBC mode with PKCS5 unpadding.
// Supports all AES key sizes: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256).
func AESCBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(iv) != aes.BlockSize {
return nil, errors.New("AES128CBCEncrypt: iv length is invalid, must equal block size")
}
plaintext = pkcs5Padding(plaintext, block.BlockSize())
encryptedData := make([]byte, len(plaintext))
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(encryptedData, plaintext)
return encryptedData, nil
}
func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
block, err := des.NewTripleDESCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < des.BlockSize {
return nil, errors.New("DES3Decrypt: ciphertext too short")
}
if len(ciphertext)%block.BlockSize() != 0 {
return nil, errors.New("DES3Decrypt: ciphertext is not a multiple of the block size")
}
blockMode := cipher.NewCBCDecrypter(block, iv)
sq := make([]byte, len(ciphertext))
blockMode.CryptBlocks(sq, ciphertext)
return pkcs5UnPadding(sq)
return cbcDecrypt(block, iv, ciphertext)
}
// DES3Encrypt encrypts data using 3DES-CBC mode with PKCS5 padding.
func DES3Encrypt(key, iv, plaintext []byte) ([]byte, error) {
block, err := des.NewTripleDESCipher(key)
if err != nil {
return nil, err
}
plaintext = pkcs5Padding(plaintext, block.BlockSize())
dst := make([]byte, len(plaintext))
blockMode := cipher.NewCBCEncrypter(block, iv)
blockMode.CryptBlocks(dst, plaintext)
return dst, nil
return cbcEncrypt(block, iv, plaintext)
}
// AESGCMDecrypt chromium > 80 https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/sync/os_crypt_win.cc
func AESGCMDecrypt(key, nounce, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
// DES3Decrypt decrypts data using 3DES-CBC mode with PKCS5 unpadding.
func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
block, err := des.NewTripleDESCipher(key)
if err != nil {
return nil, err
}
blockMode, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
origData, err := blockMode.Open(nil, nounce, ciphertext, nil)
if err != nil {
return nil, err
}
return origData, nil
return cbcDecrypt(block, iv, ciphertext)
}
// AESGCMEncrypt encrypts plaintext using AES encryption in GCM mode.
// AESGCMEncrypt encrypts data using AES-GCM mode.
func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
blockMode, err := cipher.NewGCM(block)
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// The first parameter is the prefix for the output, we can leave it nil.
// The Seal method encrypts and authenticates the data, appending the result to the dst.
encryptedData := blockMode.Seal(nil, nonce, plaintext, nil)
return encryptedData, nil
if len(nonce) != aead.NonceSize() {
return nil, errInvalidNonceLen
}
return aead.Seal(nil, nonce, plaintext, nil), nil
}
// AESGCMDecrypt decrypts data using AES-GCM mode.
func AESGCMDecrypt(key, nonce, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(nonce) != aead.NonceSize() {
return nil, errInvalidNonceLen
}
return aead.Open(nil, nonce, ciphertext, nil)
}
// cbcEncrypt adds PKCS5 padding and encrypts plaintext in CBC mode.
func cbcEncrypt(block cipher.Block, iv, plaintext []byte) ([]byte, error) {
if len(iv) != block.BlockSize() {
return nil, errInvalidIVLength
}
padded := pkcs5Padding(plaintext, block.BlockSize())
dst := make([]byte, len(padded))
cipher.NewCBCEncrypter(block, iv).CryptBlocks(dst, padded)
return dst, nil
}
// cbcDecrypt decrypts ciphertext in CBC mode and removes PKCS5 padding.
func cbcDecrypt(block cipher.Block, iv, ciphertext []byte) ([]byte, error) {
bs := block.BlockSize()
if len(iv) != bs {
return nil, errInvalidIVLength
}
if len(ciphertext) < bs {
return nil, errShortCiphertext
}
if len(ciphertext)%bs != 0 {
return nil, errInvalidBlockSize
}
dst := make([]byte, len(ciphertext))
cipher.NewCBCDecrypter(block, iv).CryptBlocks(dst, ciphertext)
dst, err := pkcs5UnPadding(dst, bs)
if err != nil {
return nil, fmt.Errorf("decrypt: %w", err)
}
return dst, nil
}
// paddingZero pads src with zero bytes to the given length.
// Returns src unchanged if already long enough; otherwise returns a new slice.
func paddingZero(src []byte, length int) []byte {
padding := length - len(src)
if padding <= 0 {
if len(src) >= length {
return src
}
return append(src, make([]byte, padding)...)
dst := make([]byte, length)
copy(dst, src)
return dst
}
func pkcs5UnPadding(src []byte) ([]byte, error) {
// pkcs5Padding adds PKCS5/PKCS7 padding to src.
// Always returns a new slice; never modifies src.
func pkcs5Padding(src []byte, blockSize int) []byte {
n := blockSize - (len(src) % blockSize)
dst := make([]byte, len(src)+n)
copy(dst, src)
for i := len(src); i < len(dst); i++ {
dst[i] = byte(n)
}
return dst
}
// pkcs5UnPadding removes PKCS5/PKCS7 padding from src.
func pkcs5UnPadding(src []byte, blockSize int) ([]byte, error) {
length := len(src)
if length == 0 {
return nil, errors.New("pkcs5UnPadding: src should not be empty")
return nil, errInvalidPadding
}
padding := int(src[length-1])
if padding < 1 || padding > aes.BlockSize {
return nil, errors.New("pkcs5UnPadding: invalid padding size")
}
if padding > length {
return nil, errors.New("pkcs5UnPadding: invalid padding length")
if padding < 1 || padding > blockSize || padding > length {
return nil, errInvalidPadding
}
for _, b := range src[length-padding:] {
if int(b) != padding {
return nil, errors.New("pkcs5UnPadding: invalid padding content")
return nil, errInvalidPadding
}
}
return src[:length-padding], nil
}
func pkcs5Padding(src []byte, blocksize int) []byte {
padding := blocksize - (len(src) % blocksize)
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padText...)
}
+13 -9
View File
@@ -2,18 +2,22 @@
package crypto
import "errors"
import (
"bytes"
"crypto/aes"
)
var ErrDarwinNotSupportDPAPI = errors.New("darwin not support dpapi")
var chromiumCBCIV = bytes.Repeat([]byte{0x20}, aes.BlockSize)
func DecryptWithChromium(key, password []byte) ([]byte, error) {
if len(password) <= 3 {
return nil, ErrCiphertextLengthIsInvalid
const minCBCDataSize = versionPrefixLen + aes.BlockSize // "v10" + one AES block = 19 bytes minimum
func DecryptChromium(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minCBCDataSize {
return nil, errShortCiphertext
}
iv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
return AES128CBCDecrypt(key, iv, password[3:])
return AESCBCDecrypt(key, chromiumCBCIV, ciphertext[versionPrefixLen:])
}
func DecryptWithDPAPI(_ []byte) ([]byte, error) {
return nil, ErrDarwinNotSupportDPAPI
func DecryptDPAPI(_ []byte) ([]byte, error) {
return nil, errDPAPINotSupported
}
+15 -7
View File
@@ -2,14 +2,22 @@
package crypto
func DecryptWithChromium(key, encryptPass []byte) ([]byte, error) {
if len(encryptPass) < 3 {
return nil, ErrCiphertextLengthIsInvalid
import (
"bytes"
"crypto/aes"
)
var chromiumCBCIV = bytes.Repeat([]byte{0x20}, aes.BlockSize)
const minCBCDataSize = versionPrefixLen + aes.BlockSize // "v10" + one AES block = 19 bytes minimum
func DecryptChromium(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minCBCDataSize {
return nil, errShortCiphertext
}
iv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
return AES128CBCDecrypt(key, iv, encryptPass[3:])
return AESCBCDecrypt(key, chromiumCBCIV, ciphertext[versionPrefixLen:])
}
func DecryptWithDPAPI(_ []byte) ([]byte, error) {
return nil, nil
func DecryptDPAPI(_ []byte) ([]byte, error) {
return nil, errDPAPINotSupported
}
+118 -4
View File
@@ -27,16 +27,16 @@ var (
aesGCMCiphertext = "6c49dac89992639713edab3a114c450968a08b53556872cea3919e2e9a"
)
func TestAES128CBCEncrypt(t *testing.T) {
encrypted, err := AES128CBCEncrypt(aesKey, aesIV, plainText)
func TestAESCBCEncrypt(t *testing.T) {
encrypted, err := AESCBCEncrypt(aesKey, aesIV, plainText)
require.NoError(t, err)
assert.NotEmpty(t, encrypted)
assert.Equal(t, aes128Ciphertext, fmt.Sprintf("%x", encrypted))
}
func TestAES128CBCDecrypt(t *testing.T) {
func TestAESCBCDecrypt(t *testing.T) {
ciphertext, _ := hex.DecodeString(aes128Ciphertext)
decrypted, err := AES128CBCDecrypt(aesKey, aesIV, ciphertext)
decrypted, err := AESCBCDecrypt(aesKey, aesIV, ciphertext)
require.NoError(t, err)
assert.NotEmpty(t, decrypted)
assert.Equal(t, plainText, decrypted)
@@ -71,3 +71,117 @@ func TestAESGCMDecrypt(t *testing.T) {
assert.NotEmpty(t, decrypted)
assert.Equal(t, plainText, decrypted)
}
// --- Bug-fix verification tests ---
// These tests verify the fixes for known issues in the crypto primitives.
// Tests marked with t.Skip document bugs that exist before the fix.
func TestPkcs5Padding_NoMutation(t *testing.T) {
// pkcs5Padding should not mutate the original slice's backing array.
src := make([]byte, 3, 32) // len=3, cap=32 — append won't allocate
copy(src, "abc")
backup := make([]byte, cap(src))
copy(backup, src[:cap(src)])
padded := pkcs5Padding(src, 16)
assert.Len(t, padded, 16)
assert.Equal(t, []byte("abc"), src) // original length unchanged
// The bytes beyond len(src) in the original backing array must not be touched.
current := make([]byte, cap(src))
copy(current, src[:cap(src)])
assert.Equal(t, backup, current, "pkcs5Padding mutated the original slice backing array")
}
func TestPaddingZero_NoMutation(t *testing.T) {
src := make([]byte, 3, 32)
copy(src, "abc")
backup := make([]byte, cap(src))
copy(backup, src[:cap(src)])
padded := paddingZero(src, 20)
assert.Len(t, padded, 20)
current := make([]byte, cap(src))
copy(current, src[:cap(src)])
assert.Equal(t, backup, current, "paddingZero mutated the original slice backing array")
}
func TestAESCBCDecrypt_WrongIVLength(t *testing.T) {
key := bytes.Repeat([]byte("k"), 16)
wrongIV := []byte("short")
ct := make([]byte, 16)
_, err := AESCBCDecrypt(key, wrongIV, ct)
require.Error(t, err, "wrong IV length should return error, not panic")
assert.ErrorIs(t, err, errInvalidIVLength)
}
func TestAESCBCEncrypt_WrongIVLength(t *testing.T) {
key := bytes.Repeat([]byte("k"), 16)
wrongIV := []byte("short")
_, err := AESCBCEncrypt(key, wrongIV, plainText)
require.Error(t, err)
assert.ErrorIs(t, err, errInvalidIVLength)
}
func TestDES3Decrypt_WrongIVLength(t *testing.T) {
key := bytes.Repeat([]byte("k"), 24)
wrongIV := []byte("ab") // DES needs 8-byte IV
ct := make([]byte, 8)
_, err := DES3Decrypt(key, wrongIV, ct)
require.Error(t, err, "wrong IV length should return error, not panic")
assert.ErrorIs(t, err, errInvalidIVLength)
}
func TestDES3Encrypt_WrongIVLength(t *testing.T) {
key := bytes.Repeat([]byte("k"), 24)
wrongIV := []byte("ab")
_, err := DES3Encrypt(key, wrongIV, plainText)
require.Error(t, err, "wrong IV length should return error, not panic")
assert.ErrorIs(t, err, errInvalidIVLength)
}
func TestAESCBCDecrypt_EmptyCiphertext(t *testing.T) {
key := bytes.Repeat([]byte("k"), 16)
iv := bytes.Repeat([]byte("i"), 16)
_, err := AESCBCDecrypt(key, iv, nil)
require.Error(t, err)
_, err = AESCBCDecrypt(key, iv, []byte{})
require.Error(t, err)
}
func TestAESGCMEncrypt_WrongNonceLength(t *testing.T) {
key := bytes.Repeat([]byte("k"), 16)
wrongNonce := []byte("short")
_, err := AESGCMEncrypt(key, wrongNonce, plainText)
require.Error(t, err, "wrong nonce length should return error, not panic")
assert.ErrorIs(t, err, errInvalidNonceLen)
}
func TestAESGCMDecrypt_WrongNonceLength(t *testing.T) {
key := bytes.Repeat([]byte("k"), 16)
wrongNonce := []byte("short")
ct := make([]byte, 32)
_, err := AESGCMDecrypt(key, wrongNonce, ct)
require.Error(t, err, "wrong nonce length should return error, not panic")
assert.ErrorIs(t, err, errInvalidNonceLen)
}
func TestDES3Decrypt_EmptyCiphertext(t *testing.T) {
key := bytes.Repeat([]byte("k"), 24)
iv := bytes.Repeat([]byte("i"), 8)
_, err := DES3Decrypt(key, iv, nil)
require.Error(t, err)
_, err = DES3Decrypt(key, iv, []byte{})
require.Error(t, err)
}
+19 -25
View File
@@ -9,35 +9,29 @@ import (
)
const (
// Assuming the nonce size is 12 bytes and the minimum encrypted data size is 3 bytes
minEncryptedDataSize = 15
nonceSize = 12
gcmNonceSize = 12 // AES-GCM standard nonce size
minGCMDataSize = versionPrefixLen + gcmNonceSize // "v10" + nonce = 15 bytes minimum
)
func DecryptWithChromium(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minEncryptedDataSize {
return nil, ErrCiphertextLengthIsInvalid
func DecryptChromium(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minGCMDataSize {
return nil, errShortCiphertext
}
nonce := ciphertext[3 : 3+nonceSize]
encryptedPassword := ciphertext[3+nonceSize:]
return AESGCMDecrypt(key, nonce, encryptedPassword)
nonce := ciphertext[versionPrefixLen : versionPrefixLen+gcmNonceSize]
payload := ciphertext[versionPrefixLen+gcmNonceSize:]
return AESGCMDecrypt(key, nonce, payload)
}
// DecryptWithYandex decrypts the password with AES-GCM
func DecryptWithYandex(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minEncryptedDataSize {
return nil, ErrCiphertextLengthIsInvalid
// DecryptYandex decrypts a Yandex-encrypted value.
// TODO: Yandex uses the same AES-GCM format as Chromium for now;
// update when Yandex-specific decryption diverges.
func DecryptYandex(key, ciphertext []byte) ([]byte, error) {
if len(ciphertext) < minGCMDataSize {
return nil, errShortCiphertext
}
// remove Prefix 'v10'
// gcmBlockSize = 16
// gcmTagSize = 16
// gcmMinimumTagSize = 12 // NIST SP 800-38D recommends tags with 12 or more bytes.
// gcmStandardNonceSize = 12
nonce := ciphertext[3 : 3+nonceSize]
encryptedPassword := ciphertext[3+nonceSize:]
return AESGCMDecrypt(key, nonce, encryptedPassword)
nonce := ciphertext[versionPrefixLen : versionPrefixLen+gcmNonceSize]
payload := ciphertext[versionPrefixLen+gcmNonceSize:]
return AESGCMDecrypt(key, nonce, payload)
}
type dataBlob struct {
@@ -61,11 +55,11 @@ func (b *dataBlob) bytes() []byte {
return d
}
// DecryptWithDPAPI (Data Protection Application Programming Interface)
// DecryptDPAPI (Data Protection Application Programming Interface)
// is a simple cryptographic application programming interface
// available as a built-in component in Windows 2000 and
// later versions of Microsoft Windows operating systems
func DecryptWithDPAPI(ciphertext []byte) ([]byte, error) {
func DecryptDPAPI(ciphertext []byte) ([]byte, error) {
crypt32 := syscall.NewLazyDLL("Crypt32.dll")
kernel32 := syscall.NewLazyDLL("Kernel32.dll")
unprotectDataProc := crypt32.NewProc("CryptUnprotectData")
+15
View File
@@ -0,0 +1,15 @@
package crypto
import "errors"
// Sentinel errors for crypto operations.
var (
errShortCiphertext = errors.New("ciphertext too short")
errInvalidBlockSize = errors.New("ciphertext is not a multiple of the block size")
errInvalidIVLength = errors.New("IV length must equal block size")
errInvalidPadding = errors.New("invalid PKCS5 padding")
errInvalidNonceLen = errors.New("nonce length must equal GCM nonce size")
errUnsupportedIVLen = errors.New("unsupported IV length")
errDecodeASN1 = errors.New("failed to decode ASN1 data")
errDPAPINotSupported = errors.New("DPAPI not supported on this platform") //nolint:unused // used on darwin/linux only
)
+3 -3
View File
@@ -69,7 +69,7 @@ type addressRange struct {
// DecryptKeychain extracts the browser storage password from login.keychain-db
// by dumping securityd memory and scanning for the keychain master key.
// Requires root privileges.
func DecryptKeychain(storagename string) (string, error) {
func DecryptKeychain(storageName string) (string, error) {
if os.Geteuid() != 0 {
return "", errors.New("requires root privileges")
}
@@ -124,13 +124,13 @@ func DecryptKeychain(storagename string) (string, error) {
continue
}
for _, rec := range records {
if rec.Account == storagename {
if rec.Account == storageName {
return string(rec.Password), nil
}
}
}
return "", fmt.Errorf("tried %d candidates, none matched storage %q", len(candidates), storagename)
return "", fmt.Errorf("tried %d candidates, none matched storage %q", len(candidates), storageName)
}
// scanMasterKeyCandidates scans the core dump for 24-byte master key candidates.
+1 -1
View File
@@ -20,7 +20,7 @@ import (
var darwinParams = pbkdf2Params{
salt: []byte("saltysalt"),
iterations: 1003,
keyLen: 16,
keySize: 16,
hashFunc: sha1.New,
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
var linuxParams = pbkdf2Params{
salt: []byte("saltysalt"),
iterations: 1,
keyLen: 16,
keySize: 16,
hashFunc: sha1.New,
}
+1 -1
View File
@@ -41,7 +41,7 @@ func (r *DPAPIRetriever) RetrieveKey(_, localStatePath string) ([]byte, error) {
return nil, fmt.Errorf("encrypted_key unexpected prefix: got %q, want %q", keyBytes[:len(dpapiPrefix)], dpapiPrefix)
}
masterKey, err := crypto.DecryptWithDPAPI(keyBytes[len(dpapiPrefix):])
masterKey, err := crypto.DecryptDPAPI(keyBytes[len(dpapiPrefix):])
if err != nil {
return nil, fmt.Errorf("DPAPI decrypt: %w", err)
}
+2 -2
View File
@@ -13,11 +13,11 @@ import (
type pbkdf2Params struct {
salt []byte
iterations int
keyLen int
keySize int
hashFunc func() hash.Hash
}
// deriveKey derives an encryption key from a secret using PBKDF2.
func (p pbkdf2Params) deriveKey(secret []byte) []byte {
return crypto.PBKDF2Key(secret, p.salt, p.iterations, p.keyLen, p.hashFunc)
return crypto.PBKDF2Key(secret, p.salt, p.iterations, p.keySize, p.hashFunc)
}
+2 -2
View File
@@ -22,7 +22,7 @@ import (
// Using a higher iteration count will increase the cost of an exhaustive
// search but will also make derivation proportionally slower.
// Copy from https://golang.org/x/crypto/pbkdf2
func PBKDF2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
func PBKDF2Key(password, salt []byte, iterations, keyLen int, h func() hash.Hash) []byte {
prf := hmac.New(h, password)
hashLen := prf.Size()
numBlocks := (keyLen + hashLen - 1) / hashLen
@@ -45,7 +45,7 @@ func PBKDF2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []by
t := dk[len(dk)-hashLen:]
copy(u, t)
for n := 2; n <= iter; n++ {
for n := 2; n <= iterations; n++ {
prf.Reset()
prf.Write(u)
u = u[:0]
+61
View File
@@ -0,0 +1,61 @@
package crypto
import (
"crypto/sha1"
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
)
// Test vectors from RFC 6070 (PKCS #5 PBKDF2 with HMAC-SHA1).
// https://www.rfc-editor.org/rfc/rfc6070
func TestPBKDF2Key_RFC6070(t *testing.T) {
tests := []struct {
name string
password string
salt string
iterations int
keyLen int
want string
}{
{
name: "iteration=1",
password: "password",
salt: "salt",
iterations: 1,
keyLen: 20,
want: "0c60c80f961f0e71f3a9b524af6012062fe037a6",
},
{
name: "iteration=2",
password: "password",
salt: "salt",
iterations: 2,
keyLen: 20,
want: "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957",
},
{
name: "iteration=4096",
password: "password",
salt: "salt",
iterations: 4096,
keyLen: 20,
want: "4b007901b765489abead49d926f721d065a429c1",
},
{
name: "long_password_and_salt",
password: "passwordPASSWORDpassword",
salt: "saltSALTsaltSALTsaltSALTsaltSALTsalt",
iterations: 4096,
keyLen: 25,
want: "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := PBKDF2Key([]byte(tt.password), []byte(tt.salt), tt.iterations, tt.keyLen, sha1.New)
assert.Equal(t, tt.want, hex.EncodeToString(got))
})
}
}
+8 -5
View File
@@ -12,14 +12,17 @@ const (
// CipherDPAPI is pre-Chrome 80 raw DPAPI encryption (no version prefix).
CipherDPAPI CipherVersion = "dpapi"
// versionPrefixLen is the byte length of the version prefix ("v10", "v20").
versionPrefixLen = 3
)
// DetectVersion identifies the encryption version from a ciphertext prefix.
func DetectVersion(ciphertext []byte) CipherVersion {
if len(ciphertext) < 3 {
if len(ciphertext) < versionPrefixLen {
return CipherDPAPI
}
prefix := string(ciphertext[:3])
prefix := string(ciphertext[:versionPrefixLen])
switch prefix {
case "v10":
return CipherV10
@@ -30,12 +33,12 @@ func DetectVersion(ciphertext []byte) CipherVersion {
}
}
// StripPrefix removes the version prefix (e.g. "v10") from ciphertext.
// stripPrefix removes the version prefix (e.g. "v10") from ciphertext.
// Returns the ciphertext unchanged if no known prefix is found.
func StripPrefix(ciphertext []byte) []byte {
func stripPrefix(ciphertext []byte) []byte {
ver := DetectVersion(ciphertext)
if ver == CipherV10 || ver == CipherV20 {
return ciphertext[3:]
return ciphertext[versionPrefixLen:]
}
return ciphertext
}
+2 -2
View File
@@ -27,7 +27,7 @@ func TestDetectVersion(t *testing.T) {
}
}
func TestStripPrefix(t *testing.T) {
func Test_stripPrefix(t *testing.T) {
tests := []struct {
name string
ciphertext []byte
@@ -41,7 +41,7 @@ func TestStripPrefix(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, StripPrefix(tt.ciphertext))
assert.Equal(t, tt.want, stripPrefix(tt.ciphertext))
})
}
}