* feat: add organization vault support - Add RSA-OAEP decryption (type 3: SHA-256, type 4: SHA-1) - Decrypt user's RSA private key from sync response (PKCS8 DER) - Decrypt organization keys using user's RSA private key - Route cipher decryption to correct key based on organizationId - Personal vault items continue to work unchanged Adds 11 new unit tests covering: - RSA cipher string parsing (type 3/4) - RSA-OAEP round-trip (SHA-1 and SHA-256) - Private key decrypt/parse round-trip - Org key decrypt + encKey/macKey split - Cipher decryption with org vs personal key - Backward compatibility for personal items Also adds CI/CD pipeline (.github/workflows/ci.yml): - Tests + build on PRs and main pushes - Docker build + push to GHCR on main * fix: split concatenated .gitignore entries * chore: clean up gitignore
727 lines
18 KiB
Go
727 lines
18 KiB
Go
package vaultwarden
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/pbkdf2"
|
|
)
|
|
|
|
func TestParseCipherString_Type2(t *testing.T) {
|
|
// Build a known cipher string of type 2 (AES-CBC-256 + HMAC-SHA256).
|
|
iv := make([]byte, 16)
|
|
ct := make([]byte, 32) // 2 blocks
|
|
mac := make([]byte, 32)
|
|
for i := range iv {
|
|
iv[i] = byte(i)
|
|
}
|
|
for i := range ct {
|
|
ct[i] = byte(i + 16)
|
|
}
|
|
for i := range mac {
|
|
mac[i] = byte(i + 48)
|
|
}
|
|
|
|
s := "2." + base64.StdEncoding.EncodeToString(iv) + "|" +
|
|
base64.StdEncoding.EncodeToString(ct) + "|" +
|
|
base64.StdEncoding.EncodeToString(mac)
|
|
|
|
cs, err := ParseCipherString(s)
|
|
if err != nil {
|
|
t.Fatalf("ParseCipherString failed: %v", err)
|
|
}
|
|
if cs.Type != 2 {
|
|
t.Errorf("expected type 2, got %d", cs.Type)
|
|
}
|
|
if len(cs.IV) != 16 {
|
|
t.Errorf("expected IV length 16, got %d", len(cs.IV))
|
|
}
|
|
if len(cs.CT) != 32 {
|
|
t.Errorf("expected CT length 32, got %d", len(cs.CT))
|
|
}
|
|
if len(cs.MAC) != 32 {
|
|
t.Errorf("expected MAC length 32, got %d", len(cs.MAC))
|
|
}
|
|
}
|
|
|
|
func TestParseCipherString_Type0(t *testing.T) {
|
|
iv := make([]byte, 16)
|
|
ct := make([]byte, 32)
|
|
s := "0." + base64.StdEncoding.EncodeToString(iv) + "|" +
|
|
base64.StdEncoding.EncodeToString(ct)
|
|
|
|
cs, err := ParseCipherString(s)
|
|
if err != nil {
|
|
t.Fatalf("ParseCipherString failed: %v", err)
|
|
}
|
|
if cs.Type != 0 {
|
|
t.Errorf("expected type 0, got %d", cs.Type)
|
|
}
|
|
if cs.MAC != nil {
|
|
t.Error("expected nil MAC for type 0")
|
|
}
|
|
}
|
|
|
|
func TestParseCipherString_Invalid(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
}{
|
|
{"empty", ""},
|
|
{"no type", "abcdef"},
|
|
{"bad type", "x.abc|def|ghi"},
|
|
{"type 2 missing mac", "2.abc|def"},
|
|
{"type 0 extra parts", "0.abc|def|ghi"},
|
|
{"unsupported type", "5.abc|def"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := ParseCipherString(tt.input)
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPKCS7Unpad(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []byte
|
|
want []byte
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "1 byte padding",
|
|
input: []byte{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x01},
|
|
want: []byte{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F},
|
|
},
|
|
{
|
|
name: "full block padding",
|
|
input: append(make([]byte, 0), 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10),
|
|
want: []byte{},
|
|
},
|
|
{
|
|
name: "empty input",
|
|
input: []byte{},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "zero padding byte",
|
|
input: []byte{0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x00},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := pkcs7Unpad(tt.input, 16)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Error("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(got) != len(tt.want) {
|
|
t.Errorf("length mismatch: got %d, want %d", len(got), len(tt.want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecrypt_Type2_RoundTrip(t *testing.T) {
|
|
// Create a known key, encrypt some data, then decrypt it.
|
|
encKey := make([]byte, 32)
|
|
macKey := make([]byte, 32)
|
|
for i := range encKey {
|
|
encKey[i] = byte(i)
|
|
}
|
|
for i := range macKey {
|
|
macKey[i] = byte(i + 32)
|
|
}
|
|
key := SymmetricKey{EncKey: encKey, MacKey: macKey}
|
|
|
|
plaintext := []byte("Hello, Vaultwarden!")
|
|
|
|
// Pad the plaintext with PKCS7.
|
|
padLen := aes.BlockSize - (len(plaintext) % aes.BlockSize)
|
|
padded := make([]byte, len(plaintext)+padLen)
|
|
copy(padded, plaintext)
|
|
for i := len(plaintext); i < len(padded); i++ {
|
|
padded[i] = byte(padLen)
|
|
}
|
|
|
|
// Encrypt with AES-CBC.
|
|
iv := make([]byte, 16)
|
|
for i := range iv {
|
|
iv[i] = byte(i + 100)
|
|
}
|
|
|
|
block, err := aes.NewCipher(encKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ct := make([]byte, len(padded))
|
|
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ct, padded)
|
|
|
|
// Compute MAC.
|
|
mac := hmac.New(sha256.New, macKey)
|
|
mac.Write(iv)
|
|
mac.Write(ct)
|
|
macBytes := mac.Sum(nil)
|
|
|
|
// Build cipher string.
|
|
s := "2." + base64.StdEncoding.EncodeToString(iv) + "|" +
|
|
base64.StdEncoding.EncodeToString(ct) + "|" +
|
|
base64.StdEncoding.EncodeToString(macBytes)
|
|
|
|
cs, err := ParseCipherString(s)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
result, err := cs.DecryptToString(key)
|
|
if err != nil {
|
|
t.Fatalf("decrypt: %v", err)
|
|
}
|
|
|
|
if result != "Hello, Vaultwarden!" {
|
|
t.Errorf("got %q, want %q", result, "Hello, Vaultwarden!")
|
|
}
|
|
}
|
|
|
|
func TestDecrypt_MACVerificationFails(t *testing.T) {
|
|
encKey := make([]byte, 32)
|
|
macKey := make([]byte, 32)
|
|
key := SymmetricKey{EncKey: encKey, MacKey: macKey}
|
|
|
|
// Create a valid ciphertext but with a wrong MAC.
|
|
iv := make([]byte, 16)
|
|
ct := make([]byte, 16)
|
|
wrongMAC := make([]byte, 32)
|
|
for i := range wrongMAC {
|
|
wrongMAC[i] = 0xFF
|
|
}
|
|
|
|
s := "2." + base64.StdEncoding.EncodeToString(iv) + "|" +
|
|
base64.StdEncoding.EncodeToString(ct) + "|" +
|
|
base64.StdEncoding.EncodeToString(wrongMAC)
|
|
|
|
cs, err := ParseCipherString(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = cs.Decrypt(key)
|
|
if err == nil {
|
|
t.Error("expected MAC verification error")
|
|
}
|
|
if !strings.Contains(err.Error(), "MAC verification failed") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMakeMasterKey_PBKDF2(t *testing.T) {
|
|
key, err := MakeMasterKey("password123", "user@example.com", KdfPBKDF2, 600000, nil, nil)
|
|
if err != nil {
|
|
t.Fatalf("MakeMasterKey failed: %v", err)
|
|
}
|
|
if len(key) != 32 {
|
|
t.Errorf("expected 32 bytes, got %d", len(key))
|
|
}
|
|
|
|
// Verify it matches raw PBKDF2.
|
|
expected := pbkdf2.Key([]byte("password123"), []byte("user@example.com"), 600000, 32, sha256.New)
|
|
for i := range key {
|
|
if key[i] != expected[i] {
|
|
t.Fatalf("key mismatch at byte %d", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMakeMasterKey_Argon2id(t *testing.T) {
|
|
mem := 64
|
|
par := 4
|
|
key, err := MakeMasterKey("password123", "user@example.com", KdfArgon2id, 3, &mem, &par)
|
|
if err != nil {
|
|
t.Fatalf("MakeMasterKey failed: %v", err)
|
|
}
|
|
if len(key) != 32 {
|
|
t.Errorf("expected 32 bytes, got %d", len(key))
|
|
}
|
|
}
|
|
|
|
func TestHashPassword(t *testing.T) {
|
|
masterKey := make([]byte, 32)
|
|
for i := range masterKey {
|
|
masterKey[i] = byte(i)
|
|
}
|
|
|
|
hash := HashPassword("password123", masterKey)
|
|
if hash == "" {
|
|
t.Error("empty hash")
|
|
}
|
|
|
|
// Should be valid base64.
|
|
_, err := base64.StdEncoding.DecodeString(hash)
|
|
if err != nil {
|
|
t.Errorf("hash is not valid base64: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStretchKey(t *testing.T) {
|
|
masterKey := make([]byte, 32)
|
|
for i := range masterKey {
|
|
masterKey[i] = byte(i)
|
|
}
|
|
|
|
key, err := StretchKey(masterKey)
|
|
if err != nil {
|
|
t.Fatalf("StretchKey failed: %v", err)
|
|
}
|
|
|
|
if len(key.EncKey) != 32 {
|
|
t.Errorf("expected 32-byte enc key, got %d", len(key.EncKey))
|
|
}
|
|
if len(key.MacKey) != 32 {
|
|
t.Errorf("expected 32-byte mac key, got %d", len(key.MacKey))
|
|
}
|
|
|
|
// Enc and Mac keys should be different.
|
|
same := true
|
|
for i := range key.EncKey {
|
|
if key.EncKey[i] != key.MacKey[i] {
|
|
same = false
|
|
break
|
|
}
|
|
}
|
|
if same {
|
|
t.Error("enc and mac keys should differ")
|
|
}
|
|
}
|
|
|
|
func TestDecryptStr_Empty(t *testing.T) {
|
|
key := SymmetricKey{EncKey: make([]byte, 32), MacKey: make([]byte, 32)}
|
|
result, err := DecryptStr("", key)
|
|
if err != nil {
|
|
t.Fatalf("DecryptStr empty should not error: %v", err)
|
|
}
|
|
if result != "" {
|
|
t.Errorf("expected empty string, got %q", result)
|
|
}
|
|
}
|
|
|
|
// --- RSA / Organization key tests ---
|
|
|
|
func TestParseCipherString_Type3_RSA_SHA256(t *testing.T) {
|
|
// Type 3: RSA-2048 OAEP SHA-256. Single base64 ciphertext, no IV or MAC.
|
|
ct := make([]byte, 256) // RSA-2048 ciphertext is 256 bytes
|
|
for i := range ct {
|
|
ct[i] = byte(i)
|
|
}
|
|
s := "3." + base64.StdEncoding.EncodeToString(ct)
|
|
|
|
cs, err := ParseCipherString(s)
|
|
if err != nil {
|
|
t.Fatalf("ParseCipherString type 3 failed: %v", err)
|
|
}
|
|
if cs.Type != EncTypeRsa2048_OaepSha256_B64 {
|
|
t.Errorf("expected type %d, got %d", EncTypeRsa2048_OaepSha256_B64, cs.Type)
|
|
}
|
|
if len(cs.CT) != 256 {
|
|
t.Errorf("expected CT length 256, got %d", len(cs.CT))
|
|
}
|
|
if cs.IV != nil {
|
|
t.Error("expected nil IV for RSA type")
|
|
}
|
|
if cs.MAC != nil {
|
|
t.Error("expected nil MAC for RSA type")
|
|
}
|
|
}
|
|
|
|
func TestParseCipherString_Type4_RSA_SHA1(t *testing.T) {
|
|
ct := make([]byte, 256)
|
|
for i := range ct {
|
|
ct[i] = byte(i)
|
|
}
|
|
s := "4." + base64.StdEncoding.EncodeToString(ct)
|
|
|
|
cs, err := ParseCipherString(s)
|
|
if err != nil {
|
|
t.Fatalf("ParseCipherString type 4 failed: %v", err)
|
|
}
|
|
if cs.Type != EncTypeRsa2048_OaepSha1_B64 {
|
|
t.Errorf("expected type %d, got %d", EncTypeRsa2048_OaepSha1_B64, cs.Type)
|
|
}
|
|
if len(cs.CT) != 256 {
|
|
t.Errorf("expected CT length 256, got %d", len(cs.CT))
|
|
}
|
|
}
|
|
|
|
func TestDecryptRSA_OAEP_SHA1_RoundTrip(t *testing.T) {
|
|
// Generate RSA key pair.
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("generate RSA key: %v", err)
|
|
}
|
|
|
|
// Encrypt a 64-byte org key with RSA-OAEP SHA-1.
|
|
orgKeyPlain := make([]byte, 64)
|
|
for i := range orgKeyPlain {
|
|
orgKeyPlain[i] = byte(i)
|
|
}
|
|
|
|
ciphertext, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, &privateKey.PublicKey, orgKeyPlain, nil)
|
|
if err != nil {
|
|
t.Fatalf("RSA encrypt: %v", err)
|
|
}
|
|
|
|
// Build cipher string type 4.
|
|
csStr := "4." + base64.StdEncoding.EncodeToString(ciphertext)
|
|
cs, err := ParseCipherString(csStr)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
decrypted, err := cs.DecryptRSA(privateKey)
|
|
if err != nil {
|
|
t.Fatalf("DecryptRSA: %v", err)
|
|
}
|
|
|
|
if len(decrypted) != 64 {
|
|
t.Fatalf("expected 64 bytes, got %d", len(decrypted))
|
|
}
|
|
for i := range orgKeyPlain {
|
|
if decrypted[i] != orgKeyPlain[i] {
|
|
t.Fatalf("mismatch at byte %d", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecryptRSA_OAEP_SHA256_RoundTrip(t *testing.T) {
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("generate RSA key: %v", err)
|
|
}
|
|
|
|
plaintext := []byte("Hello RSA-OAEP-SHA256!")
|
|
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &privateKey.PublicKey, plaintext, nil)
|
|
if err != nil {
|
|
t.Fatalf("RSA encrypt: %v", err)
|
|
}
|
|
|
|
csStr := "3." + base64.StdEncoding.EncodeToString(ciphertext)
|
|
cs, err := ParseCipherString(csStr)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
decrypted, err := cs.DecryptRSA(privateKey)
|
|
if err != nil {
|
|
t.Fatalf("DecryptRSA: %v", err)
|
|
}
|
|
|
|
if string(decrypted) != string(plaintext) {
|
|
t.Errorf("got %q, want %q", decrypted, plaintext)
|
|
}
|
|
}
|
|
|
|
func TestDecryptRSA_WrongType(t *testing.T) {
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Type 2 is AES, not RSA.
|
|
cs := &CipherString{Type: EncTypeAesCbc256_HmacSha256_B64, CT: []byte("test")}
|
|
_, err = cs.DecryptRSA(privateKey)
|
|
if err == nil {
|
|
t.Error("expected error for non-RSA type")
|
|
}
|
|
}
|
|
|
|
func TestDecryptPrivateKey_RoundTrip(t *testing.T) {
|
|
// Generate RSA key pair.
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("generate RSA key: %v", err)
|
|
}
|
|
|
|
// Marshal to PKCS8 DER.
|
|
derBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
t.Fatalf("marshal PKCS8: %v", err)
|
|
}
|
|
|
|
// Encrypt with AES-CBC + HMAC (type 2) using a known symmetric key.
|
|
encKey := make([]byte, 32)
|
|
macKey := make([]byte, 32)
|
|
for i := range encKey {
|
|
encKey[i] = byte(i)
|
|
}
|
|
for i := range macKey {
|
|
macKey[i] = byte(i + 32)
|
|
}
|
|
symKey := SymmetricKey{EncKey: encKey, MacKey: macKey}
|
|
|
|
// PKCS7 pad the DER bytes.
|
|
padLen := aes.BlockSize - (len(derBytes) % aes.BlockSize)
|
|
padded := make([]byte, len(derBytes)+padLen)
|
|
copy(padded, derBytes)
|
|
for i := len(derBytes); i < len(padded); i++ {
|
|
padded[i] = byte(padLen)
|
|
}
|
|
|
|
// Encrypt.
|
|
iv := make([]byte, 16)
|
|
for i := range iv {
|
|
iv[i] = byte(i + 100)
|
|
}
|
|
block, err := aes.NewCipher(encKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ct := make([]byte, len(padded))
|
|
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ct, padded)
|
|
|
|
// Compute MAC.
|
|
mac := hmac.New(sha256.New, macKey)
|
|
mac.Write(iv)
|
|
mac.Write(ct)
|
|
macBytes := mac.Sum(nil)
|
|
|
|
// Build cipher string.
|
|
csStr := fmt.Sprintf("2.%s|%s|%s",
|
|
base64.StdEncoding.EncodeToString(iv),
|
|
base64.StdEncoding.EncodeToString(ct),
|
|
base64.StdEncoding.EncodeToString(macBytes),
|
|
)
|
|
|
|
// Decrypt and verify.
|
|
decryptedKey, err := DecryptPrivateKey(csStr, symKey)
|
|
if err != nil {
|
|
t.Fatalf("DecryptPrivateKey: %v", err)
|
|
}
|
|
|
|
// Verify the decrypted key matches the original.
|
|
if decryptedKey.N.Cmp(privateKey.N) != 0 {
|
|
t.Error("decrypted private key N mismatch")
|
|
}
|
|
if decryptedKey.D.Cmp(privateKey.D) != 0 {
|
|
t.Error("decrypted private key D mismatch")
|
|
}
|
|
}
|
|
|
|
func TestDecryptPrivateKey_Empty(t *testing.T) {
|
|
symKey := SymmetricKey{EncKey: make([]byte, 32), MacKey: make([]byte, 32)}
|
|
_, err := DecryptPrivateKey("", symKey)
|
|
if err == nil {
|
|
t.Error("expected error for empty private key")
|
|
}
|
|
}
|
|
|
|
func TestDecryptOrgKey_RoundTrip(t *testing.T) {
|
|
// Generate RSA key pair.
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("generate RSA key: %v", err)
|
|
}
|
|
|
|
// Create a 64-byte org key (32 enc + 32 mac).
|
|
orgKeyPlain := make([]byte, 64)
|
|
for i := 0; i < 32; i++ {
|
|
orgKeyPlain[i] = byte(i) // encKey
|
|
orgKeyPlain[32+i] = byte(i + 64) // macKey
|
|
}
|
|
|
|
// Encrypt with RSA-OAEP SHA-1 (type 4).
|
|
ciphertext, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, &privateKey.PublicKey, orgKeyPlain, nil)
|
|
if err != nil {
|
|
t.Fatalf("RSA encrypt: %v", err)
|
|
}
|
|
|
|
csStr := "4." + base64.StdEncoding.EncodeToString(ciphertext)
|
|
|
|
// Decrypt.
|
|
orgKey, err := DecryptOrgKey(csStr, privateKey)
|
|
if err != nil {
|
|
t.Fatalf("DecryptOrgKey: %v", err)
|
|
}
|
|
|
|
// Verify encKey.
|
|
for i := 0; i < 32; i++ {
|
|
if orgKey.EncKey[i] != byte(i) {
|
|
t.Fatalf("encKey mismatch at byte %d", i)
|
|
}
|
|
}
|
|
|
|
// Verify macKey.
|
|
for i := 0; i < 32; i++ {
|
|
if orgKey.MacKey[i] != byte(i+64) {
|
|
t.Fatalf("macKey mismatch at byte %d", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecryptOrgKey_Empty(t *testing.T) {
|
|
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
_, err := DecryptOrgKey("", privateKey)
|
|
if err == nil {
|
|
t.Error("expected error for empty org key")
|
|
}
|
|
}
|
|
|
|
func TestDecryptOrgKey_WrongLength(t *testing.T) {
|
|
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
|
|
|
|
// Encrypt a 32-byte value (should be 64).
|
|
plaintext := make([]byte, 32)
|
|
ct, _ := rsa.EncryptOAEP(sha1.New(), rand.Reader, &privateKey.PublicKey, plaintext, nil)
|
|
csStr := "4." + base64.StdEncoding.EncodeToString(ct)
|
|
|
|
_, err := DecryptOrgKey(csStr, privateKey)
|
|
if err == nil {
|
|
t.Error("expected error for wrong org key length")
|
|
}
|
|
if !strings.Contains(err.Error(), "unexpected org key length") {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecryptCipher_WithOrgKey(t *testing.T) {
|
|
// Create two different symmetric keys: personal and org.
|
|
personalKey := SymmetricKey{
|
|
EncKey: make([]byte, 32),
|
|
MacKey: make([]byte, 32),
|
|
}
|
|
orgKey := SymmetricKey{
|
|
EncKey: make([]byte, 32),
|
|
MacKey: make([]byte, 32),
|
|
}
|
|
for i := range orgKey.EncKey {
|
|
orgKey.EncKey[i] = byte(i + 100)
|
|
}
|
|
for i := range orgKey.MacKey {
|
|
orgKey.MacKey[i] = byte(i + 132)
|
|
}
|
|
|
|
// Encrypt a cipher name with the org key.
|
|
plainName := []byte("ORG_SECRET")
|
|
padLen := aes.BlockSize - (len(plainName) % aes.BlockSize)
|
|
padded := make([]byte, len(plainName)+padLen)
|
|
copy(padded, plainName)
|
|
for i := len(plainName); i < len(padded); i++ {
|
|
padded[i] = byte(padLen)
|
|
}
|
|
|
|
iv := make([]byte, 16)
|
|
for i := range iv {
|
|
iv[i] = byte(i + 50)
|
|
}
|
|
|
|
block, _ := aes.NewCipher(orgKey.EncKey)
|
|
ct := make([]byte, len(padded))
|
|
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ct, padded)
|
|
|
|
mac := hmac.New(sha256.New, orgKey.MacKey)
|
|
mac.Write(iv)
|
|
mac.Write(ct)
|
|
macBytes := mac.Sum(nil)
|
|
|
|
encryptedName := fmt.Sprintf("2.%s|%s|%s",
|
|
base64.StdEncoding.EncodeToString(iv),
|
|
base64.StdEncoding.EncodeToString(ct),
|
|
base64.StdEncoding.EncodeToString(macBytes),
|
|
)
|
|
|
|
orgID := "org-123"
|
|
cipher := SyncCipher{
|
|
ID: "cipher-1",
|
|
Type: CipherTypeLogin,
|
|
OrganizationID: &orgID,
|
|
Name: encryptedName,
|
|
}
|
|
|
|
// Should fail with personal key (MAC mismatch).
|
|
_, err := decryptCipher(cipher, personalKey)
|
|
if err == nil {
|
|
t.Error("expected error when decrypting org cipher with personal key")
|
|
}
|
|
|
|
// Should succeed with org key.
|
|
item, err := decryptCipher(cipher, orgKey)
|
|
if err != nil {
|
|
t.Fatalf("decryptCipher with org key failed: %v", err)
|
|
}
|
|
if item.Name != "ORG_SECRET" {
|
|
t.Errorf("got name %q, want %q", item.Name, "ORG_SECRET")
|
|
}
|
|
}
|
|
|
|
func TestDecryptCipher_PersonalItemStillWorks(t *testing.T) {
|
|
// Verify backward compatibility: personal items with no orgID decrypt with personal key.
|
|
key := SymmetricKey{
|
|
EncKey: make([]byte, 32),
|
|
MacKey: make([]byte, 32),
|
|
}
|
|
for i := range key.EncKey {
|
|
key.EncKey[i] = byte(i)
|
|
}
|
|
for i := range key.MacKey {
|
|
key.MacKey[i] = byte(i + 32)
|
|
}
|
|
|
|
plainName := []byte("PERSONAL_SECRET")
|
|
padLen := aes.BlockSize - (len(plainName) % aes.BlockSize)
|
|
padded := make([]byte, len(plainName)+padLen)
|
|
copy(padded, plainName)
|
|
for i := len(plainName); i < len(padded); i++ {
|
|
padded[i] = byte(padLen)
|
|
}
|
|
|
|
iv := make([]byte, 16)
|
|
block, _ := aes.NewCipher(key.EncKey)
|
|
ct := make([]byte, len(padded))
|
|
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ct, padded)
|
|
|
|
mac := hmac.New(sha256.New, key.MacKey)
|
|
mac.Write(iv)
|
|
mac.Write(ct)
|
|
macBytes := mac.Sum(nil)
|
|
|
|
encryptedName := fmt.Sprintf("2.%s|%s|%s",
|
|
base64.StdEncoding.EncodeToString(iv),
|
|
base64.StdEncoding.EncodeToString(ct),
|
|
base64.StdEncoding.EncodeToString(macBytes),
|
|
)
|
|
|
|
// No organizationId — personal item.
|
|
c := SyncCipher{
|
|
ID: "personal-1",
|
|
Type: CipherTypeSecureNote,
|
|
Name: encryptedName,
|
|
}
|
|
|
|
item, err := decryptCipher(c, key)
|
|
if err != nil {
|
|
t.Fatalf("decryptCipher personal item failed: %v", err)
|
|
}
|
|
if item.Name != "PERSONAL_SECRET" {
|
|
t.Errorf("got name %q, want %q", item.Name, "PERSONAL_SECRET")
|
|
}
|
|
}
|