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>
1011 lines
38 KiB
Go
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)
|
|
}
|