package bitwarden import ( "bytes" "encoding/base64" "strings" "testing" ) // Vectors below are the published test vectors from the Bitwarden SDK // (bitwarden/sdk-internal, crates bitwarden-crypto): they pin this // stdlib-only implementation to the exact wire construction the official // clients use. func TestDeriveShareableKeyVectors(t *testing.T) { cases := []struct { name string secret string // raw bytes salt string // name argument (salt becomes "bitwarden-"+name) info string want string // base64 of the 64-byte key }{ { name: "no info", secret: "&/$%F1a895g67HlX", salt: "test_key", info: "", want: "4PV6+PcmF2w7YHRatvyMcVQtI7zvCyssv/wFWmzjiH6Iv9altjmDkuBD1aagLVaLezbthbSe+ktR+U6qswxNnQ==", }, { name: "with info", secret: "67t9b5g67$%Dh89n", salt: "test_key", info: "test", want: "F9jVQmrACGx9VUPjuzfMYDjr726JtL300Y3Yg+VYUnVQtQ1s8oImJ5xtp1KALC9h2nav04++1LDW4iFD+infng==", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { key, err := deriveShareableKey([]byte(tc.secret), tc.salt, tc.info) if err != nil { t.Fatalf("derive: %v", err) } got := base64.StdEncoding.EncodeToString(append(key.EncKey, key.MACKey...)) if got != tc.want { t.Fatalf("derived key mismatch:\n got %s\nwant %s", got, tc.want) } }) } } func TestParseAccessTokenVector(t *testing.T) { // Published vector: the credential format and the exact derived // payload-unwrapping key. const cred = "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==" id, secret, key, err := parseAccessToken(cred) if err != nil { t.Fatalf("parse: %v", err) } if id != "ec2c1d46-6a4b-4751-a310-af9601317f2d" { t.Fatalf("client id: %s", id) } if secret != "C2IgxjjLF7qSshsbwe8JGcbM075YXw" { t.Fatalf("client secret: %s", secret) } got := base64.StdEncoding.EncodeToString(append(key.EncKey, key.MACKey...)) const want = "H9/oIRLtL9nGCQOVDjSMoEbJsjWXSOCb3qeyDt6ckzS3FhyboEDWyTP/CQfbIszNmAVg2ExFganG1FVFGXO/Jg==" if got != want { t.Fatalf("derived key mismatch:\n got %s\nwant %s", got, want) } } func TestParseAccessTokenMalformed(t *testing.T) { bad := []string{ "", "nonsense", "1.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==", // wrong version "ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==", // no version part "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw", // no key part "0..C2IgxjjLF7qSshsbwe8JGcbM075YXw:X8vbvA0bduihIDe/qrzIQQ==", // empty id "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:c2hvcnQ=", // key not 16 bytes "0.ec2c1d46-6a4b-4751-a310-af9601317f2d.C2IgxjjLF7qSshsbwe8JGcbM075YXw:!!!not-base64!!!", // key not base64 } for _, s := range bad { _, _, _, err := parseAccessToken(s) if err == nil { t.Fatalf("expected error for %q", s) } if s != "" && strings.Contains(err.Error(), s) { t.Fatalf("error echoes credential: %v", err) } } } func TestLegacyAesCbcHmacVector(t *testing.T) { // Published SDK vector for the type-2 EncString construction // (AES-256-CBC + HMAC-SHA256 over iv||ciphertext). key := make([]byte, 64) for i := range key { key[i] = byte(i) } sk, err := NewSymmetricKey(key) if err != nil { t.Fatalf("key: %v", err) } iv := []byte{216, 218, 36, 0, 196, 186, 150, 85, 49, 147, 110, 168, 185, 227, 42, 172} ct := []byte{ 234, 77, 16, 15, 189, 82, 36, 188, 182, 88, 64, 67, 145, 94, 30, 178, 36, 235, 130, 67, 255, 207, 183, 168, 73, 231, 82, 122, 193, 139, 25, 129, } mac := []byte{ 60, 78, 44, 111, 72, 233, 3, 6, 86, 250, 217, 242, 62, 229, 184, 221, 231, 150, 189, 44, 99, 189, 220, 55, 196, 194, 101, 60, 102, 195, 149, 130, } const plaintext = "Bitwarden SDK test vector" e := EncString{Type: '2', IV: iv, CT: ct, MAC: mac} pt, err := e.Decrypt(sk) if err != nil { t.Fatalf("decrypt: %v", err) } if string(pt) != plaintext { t.Fatalf("plaintext mismatch: %q", pt) } } func TestEncryptDecryptRoundTrip(t *testing.T) { raw := make([]byte, 64) for i := range raw { raw[i] = byte(i * 7) } sk, err := NewSymmetricKey(raw) if err != nil { t.Fatalf("key: %v", err) } for _, pt := range []string{"", "a", "exactly-16-chars", "the quick brown fox jumps over the lazy dog 1234567890"} { e, err := Encrypt(sk, []byte(pt)) if err != nil { t.Fatalf("encrypt %q: %v", pt, err) } got, err := e.Decrypt(sk) if err != nil { t.Fatalf("decrypt %q: %v", pt, err) } if !bytes.Equal(got, []byte(pt)) { t.Fatalf("round trip mismatch: %q != %q", got, pt) } } } func TestTamperDetection(t *testing.T) { raw := make([]byte, 64) for i := range raw { raw[i] = byte(i) } sk, _ := NewSymmetricKey(raw) e, err := Encrypt(sk, []byte("sensitive value")) if err != nil { t.Fatalf("encrypt: %v", err) } tampered := e tampered.CT[0] ^= 0xFF if _, err := tampered.Decrypt(sk); err == nil { t.Fatal("tampered ciphertext decrypted: MAC not enforced") } badMAC := e badMAC.MAC[0] ^= 0xFF if _, err := badMAC.Decrypt(sk); err == nil { t.Fatal("tampered MAC accepted") } } func TestParseEncStringShapes(t *testing.T) { raw := make([]byte, 64) sk, _ := NewSymmetricKey(raw) e, _ := Encrypt(sk, []byte("x")) if _, ok := ParseEncString(e.String()); !ok { t.Fatal("valid encstring not recognized") } notEnc := []string{"", "plaintext", "2.short", "2.aGVsbG8.aGVsbG8", "9.aGVsbG8.x.y.z", "2!!!"} for _, s := range notEnc { if _, ok := ParseEncString(s); ok { t.Fatalf("non-encstring accepted: %q", s) } } } func TestNewSymmetricKeyLengths(t *testing.T) { if _, err := NewSymmetricKey(make([]byte, 32)); err != nil { t.Fatalf("32-byte key rejected: %v", err) } if _, err := NewSymmetricKey(make([]byte, 64)); err != nil { t.Fatalf("64-byte key rejected: %v", err) } if _, err := NewSymmetricKey(make([]byte, 16)); err == nil { t.Fatal("16-byte key accepted") } } func TestParseJWTClaims(t *testing.T) { // header.payload.sig with payload {"sub":"acc","organization":"org-id","exp":1893456000} payload := base64.RawURLEncoding.EncodeToString([]byte(`{"sub":"acc","organization":"3fb1c0de-0000-4000-8000-000000000000","exp":1893456000}`)) tok := "eyJhbGciOiJIUzI1NiJ9." + payload + ".c2ln" c, ok := parseJWTClaims(tok) if !ok { t.Fatal("claims not parsed") } if c.Organization != "3fb1c0de-0000-4000-8000-000000000000" || c.Exp != 1893456000 { t.Fatalf("claims wrong: %+v", c) } if _, ok := parseJWTClaims("not-a-jwt"); ok { t.Fatal("non-jwt accepted") } }