Files
rclone/backend/protondrive/crypto_internal_test.go
T
Lucas WintherandClaude Opus 5.5 cf7577dc1d protondrive: drop Proton-API-Bridge and go-proton-api
The old libraries were only used by the differential crypto tests. Those
go, and TestFrozenFixtures moves into crypto_internal_test.go without its
-update branch, so files the old library wrote stay checked through the
frozen fixtures in testdata/oldlib. go mod tidy removes the two libraries
and the modules only they pulled in; relvacode/iso8601 leaves go.mod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 09:51:43 +01:00

1011 lines
38 KiB
Go

package protondrive
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/ProtonMail/go-crypto/openpgp/s2k"
"github.com/ProtonMail/gopenpgp/v3/armor"
"github.com/ProtonMail/gopenpgp/v3/constants"
"github.com/ProtonMail/gopenpgp/v3/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestComputeKeyPassword(t *testing.T) {
// The vector was produced by go-proton-api v1.0.4:
// proton.Salts{{ID: "key", KeySalt: salt}}.SaltForKey([]byte(password), "key")
const (
password = "correct horse battery staple"
salt = "dGVzdC1rZXktc2FsdC0xNg==" // base64 of "test-key-salt-16"
want = "K97KhNyfE4CFNr9Gu8ONGrNnFPQYivK"
)
got, err := computeKeyPassword([]byte(password), salt)
require.NoError(t, err)
assert.Len(t, got, 31)
assert.Equal(t, want, string(got))
_, err = computeKeyPassword([]byte(password), "not base64!")
assert.Error(t, err)
_, err = computeKeyPassword([]byte(password), "")
assert.Error(t, err)
}
// sealForkPayload encrypts plain as Proton's sign-in page does.
func sealForkPayload(t testing.TB, key []byte, plain, aad string) string {
t.Helper()
block, err := aes.NewCipher(key)
require.NoError(t, err)
gcm, err := cipher.NewGCM(block)
require.NoError(t, err)
nonce := make([]byte, gcm.NonceSize())
_, err = rand.Read(nonce)
require.NoError(t, err)
return base64.StdEncoding.EncodeToString(gcm.Seal(nonce, nonce, []byte(plain), []byte(aad)))
}
func TestDecryptForkPayload(t *testing.T) {
key := make([]byte, 32)
_, err := rand.Read(key)
require.NoError(t, err)
payload := sealForkPayload(t, key, `{"type":"default","keyPassword":"the-key-password"}`, "fork")
got, err := decryptForkPayload(payload, key)
require.NoError(t, err)
assert.Equal(t, "the-key-password", got)
t.Run("FlippedByte", func(t *testing.T) {
blob, err := base64.StdEncoding.DecodeString(payload)
require.NoError(t, err)
blob[len(blob)/2] ^= 1
_, err = decryptForkPayload(base64.StdEncoding.EncodeToString(blob), key)
assert.Error(t, err)
})
t.Run("WrongAAD", func(t *testing.T) {
_, err := decryptForkPayload(sealForkPayload(t, key, `{"keyPassword":"x"}`, "other"), key)
assert.Error(t, err)
})
t.Run("WrongKey", func(t *testing.T) {
other := make([]byte, 32)
_, err := decryptForkPayload(payload, other)
assert.Error(t, err)
})
t.Run("Short", func(t *testing.T) {
_, err := decryptForkPayload(base64.StdEncoding.EncodeToString(make([]byte, 27)), key)
assert.Error(t, err)
})
t.Run("NoKeyPassword", func(t *testing.T) {
_, err := decryptForkPayload(sealForkPayload(t, key, `{"type":"default"}`, "fork"), key)
assert.Error(t, err)
})
}
// newTestKeyRing returns a key ring of a new unlocked key created at now.
func newTestKeyRing(t *testing.T, now time.Time) *crypto.KeyRing {
t.Helper()
key, err := crypto.PGP().KeyGeneration().AddUserId("test", "test@proton.test").GenerationTime(now.Unix()).New().GenerateKey()
require.NoError(t, err)
kr, err := crypto.NewKeyRing(key)
require.NoError(t, err)
return kr
}
// publicOf returns the public keys of kr.
func publicOf(t *testing.T, kr *crypto.KeyRing) *crypto.KeyRing {
t.Helper()
pub, err := publicKeyRing(kr)
require.NoError(t, err)
return pub
}
// readPackets returns the first n packets of the binary message.
func readPackets(t *testing.T, msg []byte, n int) []packet.Packet {
t.Helper()
r := packet.NewReader(bytes.NewReader(msg))
var ps []packet.Packet
for range n {
p, err := r.Next()
require.NoError(t, err)
ps = append(ps, p)
}
return ps
}
// unarmor returns the binary form of the armored message.
func unarmor(t *testing.T, armored string) []byte {
t.Helper()
b, err := armor.Unarmor(armored)
require.NoError(t, err)
return b
}
// requireNonAEAD checks that the armored message is a v3 PKESK followed
// by a SEIPDv1 packet.
func requireNonAEAD(t *testing.T, armored string) {
t.Helper()
ps := readPackets(t, unarmor(t, armored), 2)
pkesk, ok := ps[0].(*packet.EncryptedKey)
require.True(t, ok, "first packet is %T", ps[0])
assert.Equal(t, 3, pkesk.Version)
seipd, ok := ps[1].(*packet.SymmetricallyEncrypted)
require.True(t, ok, "second packet is %T", ps[1])
assert.Equal(t, 1, seipd.Version)
}
// testNodeKeys returns new node keys under parentKR, signed by signerKR.
func testNodeKeys(t *testing.T, parentKR, signerKR *crypto.KeyRing, aead bool, now time.Time) (armoredKey, encPassphrase, passphraseSig string, keys *nodeKeys) {
t.Helper()
armoredKey, encPassphrase, passphraseSig, keys, err := generateNodeKeys(parentKR, signerKR, aead, now)
require.NoError(t, err)
return armoredKey, encPassphrase, passphraseSig, keys
}
// encryptSigned returns plain encrypted to encKR and signed by signKR,
// as another client would write it.
func encryptSigned(t *testing.T, plain []byte, encKR, signKR *crypto.KeyRing, now time.Time) string {
t.Helper()
sk, err := crypto.GenerateSessionKeyAlgo(constants.AES256)
require.NoError(t, err)
msg, err := encryptWithSessionKey(plain, sk, encKR, signKR, false, now)
require.NoError(t, err)
armored, err := armorMessage(msg, sk)
require.NoError(t, err)
return armored
}
func TestNodeKeysRoundTrip(t *testing.T) {
now := time.Now()
parent, signer, other := newTestKeyRing(t, now), newTestKeyRing(t, now), newTestKeyRing(t, now)
for _, aead := range []bool{false, true} {
armoredKey, encPassphrase, passphraseSig, keys := testNodeKeys(t, parent, signer, aead, now)
raw, err := base64.StdEncoding.DecodeString(string(keys.passphrase))
require.NoError(t, err)
assert.Len(t, raw, 32)
got, verifyErr, err := decryptNodeKeys(parent, publicOf(t, signer), armoredKey, encPassphrase, passphraseSig, now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, keys.passphrase, got.passphrase)
assert.Equal(t, keys.passphraseSessionKey.Key, got.passphraseSessionKey.Key)
assert.Equal(t, keys.keyRing.GetKeys()[0].GetFingerprint(), got.keyRing.GetKeys()[0].GetFingerprint())
unlocked, err := got.keyRing.GetKeys()[0].IsUnlocked()
require.NoError(t, err)
assert.True(t, unlocked)
t.Run("WrongVerifyKey", func(t *testing.T) {
got, verifyErr, err := decryptNodeKeys(parent, publicOf(t, other), armoredKey, encPassphrase, passphraseSig, now)
require.NoError(t, err)
assert.Error(t, verifyErr)
require.NotNil(t, got)
assert.Equal(t, keys.passphrase, got.passphrase)
})
t.Run("NoSignature", func(t *testing.T) {
got, verifyErr, err := decryptNodeKeys(parent, publicOf(t, signer), armoredKey, encPassphrase, "", now)
require.NoError(t, err)
assert.Error(t, verifyErr)
require.NotNil(t, got)
})
t.Run("WrongParent", func(t *testing.T) {
_, _, err := decryptNodeKeys(other, publicOf(t, signer), armoredKey, encPassphrase, passphraseSig, now)
assert.Error(t, err)
})
}
}
// secretKeyLocking returns the version, algorithm, S2K usage and S2K
// specifier of a v4 secret key or subkey packet body of an ECC key.
func secretKeyLocking(t *testing.T, body []byte) (version, algo, usage, specifier byte) {
t.Helper()
require.Greater(t, len(body), 7)
version, algo = body[0], body[5]
i := 6
i += 1 + int(body[i]) // curve OID
bits := int(body[i])<<8 | int(body[i+1])
i += 2 + (bits+7)/8 // public point
if packet.PublicKeyAlgorithm(algo) == packet.PubKeyAlgoECDH {
i += 1 + int(body[i]) // KDF parameters
}
usage = body[i]
if usage == 254 || usage == 255 {
specifier = body[i+2]
}
return version, algo, usage, specifier
}
func TestNodeKeyFormat(t *testing.T) {
now := time.Now()
parent, signer := newTestKeyRing(t, now), newTestKeyRing(t, now)
for _, aead := range []bool{false, true} {
armoredKey, encPassphrase, passphraseSig, keys := testNodeKeys(t, parent, signer, aead, now)
// v4 EdDSA (legacy Ed25519) with an ECDH Curve25519 subkey,
// both locked with CFB and an iterated and salted S2K: not
// Argon2, not AEAD, which the official clients cannot all read.
r := packet.NewOpaqueReader(bytes.NewReader(unarmor(t, armoredKey)))
var algos []packet.PublicKeyAlgorithm
for {
op, err := r.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
if op.Tag != 5 && op.Tag != 7 {
continue
}
version, algo, usage, specifier := secretKeyLocking(t, op.Contents)
assert.Equal(t, byte(4), version)
assert.Equal(t, byte(254), usage)
assert.Equal(t, byte(s2k.IteratedSaltedS2K), specifier)
algos = append(algos, packet.PublicKeyAlgorithm(algo))
}
assert.Equal(t, []packet.PublicKeyAlgorithm{packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDH}, algos)
key, err := crypto.NewKeyFromArmored(armoredKey)
require.NoError(t, err)
assert.Equal(t, 4, key.GetVersion())
entity := key.GetEntity()
assert.Contains(t, entity.Identities, "Drive key")
assert.Len(t, entity.Identities, 1)
sig, err := entity.PrimarySelfSignature(now, &packet.Config{})
require.NoError(t, err)
assert.Equal(t, aead, sig.SEIPDv2)
if aead {
require.NotEmpty(t, sig.PreferredCipherSuites)
assert.Equal(t, [2]uint8{uint8(packet.CipherAES256), uint8(packet.AEADModeGCM)}, sig.PreferredCipherSuites[0])
}
requireNonAEAD(t, encPassphrase)
assert.Contains(t, passphraseSig, "BEGIN PGP SIGNATURE")
// The passphrase of a child stays non-AEAD even when its parent
// key accepts AEAD.
_, childPassphrase, _, _ := testNodeKeys(t, keys.keyRing, signer, false, now)
requireNonAEAD(t, childPassphrase)
}
}
func TestReencryptPassphrase(t *testing.T) {
now := time.Now()
parent, newParent, signer, newSigner := newTestKeyRing(t, now), newTestKeyRing(t, now), newTestKeyRing(t, now), newTestKeyRing(t, now)
armoredKey, encPassphrase, _, keys := testNodeKeys(t, parent, signer, true, now)
enc, sig, err := reencryptPassphrase(keys, newParent, newSigner, now)
require.NoError(t, err)
requireNonAEAD(t, enc)
assert.NotEqual(t, encPassphrase, enc)
got, verifyErr, err := decryptNodeKeys(newParent, publicOf(t, newSigner), armoredKey, enc, sig, now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, keys.passphrase, got.passphrase)
assert.Equal(t, keys.passphraseSessionKey.Key, got.passphraseSessionKey.Key)
_, _, err = decryptNodeKeys(parent, publicOf(t, newSigner), armoredKey, enc, sig, now)
assert.Error(t, err)
}
func TestNameRoundTrip(t *testing.T) {
now := time.Now()
parent, signer, other := newTestKeyRing(t, now), newTestKeyRing(t, now), newTestKeyRing(t, now)
const name = "Résumé 2024 ✓.pdf"
enc, err := encryptName(name, nil, parent, signer, now)
require.NoError(t, err)
requireNonAEAD(t, enc)
got, verifyErr, err := decryptName(enc, parent, publicOf(t, signer), now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, name, got)
t.Run("WrongVerifyKey", func(t *testing.T) {
got, verifyErr, err := decryptName(enc, parent, publicOf(t, other), now)
require.NoError(t, err)
assert.Error(t, verifyErr)
assert.Equal(t, name, got)
})
t.Run("WrongParent", func(t *testing.T) {
_, _, err := decryptName(enc, other, publicOf(t, signer), now)
assert.Error(t, err)
})
t.Run("AEADParent", func(t *testing.T) {
_, _, _, keys := testNodeKeys(t, parent, signer, true, now)
enc, err := encryptName(name, nil, keys.keyRing, signer, now)
require.NoError(t, err)
requireNonAEAD(t, enc)
})
t.Run("SameSessionKey", func(t *testing.T) {
sk, err := nameSessionKey(enc, parent)
require.NoError(t, err)
renamed, err := encryptName("renamed", sk, parent, signer, now)
require.NoError(t, err)
sk2, err := nameSessionKey(renamed, parent)
require.NoError(t, err)
assert.Equal(t, sk.Key, sk2.Key)
got, verifyErr, err := decryptName(renamed, parent, publicOf(t, signer), now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, "renamed", got)
})
t.Run("SignedByParentKey", func(t *testing.T) {
// The decryption key is not a verification key: any holder of
// the parent key could sign a name.
enc := encryptSigned(t, []byte(name), parent, parent, now)
got, verifyErr, err := decryptName(enc, parent, publicOf(t, other), now)
require.NoError(t, err)
assert.Error(t, verifyErr)
assert.Equal(t, name, got)
})
t.Run("InvalidUTF8", func(t *testing.T) {
_, _, err := decryptName(encryptSigned(t, []byte{'a', 0xff}, parent, signer, now), parent, publicOf(t, signer), now)
assert.Error(t, err)
})
}
func TestValidateName(t *testing.T) {
for _, test := range []struct {
name string
ok bool
}{
{"a", true},
{"file.txt", true},
{"..a", true},
{"...", true},
{"with space ", true},
{"日本語", true},
{strings.Repeat("a", 255), true},
{strings.Repeat("é", 255), true},
{"", false},
{".", false},
{"..", false},
{"a/b", false},
{"/", false},
{"a\x00b", false},
{strings.Repeat("a", 256), false},
{strings.Repeat("é", 256), false},
// The limit is in UTF-16 code units, as in the SDK: each of
// these takes two.
{strings.Repeat("😀", 127) + "a", true},
{strings.Repeat("😀", 128), false},
// Other clients couldn't read it back: decryptName refuses it.
{"a\xffb", false},
{"\xed\xa0\x80", false}, // a UTF-16 surrogate
} {
err := validateName(test.name)
if test.ok {
assert.NoError(t, err, "%q", test.name)
} else {
assert.Error(t, err, "%q", test.name)
}
}
}
func TestNameHash(t *testing.T) {
key := []byte("a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2V5a2U=")
mac := hmac.New(sha256.New, key)
mac.Write([]byte("report.pdf"))
assert.Equal(t, hex.EncodeToString(mac.Sum(nil)), nameHash("report.pdf", key))
nfc, nfd := "é", "é"
assert.NotEqual(t, nameHash(nfc, key), nameHash(nfd, key))
assert.NotEqual(t, nameHash("A", key), nameHash("a", key))
assert.Regexp(t, "^[0-9a-f]{64}$", nameHash("a", key))
}
func TestHashKeyRoundTrip(t *testing.T) {
now := time.Now()
nodeKR, addrKR, other := newTestKeyRing(t, now), newTestKeyRing(t, now), newTestKeyRing(t, now)
enc, hashKey, err := generateHashKey(nodeKR, now)
require.NoError(t, err)
requireNonAEAD(t, enc)
raw, err := base64.StdEncoding.DecodeString(string(hashKey))
require.NoError(t, err)
assert.Len(t, raw, 32)
got, verifyErr, err := decryptHashKey(enc, nodeKR, nil, now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, hashKey, got)
t.Run("SignedByAddressKey", func(t *testing.T) {
enc := encryptSigned(t, hashKey, nodeKR, addrKR, now)
got, verifyErr, err := decryptHashKey(enc, nodeKR, publicOf(t, addrKR), now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, hashKey, got)
})
t.Run("WrongVerifyKey", func(t *testing.T) {
enc := encryptSigned(t, hashKey, nodeKR, addrKR, now)
got, verifyErr, err := decryptHashKey(enc, nodeKR, publicOf(t, other), now)
require.NoError(t, err)
assert.Error(t, verifyErr)
assert.Equal(t, hashKey, got)
})
t.Run("SignedByOtherNodeKey", func(t *testing.T) {
// Only the node key and the extra keys verify, not every key
// which can decrypt.
bothKR, err := joinKeyRings(nodeKR, other)
require.NoError(t, err)
enc := encryptSigned(t, hashKey, nodeKR, other, now)
got, verifyErr, err := decryptHashKey(enc, bothKR, nil, now)
require.NoError(t, err)
assert.NoError(t, verifyErr, "a key in nodeKR verifies")
assert.Equal(t, hashKey, got)
got, verifyErr, err = decryptHashKey(enc, nodeKR, publicOf(t, addrKR), now)
require.NoError(t, err)
assert.Error(t, verifyErr)
assert.Equal(t, hashKey, got)
})
t.Run("IntendedRecipients", func(t *testing.T) {
// gopenpgp lists the recipients in the signature when it encrypts
// and signs in one go, as go-proton-api's SetNodeHashKey does.
eh, err := crypto.PGP().Encryption().Recipients(nodeKR).SigningKeys(nodeKR).New()
require.NoError(t, err)
msg, err := eh.Encrypt(hashKey)
require.NoError(t, err)
enc, err := msg.Armor()
require.NoError(t, err)
got, verifyErr, err := decryptHashKey(enc, nodeKR, nil, now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, hashKey, got)
// A hidden recipient is not listed, so the signature is not
// intended for it.
eh, err = crypto.PGP().Encryption().Recipients(publicOf(t, other)).HiddenRecipients(nodeKR).SigningKeys(nodeKR).New()
require.NoError(t, err)
msg, err = eh.Encrypt(hashKey)
require.NoError(t, err)
enc, err = msg.Armor()
require.NoError(t, err)
got, verifyErr, err = decryptHashKey(enc, nodeKR, nil, now)
require.NoError(t, err)
assert.Error(t, verifyErr)
assert.Equal(t, hashKey, got)
})
t.Run("WrongNodeKey", func(t *testing.T) {
_, _, err := decryptHashKey(enc, other, nil, now)
assert.Error(t, err)
})
}
func TestContentKeyRoundTrip(t *testing.T) {
now := time.Now()
parent, signer, other := newTestKeyRing(t, now), newTestKeyRing(t, now), newTestKeyRing(t, now)
for _, aead := range []bool{true, false} {
_, _, _, keys := testNodeKeys(t, parent, signer, aead, now)
b64, sig, sk, err := generateContentKey(keys.keyRing, now)
require.NoError(t, err)
raw, err := base64.StdEncoding.DecodeString(b64)
require.NoError(t, err)
ps := readPackets(t, raw, 1)
pkesk, ok := ps[0].(*packet.EncryptedKey)
require.True(t, ok)
// The format follows the node key, as in the SDK: v6 for a
// key which accepts AEAD, v3 otherwise.
if aead {
assert.Equal(t, 6, pkesk.Version)
assert.True(t, sk.IsV6())
} else {
assert.Equal(t, 3, pkesk.Version)
assert.False(t, sk.IsV6())
}
assert.Len(t, sk.Key, 32)
got, verifyErr, err := decryptContentKey(b64, sig, keys.keyRing, nil, now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, sk.Key, got.Key)
assert.Equal(t, aead, got.IsV6())
t.Run("NoSignature", func(t *testing.T) {
got, verifyErr, err := decryptContentKey(b64, "", keys.keyRing, nil, now)
require.NoError(t, err)
assert.NoError(t, verifyErr)
assert.Equal(t, sk.Key, got.Key)
})
t.Run("SignedByAddressKey", func(t *testing.T) {
sig, err := signDetached(sk.Key, signer, now)
require.NoError(t, err)
_, verifyErr, err := decryptContentKey(b64, sig, keys.keyRing, publicOf(t, signer), now)
require.NoError(t, err)
assert.NoError(t, verifyErr)
})
t.Run("WrongVerifyKey", func(t *testing.T) {
sig, err := signDetached(sk.Key, other, now)
require.NoError(t, err)
got, verifyErr, err := decryptContentKey(b64, sig, keys.keyRing, publicOf(t, signer), now)
require.NoError(t, err)
assert.Error(t, verifyErr)
assert.Equal(t, sk.Key, got.Key)
})
t.Run("WrongNodeKey", func(t *testing.T) {
_, _, err := decryptContentKey(b64, sig, other, nil, now)
assert.Error(t, err)
})
}
}
// decryptBlockSignature returns the result of checking the encrypted
// block signature against plain.
func decryptBlockSignature(t *testing.T, encSignature string, plain []byte, nodeKR, verifyKR *crypto.KeyRing) error {
t.Helper()
sig := decryptWith(t, nodeKR, encSignature)
vh, err := crypto.PGP().Verify().VerificationKeys(verifyKR).New()
require.NoError(t, err)
res, err := vh.VerifyDetached(plain, []byte(sig), crypto.Bytes)
require.NoError(t, err)
return res.SignatureError()
}
func TestBlockRoundTrip(t *testing.T) {
now := time.Now()
parent, signer, other := newTestKeyRing(t, now), newTestKeyRing(t, now), newTestKeyRing(t, now)
plain := make([]byte, 100000)
_, err := rand.Read(plain)
require.NoError(t, err)
for _, aead := range []bool{true, false} {
_, _, _, keys := testNodeKeys(t, parent, signer, aead, now)
b64, sig, sk, err := generateContentKey(keys.keyRing, now)
require.NoError(t, err)
enc, encSignature, hash, err := encryptBlock(plain, sk, keys.keyRing, signer, now)
require.NoError(t, err)
assert.Equal(t, sha256.Sum256(enc), hash)
got, err := decryptBlock(enc, sk)
require.NoError(t, err)
assert.Equal(t, plain, got)
// A reader has only the content key packet.
readSK, _, err := decryptContentKey(b64, sig, keys.keyRing, nil, now)
require.NoError(t, err)
got, err = decryptBlock(enc, readSK)
require.NoError(t, err)
assert.Equal(t, plain, got)
assert.NoError(t, decryptBlockSignature(t, encSignature, plain, keys.keyRing, publicOf(t, signer)))
assert.Error(t, decryptBlockSignature(t, encSignature, plain, keys.keyRing, publicOf(t, other)))
t.Run("WrongSessionKey", func(t *testing.T) {
_, _, otherSK, err := generateContentKey(keys.keyRing, now)
require.NoError(t, err)
_, err = decryptBlock(enc, otherSK)
assert.Error(t, err)
})
t.Run("Empty", func(t *testing.T) {
enc, _, _, err := encryptBlock(nil, sk, keys.keyRing, signer, now)
require.NoError(t, err)
got, err := decryptBlock(enc, sk)
require.NoError(t, err)
assert.Empty(t, got)
})
}
}
func TestEncryptBlockFormat(t *testing.T) {
now := time.Now()
parent, signer := newTestKeyRing(t, now), newTestKeyRing(t, now)
_, _, _, keys := testNodeKeys(t, parent, signer, true, now)
b64, _, sk, err := generateContentKey(keys.keyRing, now)
require.NoError(t, err)
raw, err := base64.StdEncoding.DecodeString(b64)
require.NoError(t, err)
pkesk, ok := readPackets(t, raw, 1)[0].(*packet.EncryptedKey)
require.True(t, ok)
assert.Equal(t, 6, pkesk.Version)
enc, encSignature, _, err := encryptBlock([]byte("hello"), sk, keys.keyRing, signer, now)
require.NoError(t, err)
// A block is the data packet alone: the key is in the content key packet.
seipd, ok := readPackets(t, enc, 1)[0].(*packet.SymmetricallyEncrypted)
require.True(t, ok)
assert.Equal(t, 2, seipd.Version)
assert.Equal(t, packet.CipherAES256, seipd.Cipher)
assert.Equal(t, packet.AEADModeGCM, seipd.Mode)
// As in the SDK, the block signature is encrypted to the node key
// with the content session key, so it takes the same format.
ps := readPackets(t, unarmor(t, encSignature), 2)
sigPKESK, ok := ps[0].(*packet.EncryptedKey)
require.True(t, ok)
assert.Equal(t, 6, sigPKESK.Version)
sigSEIPD, ok := ps[1].(*packet.SymmetricallyEncrypted)
require.True(t, ok)
assert.Equal(t, 2, sigSEIPD.Version)
assert.Equal(t, packet.AEADModeGCM, sigSEIPD.Mode)
}
func TestVerifierToken(t *testing.T) {
assert.Equal(t, []byte{0x0f ^ 0x01, 0xf0 ^ 0x02, 0xaa ^ 0x03, 0x55 ^ 0x04}, verifierToken([]byte{0x0f, 0xf0, 0xaa, 0x55}, []byte{0x01, 0x02, 0x03, 0x04}))
assert.Equal(t, []byte{0x0e, 0xf2, 0xa9, 0x51}, verifierToken([]byte{0x0f, 0xf0, 0xaa, 0x55}, []byte{0x01, 0x02, 0x03, 0x04}))
code := []byte{1, 2, 3, 4}
// Code longer than the block: the block is padded with zeros.
assert.Equal(t, []byte{1 ^ 9, 2, 3, 4}, verifierToken(code, []byte{9}))
// Equal lengths.
assert.Equal(t, []byte{0, 0, 0, 0}, verifierToken(code, []byte{1, 2, 3, 4}))
// Code shorter than the block: the rest of the block is ignored.
assert.Equal(t, []byte{0, 0}, verifierToken(code[:2], []byte{1, 2, 3, 4, 5}))
assert.Empty(t, verifierToken(nil, []byte{1}))
}
func TestManifest(t *testing.T) {
now := time.Now()
signer, other := newTestKeyRing(t, now), newTestKeyRing(t, now)
h1, h2 := sha256.Sum256([]byte("one")), sha256.Sum256([]byte("two"))
hashes := [][]byte{h1[:], h2[:]}
sig, err := signManifest(hashes, signer, now)
require.NoError(t, err)
assert.Contains(t, sig, "BEGIN PGP SIGNATURE")
require.NoError(t, verifyManifest(hashes, sig, publicOf(t, signer), now))
// The signature is over the concatenated hashes.
vh, err := crypto.PGP().Verify().VerificationKeys(signer).New()
require.NoError(t, err)
res, err := vh.VerifyDetached(append(h1[:], h2[:]...), []byte(sig), crypto.Armor)
require.NoError(t, err)
assert.NoError(t, res.SignatureError())
assert.Error(t, verifyManifest(hashes, sig, publicOf(t, other), now))
assert.Error(t, verifyManifest([][]byte{h2[:], h1[:]}, sig, publicOf(t, signer), now))
assert.Error(t, verifyManifest(hashes[:1], sig, publicOf(t, signer), now))
assert.Error(t, verifyManifest(hashes, "", publicOf(t, signer), now))
}
func TestXAttrs(t *testing.T) {
now := time.Now()
parent, signer, other := newTestKeyRing(t, now), newTestKeyRing(t, now), newTestKeyRing(t, now)
_, _, _, keys := testNodeKeys(t, parent, signer, true, now)
nodeKR := keys.keyRing
modTime := time.Date(2024, 3, 5, 7, 7, 8, 123456789, time.FixedZone("CET", 3600))
x := &xattrs{ModTime: modTime, Size: 5 << 20, BlockSizes: []int64{4 << 20, 1 << 20}, SHA1: "da39a3ee5e6b4b0d3255bfef95601890afd80709"}
enc, err := encryptXAttrs(x, nodeKR, signer, now)
require.NoError(t, err)
requireNonAEAD(t, enc)
assert.JSONEq(t,
`{"Common":{"ModificationTime":"2024-03-05T06:07:08.123Z","Size":5242880,"BlockSizes":[4194304,1048576],"Digests":{"SHA1":"da39a3ee5e6b4b0d3255bfef95601890afd80709"}}}`,
decryptWith(t, nodeKR, enc))
got, verifyErr, err := decryptXAttrs(enc, nodeKR, publicOf(t, signer), now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.True(t, modTime.Truncate(time.Millisecond).Equal(got.ModTime), "%v", got.ModTime)
assert.Equal(t, x.Size, got.Size)
assert.Equal(t, x.BlockSizes, got.BlockSizes)
assert.Equal(t, x.SHA1, got.SHA1)
assert.Empty(t, got.Extra)
t.Run("WrongVerifyKey", func(t *testing.T) {
got, verifyErr, err := decryptXAttrs(enc, nodeKR, publicOf(t, other), now)
require.NoError(t, err)
assert.Error(t, verifyErr)
assert.Equal(t, x.Size, got.Size)
})
t.Run("SignedByNodeKey", func(t *testing.T) {
enc, err := encryptXAttrs(x, nodeKR, nodeKR, now)
require.NoError(t, err)
got, verifyErr, err := decryptXAttrs(enc, nodeKR, publicOf(t, other), now)
require.NoError(t, err)
assert.Error(t, verifyErr)
assert.Equal(t, x.Size, got.Size)
})
t.Run("WrongNodeKey", func(t *testing.T) {
_, _, err := decryptXAttrs(enc, other, publicOf(t, signer), now)
assert.Error(t, err)
})
t.Run("EmptyFile", func(t *testing.T) {
enc, err := encryptXAttrs(&xattrs{SHA1: "da39a3ee5e6b4b0d3255bfef95601890afd80709"}, nodeKR, signer, now)
require.NoError(t, err)
assert.JSONEq(t, `{"Common":{"Size":0,"BlockSizes":[],"Digests":{"SHA1":"da39a3ee5e6b4b0d3255bfef95601890afd80709"}}}`, decryptWith(t, nodeKR, enc))
})
t.Run("Folder", func(t *testing.T) {
enc, err := encryptXAttrs(&xattrs{ModTime: modTime}, nodeKR, signer, now)
require.NoError(t, err)
assert.JSONEq(t, `{"Common":{"ModificationTime":"2024-03-05T06:07:08.123Z"}}`, decryptWith(t, nodeKR, enc))
})
t.Run("Extra", func(t *testing.T) {
media := `{"Width":1920,"Height":1080,"Duration":12.5}`
in := `{"Media":` + media + `,"Common":{"ModificationTime":"2021-09-16T07:40:54+0000","Size":3,"BlockSizes":[3],"Digests":{"SHA1":"abc"}}}`
got, verifyErr, err := decryptXAttrs(encryptSigned(t, []byte(in), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, media, string(got.Extra["Media"]))
assert.Len(t, got.Extra, 1)
assert.True(t, time.Date(2021, 9, 16, 7, 40, 54, 0, time.UTC).Equal(got.ModTime))
assert.Equal(t, int64(3), got.Size)
enc, err := encryptXAttrs(got, nodeKR, signer, now)
require.NoError(t, err)
again, _, err := decryptXAttrs(enc, nodeKR, publicOf(t, signer), now)
require.NoError(t, err)
assert.Equal(t, media, string(again.Extra["Media"]))
assert.Equal(t, got, again)
})
t.Run("ExtraCommon", func(t *testing.T) {
_, err := encryptXAttrs(&xattrs{Extra: map[string]json.RawMessage{"Common": json.RawMessage(`{}`)}}, nodeKR, signer, now)
assert.Error(t, err)
})
t.Run("MissingCommon", func(t *testing.T) {
got, verifyErr, err := decryptXAttrs(encryptSigned(t, []byte(`{}`), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, &xattrs{NoSize: true}, got)
})
t.Run("Empty", func(t *testing.T) {
got, verifyErr, err := decryptXAttrs("", nodeKR, publicOf(t, signer), now)
require.NoError(t, err)
require.NoError(t, verifyErr)
assert.Equal(t, &xattrs{}, got)
})
t.Run("BadValues", func(t *testing.T) {
// As in the SDK, malformed members are ignored, not errors.
in := `{"Common":{"ModificationTime":"yesterday","Size":"big","BlockSizes":[1,0],"Digests":{"SHA1":7}}}`
got, _, err := decryptXAttrs(encryptSigned(t, []byte(in), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
require.NoError(t, err)
assert.Equal(t, &xattrs{NoSize: true}, got)
got, _, err = decryptXAttrs(encryptSigned(t, []byte(`not json`), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
require.NoError(t, err)
assert.Equal(t, &xattrs{NoSize: true}, got)
})
t.Run("NegativeSize", func(t *testing.T) {
// The size is taken from the block sizes, if they are usable.
for in, want := range map[string]*xattrs{
`{"Common":{"Size":-5,"BlockSizes":[5],"Digests":{"SHA1":"abc"}}}`: {Size: 5, BlockSizes: []int64{5}, SHA1: "abc"},
`{"Common":{"BlockSizes":[4194304,3],"Digests":{"SHA1":"abc"}}}`: {Size: 4194307, BlockSizes: []int64{4194304, 3}, SHA1: "abc"},
`{"Common":{"Size":"big","BlockSizes":[4194304,3],"Digests":{"SHA1":"abc"}}}`: {Size: 4194307, BlockSizes: []int64{4194304, 3}, SHA1: "abc"},
`{"Common":{"Size":-5,"Digests":{"SHA1":"abc"}}}`: {SHA1: "abc", NoSize: true},
`{"Common":{"Size":-5,"BlockSizes":[1152921504606846976],"Digests":{"SHA1":"abc"}}}`: {BlockSizes: []int64{1 << 60}, SHA1: "abc", NoSize: true},
} {
got, _, err := decryptXAttrs(encryptSigned(t, []byte(in), nodeKR, signer, now), nodeKR, publicOf(t, signer), now)
require.NoError(t, err)
assert.Equal(t, want, got, in)
}
})
}
// TestDecryptedSizeBounded checks a compressed message which expands
// past what its kind of metadata needs is refused rather than
// decompressed into memory: anyone holding the public key, the server
// included, can make one.
func TestDecryptedSizeBounded(t *testing.T) {
now := time.Now()
parent, signer := newTestKeyRing(t, now), newTestKeyRing(t, now)
signerPub := publicOf(t, signer)
compressed := func(plain []byte) string {
enc, err := encryptArmored(plain, nil, parent, signer, true, now)
require.NoError(t, err)
require.Less(t, len(enc), len(plain)/10, "compressed")
return enc
}
padded := func(prefix string, n int) []byte {
return append([]byte(prefix), bytes.Repeat([]byte(" "), n-len(prefix))...)
}
// The limit counts the packets inside the compressed one: the data
// with its header, and the signature.
const overhead = 1 << 10
_, _, err := decryptName(compressed(padded("name", maxSmallMessageSize-overhead)), parent, signerPub, now)
require.NoError(t, err, "within the limit")
_, _, err = decryptName(compressed(padded("name", maxSmallMessageSize+1)), parent, signerPub, now)
assert.Error(t, err)
_, _, err = decryptHashKey(compressed(padded("key", maxSmallMessageSize+1)), parent, signerPub, now)
assert.Error(t, err)
_, _, err = decryptNodeKeys(parent, signerPub, "", compressed(padded("passphrase", maxSmallMessageSize+1)), "", now)
assert.ErrorContains(t, err, "passphrase")
x, _, err := decryptXAttrs(compressed(padded(`{"Common":{"Size":7}}`, maxXAttrsSize-overhead)), parent, signerPub, now)
require.NoError(t, err, "within the limit")
assert.Equal(t, int64(7), x.Size)
_, _, err = decryptXAttrs(compressed(padded(`{"Common":{"Size":7}}`, maxXAttrsSize+1)), parent, signerPub, now)
assert.Error(t, err)
}
func TestServerTimeUsedForSignatures(t *testing.T) {
// The server clock is three hours ahead of the local one: keys and
// signatures made at server time are in the local future.
now := time.Now().Add(3 * time.Hour)
parent, signer := newTestKeyRing(t, now), newTestKeyRing(t, now)
signerPub := publicOf(t, signer)
armoredKey, encPassphrase, passphraseSig, keys := testNodeKeys(t, parent, signer, true, now)
_, verifyErr, err := decryptNodeKeys(parent, signerPub, armoredKey, encPassphrase, passphraseSig, now)
require.NoError(t, err)
assert.NoError(t, verifyErr)
// The same values checked at local time do not verify.
_, verifyErr, err = decryptNodeKeys(parent, signerPub, armoredKey, encPassphrase, passphraseSig, time.Now())
require.NoError(t, err)
assert.Error(t, verifyErr)
encName, err := encryptName("name", nil, parent, signer, now)
require.NoError(t, err)
_, verifyErr, err = decryptName(encName, parent, signerPub, now)
require.NoError(t, err)
assert.NoError(t, verifyErr)
encHashKey, _, err := generateHashKey(keys.keyRing, now)
require.NoError(t, err)
_, verifyErr, err = decryptHashKey(encHashKey, keys.keyRing, nil, now)
require.NoError(t, err)
assert.NoError(t, verifyErr)
b64, sig, _, err := generateContentKey(keys.keyRing, now)
require.NoError(t, err)
_, verifyErr, err = decryptContentKey(b64, sig, keys.keyRing, nil, now)
require.NoError(t, err)
assert.NoError(t, verifyErr)
manifestSig, err := signManifest([][]byte{{1}}, signer, now)
require.NoError(t, err)
assert.NoError(t, verifyManifest([][]byte{{1}}, manifestSig, signerPub, now))
encXAttrs, err := encryptXAttrs(&xattrs{ModTime: now}, keys.keyRing, signer, now)
require.NoError(t, err)
_, verifyErr, err = decryptXAttrs(encXAttrs, keys.keyRing, signerPub, now)
require.NoError(t, err)
assert.NoError(t, verifyErr)
}
// oldLibDir holds nodes the old library (go-proton-api v1.0.4 and
// Proton-API-Bridge v1.0.5) wrote, so that they stay readable.
const oldLibDir = "testdata/oldlib"
// oldLibFixture is a node written by the old library with the test-only
// keys it was written with and the plaintexts it encrypted.
type oldLibFixture struct {
Comment string
Type string // "folder" or "file"
ParentKey string // armored parent node key, locked with ParentKeyPassphrase
ParentKeyPassphrase string
AddressKey string // armored address key which signs, locked with AddressKeyPassphrase
AddressKeyPassphrase string
NodeKey string
NodePassphrase string // encrypted to the parent key
NodePassphraseSignature string // by the address key
NodePassphrasePlain string
Name string // encrypted to the parent key, signed by the address key
NamePlain string
// A folder's hash key, encrypted to and signed by its node key, and
// the hash of ChildName with it.
NodeHashKey string `json:",omitempty"`
HashKeyPlain string `json:",omitempty"` // base64 of the raw bytes
ChildName string `json:",omitempty"`
ChildNameHash string `json:",omitempty"`
// A file's content key, signed by its node key, and one block of
// content with its signature by the address key, encrypted to the
// node key, and the manifest signature by the address key.
ContentKeyPacket string `json:",omitempty"` // base64, encrypted to the node key
ContentKeyPacketSignature string `json:",omitempty"`
ContentSessionKey string `json:",omitempty"` // hex
Block string `json:",omitempty"` // base64
BlockPlain string `json:",omitempty"` // base64
BlockEncSignature string `json:",omitempty"`
BlockHash string `json:",omitempty"` // base64 SHA-256 of Block
ManifestSignature string `json:",omitempty"`
XAttr string `json:",omitempty"` // encrypted to the node key, signed by the address key
XAttrModificationTime string `json:",omitempty"` // as the old library wrote it
XAttrSize int64 `json:",omitempty"`
XAttrBlockSizes []int64 `json:",omitempty"`
XAttrSHA1 string `json:",omitempty"`
}
// loadOldLibFixture returns the fixture testdata/oldlib/name.json.
func loadOldLibFixture(t *testing.T, name string) oldLibFixture {
t.Helper()
data, err := os.ReadFile(filepath.Join(oldLibDir, name+".json"))
require.NoError(t, err)
var f oldLibFixture
require.NoError(t, json.Unmarshal(data, &f))
return f
}
// unlockedKeyRing returns a key ring of the armored key unlocked with
// passphrase.
func unlockedKeyRing(t *testing.T, armored, passphrase string) *crypto.KeyRing {
t.Helper()
key, err := unlockKey(armored, []byte(passphrase))
require.NoError(t, err)
kr, err := crypto.NewKeyRing(key)
require.NoError(t, err)
return kr
}
// checkOldLibFixture reads f with crypto.go and checks every value
// against its plaintext and every signature against its signer.
func checkOldLibFixture(t *testing.T, f oldLibFixture, now time.Time) {
t.Helper()
parentKR := unlockedKeyRing(t, f.ParentKey, f.ParentKeyPassphrase)
addrPub := publicOf(t, unlockedKeyRing(t, f.AddressKey, f.AddressKeyPassphrase))
keys, verifyErr, err := decryptNodeKeys(parentKR, addrPub, f.NodeKey, f.NodePassphrase, f.NodePassphraseSignature, now)
require.NoError(t, err)
assert.NoError(t, verifyErr, "node passphrase signature")
assert.Equal(t, f.NodePassphrasePlain, string(keys.passphrase))
nodeKR := keys.keyRing
name, verifyErr, err := decryptName(f.Name, parentKR, addrPub, now)
require.NoError(t, err)
assert.NoError(t, verifyErr, "name signature")
assert.Equal(t, f.NamePlain, name)
switch f.Type {
case "folder":
hashKey, verifyErr, err := decryptHashKey(f.NodeHashKey, nodeKR, nil, now)
require.NoError(t, err)
assert.NoError(t, verifyErr, "hash key signature")
assert.Equal(t, f.HashKeyPlain, base64.StdEncoding.EncodeToString(hashKey))
assert.Equal(t, f.ChildNameHash, nameHash(f.ChildName, hashKey))
case "file":
sk, verifyErr, err := decryptContentKey(f.ContentKeyPacket, f.ContentKeyPacketSignature, nodeKR, nil, now)
require.NoError(t, err)
assert.NoError(t, verifyErr, "content key signature")
assert.Equal(t, f.ContentSessionKey, hex.EncodeToString(sk.Key))
block, err := base64.StdEncoding.DecodeString(f.Block)
require.NoError(t, err)
plain, err := decryptBlock(block, sk)
require.NoError(t, err)
assert.Equal(t, f.BlockPlain, base64.StdEncoding.EncodeToString(plain))
hash := sha256.Sum256(block)
assert.Equal(t, f.BlockHash, base64.StdEncoding.EncodeToString(hash[:]))
assert.NoError(t, decryptBlockSignature(t, f.BlockEncSignature, plain, nodeKR, addrPub), "block signature")
assert.NoError(t, verifyManifest([][]byte{hash[:]}, f.ManifestSignature, addrPub, now))
x, verifyErr, err := decryptXAttrs(f.XAttr, nodeKR, addrPub, now)
require.NoError(t, err)
assert.NoError(t, verifyErr, "extended attributes signature")
modTime, err := time.Parse("2006-01-02T15:04:05-0700", f.XAttrModificationTime)
require.NoError(t, err)
assert.True(t, modTime.Equal(x.ModTime), "%v != %v", modTime, x.ModTime)
assert.Equal(t, f.XAttrSize, x.Size)
assert.Equal(t, f.XAttrBlockSizes, x.BlockSizes)
assert.Equal(t, f.XAttrSHA1, x.SHA1)
default:
t.Fatalf("unknown fixture type %q", f.Type)
}
}
func TestFrozenFixtures(t *testing.T) {
entries, err := os.ReadDir(oldLibDir)
require.NoError(t, err)
var n int
for _, e := range entries {
name, ok := strings.CutSuffix(e.Name(), ".json")
if !ok {
continue
}
n++
t.Run(name, func(t *testing.T) {
checkOldLibFixture(t, loadOldLibFixture(t, name), time.Time{})
})
}
assert.Equal(t, 2, n, "fixtures in %s", oldLibDir)
}