feat(yandex): password and credit card decryption (#585)

This commit is contained in:
Roger
2026-04-23 17:00:09 +08:00
committed by GitHub
parent 7e64d50891
commit 0c6c781567
17 changed files with 1005 additions and 48 deletions
+17
View File
@@ -98,6 +98,23 @@ func AESGCMDecrypt(key, nonce, ciphertext []byte) ([]byte, error) {
return aead.Open(nil, nonce, ciphertext, nil)
}
// AESGCMDecryptBlob decrypts a blob shaped as [12B nonce][ciphertext+16B GCM tag] with caller-supplied AAD.
// Used by protocols that wrap AES-GCM output with a fixed-length nonce prefix (Yandex passwords/cards).
func AESGCMDecryptBlob(key, blob, aad []byte) ([]byte, error) {
if len(blob) < gcmNonceSize {
return nil, errShortCiphertext
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return aead.Open(nil, blob[:gcmNonceSize], blob[gcmNonceSize:], aad)
}
// 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() {
-12
View File
@@ -18,18 +18,6 @@ func DecryptChromium(key, ciphertext []byte) ([]byte, error) {
return AESGCMDecrypt(key, nonce, payload)
}
// 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
}
nonce := ciphertext[versionPrefixLen : versionPrefixLen+gcmNonceSize]
payload := ciphertext[versionPrefixLen+gcmNonceSize:]
return AESGCMDecrypt(key, nonce, payload)
}
// DecryptDPAPI decrypts a DPAPI-protected blob using the current user's
// master key. The actual Win32 call (and its DATA_BLOB / LocalFree dance)
// lives in utils/winapi so every package that needs a syscall handle
+48
View File
@@ -0,0 +1,48 @@
package crypto
import (
"bytes"
"errors"
)
// yandexSignature is the protobuf wire-format header (field1 varint=1, field2 len=32) on every wrapped key.
var yandexSignature = []byte{0x08, 0x01, 0x12, 0x20}
var localEncryptorPrefix = []byte("v10")
const (
yandexIntKeyBlobLen = 96 // 12B nonce + 68B ciphertext + 16B GCM tag
yandexDataKeyLen = 32
)
var (
errYandexMarkerNotFound = errors.New("yandex: v10 marker not found in local_encryptor_data")
errYandexBlobShort = errors.New("yandex: encrypted intermediate key truncated")
errYandexBadSignature = errors.New("yandex: invalid protobuf signature on decrypted key")
errYandexKeyTooShort = errors.New("yandex: decrypted intermediate key shorter than 32 bytes")
)
// DecryptYandexIntermediateKey unwraps the per-DB data key from meta.local_encryptor_data. See RFC-012 §4.2.
func DecryptYandexIntermediateKey(masterKey, blob []byte) ([]byte, error) {
idx := bytes.Index(blob, localEncryptorPrefix)
if idx < 0 {
return nil, errYandexMarkerNotFound
}
payload := blob[idx+len(localEncryptorPrefix):]
if len(payload) < yandexIntKeyBlobLen {
return nil, errYandexBlobShort
}
plaintext, err := AESGCMDecryptBlob(masterKey, payload[:yandexIntKeyBlobLen], nil)
if err != nil {
return nil, err
}
if !bytes.HasPrefix(plaintext, yandexSignature) {
return nil, errYandexBadSignature
}
plaintext = plaintext[len(yandexSignature):]
if len(plaintext) < yandexDataKeyLen {
return nil, errYandexKeyTooShort
}
return plaintext[:yandexDataKeyLen], nil
}
+150
View File
@@ -0,0 +1,150 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"errors"
"testing"
)
// encryptAESGCM is a test helper that produces a GCM ciphertext with caller-supplied AAD.
func encryptAESGCM(t *testing.T, key, nonce, plaintext, aad []byte) []byte {
t.Helper()
block, err := aes.NewCipher(key)
if err != nil {
t.Fatalf("aes.NewCipher: %v", err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
t.Fatalf("cipher.NewGCM: %v", err)
}
return aead.Seal(nil, nonce, plaintext, aad)
}
// testPlaintextPayloadLen: plaintext size before AES-GCM seal inside meta.local_encryptor_data.
// 96 (blob) - 12 (nonce) - 16 (tag) = 68 bytes.
const testPlaintextPayloadLen = yandexIntKeyBlobLen - gcmNonceSize - 16
func buildLocalEncryptorBlob(t *testing.T, masterKey, dataKey []byte) []byte {
t.Helper()
nonce := bytes.Repeat([]byte{0xAB}, gcmNonceSize)
plaintext := append([]byte{}, yandexSignature...)
plaintext = append(plaintext, dataKey...)
plaintext = append(plaintext, make([]byte, testPlaintextPayloadLen-len(plaintext))...)
ciphertext := encryptAESGCM(t, masterKey, nonce, plaintext, nil)
if len(ciphertext) != yandexIntKeyBlobLen-gcmNonceSize {
t.Fatalf("unexpected ciphertext len: got %d want %d", len(ciphertext), yandexIntKeyBlobLen-gcmNonceSize)
}
blob := []byte{0x01, 0x02, 0x03, 0x04} // arbitrary protobuf preamble
blob = append(blob, localEncryptorPrefix...)
blob = append(blob, nonce...)
blob = append(blob, ciphertext...)
blob = append(blob, 0xFF, 0xFE) // trailing junk should be ignored
return blob
}
func TestDecryptYandexIntermediateKey_RoundTrip(t *testing.T) {
masterKey := bytes.Repeat([]byte{0x11}, 32)
dataKey := bytes.Repeat([]byte{0x22}, yandexDataKeyLen)
blob := buildLocalEncryptorBlob(t, masterKey, dataKey)
got, err := DecryptYandexIntermediateKey(masterKey, blob)
if err != nil {
t.Fatalf("DecryptYandexIntermediateKey: %v", err)
}
if !bytes.Equal(got, dataKey) {
t.Errorf("key mismatch: got %x want %x", got, dataKey)
}
}
func TestDecryptYandexIntermediateKey_MissingMarker(t *testing.T) {
_, err := DecryptYandexIntermediateKey(bytes.Repeat([]byte{0x11}, 32), []byte("no marker here"))
if !errors.Is(err, errYandexMarkerNotFound) {
t.Fatalf("expected errYandexMarkerNotFound, got %v", err)
}
}
func TestDecryptYandexIntermediateKey_Truncated(t *testing.T) {
blob := append([]byte{0x00, 0x00}, localEncryptorPrefix...)
blob = append(blob, bytes.Repeat([]byte{0x55}, yandexIntKeyBlobLen-1)...)
_, err := DecryptYandexIntermediateKey(bytes.Repeat([]byte{0x11}, 32), blob)
if !errors.Is(err, errYandexBlobShort) {
t.Fatalf("expected errYandexBlobShort, got %v", err)
}
}
func TestDecryptYandexIntermediateKey_BadSignature(t *testing.T) {
masterKey := bytes.Repeat([]byte{0x11}, 32)
nonce := bytes.Repeat([]byte{0xAB}, gcmNonceSize)
plaintext := append([]byte{0xDE, 0xAD, 0xBE, 0xEF}, bytes.Repeat([]byte{0x22}, yandexDataKeyLen)...)
plaintext = append(plaintext, make([]byte, testPlaintextPayloadLen-len(plaintext))...)
ciphertext := encryptAESGCM(t, masterKey, nonce, plaintext, nil)
blob := append([]byte{}, localEncryptorPrefix...)
blob = append(blob, nonce...)
blob = append(blob, ciphertext...)
_, err := DecryptYandexIntermediateKey(masterKey, blob)
if !errors.Is(err, errYandexBadSignature) {
t.Fatalf("expected errYandexBadSignature, got %v", err)
}
}
// TestDecryptYandexIntermediateKey_TrailingDataIgnored verifies that trailing bytes past
// signature+32 are discarded.
func TestDecryptYandexIntermediateKey_TrailingDataIgnored(t *testing.T) {
masterKey := bytes.Repeat([]byte{0x11}, 32)
nonce := bytes.Repeat([]byte{0xAB}, gcmNonceSize)
plaintext := append([]byte{}, yandexSignature...)
plaintext = append(plaintext, bytes.Repeat([]byte{0x22}, 16)...)
plaintext = append(plaintext, make([]byte, testPlaintextPayloadLen-len(plaintext))...)
ciphertext := encryptAESGCM(t, masterKey, nonce, plaintext, nil)
blob := append([]byte{}, localEncryptorPrefix...)
blob = append(blob, nonce...)
blob = append(blob, ciphertext...)
got, err := DecryptYandexIntermediateKey(masterKey, blob)
if err != nil {
t.Fatalf("DecryptYandexIntermediateKey: %v", err)
}
want := bytes.Repeat([]byte{0x22}, 16)
want = append(want, make([]byte, 16)...)
if !bytes.Equal(got, want) {
t.Errorf("key mismatch: got %x want %x", got, want)
}
}
func TestAESGCMDecryptBlob_RoundTrip(t *testing.T) {
key := bytes.Repeat([]byte{0x55}, 32)
nonce := bytes.Repeat([]byte{0x66}, gcmNonceSize)
aad := []byte("row-aad")
plaintext := []byte("row-plaintext")
blob := append([]byte{}, nonce...)
blob = append(blob, encryptAESGCM(t, key, nonce, plaintext, aad)...)
got, err := AESGCMDecryptBlob(key, blob, aad)
if err != nil {
t.Fatalf("AESGCMDecryptBlob: %v", err)
}
if !bytes.Equal(got, plaintext) {
t.Errorf("plaintext mismatch: got %q want %q", got, plaintext)
}
}
func TestAESGCMDecryptBlob_BadAAD(t *testing.T) {
key := bytes.Repeat([]byte{0x55}, 32)
nonce := bytes.Repeat([]byte{0x66}, gcmNonceSize)
blob := append([]byte{}, nonce...)
blob = append(blob, encryptAESGCM(t, key, nonce, []byte("x"), []byte("aad-A"))...)
if _, err := AESGCMDecryptBlob(key, blob, []byte("aad-B")); err == nil {
t.Fatal("expected authentication failure with mismatched AAD")
}
}
func TestAESGCMDecryptBlob_TooShort(t *testing.T) {
_, err := AESGCMDecryptBlob(bytes.Repeat([]byte{0x55}, 32), []byte{0x01, 0x02}, nil)
if !errors.Is(err, errShortCiphertext) {
t.Fatalf("expected errShortCiphertext, got %v", err)
}
}