- 27 new tests covering all PGP service methods - Fixes: armored public key in NewPGPService/GenerateKeyPair/EncryptBody, IsLocked check in getUnlockedKeyRing, aes256 cipher token in EncryptAttachment Co-Authored-By: Paperclip <noreply@paperclip.ing>
558 lines
14 KiB
Go
558 lines
14 KiB
Go
package mail
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
|
)
|
|
|
|
// testKey generates a fresh PGP key pair for tests.
|
|
func testKey(t *testing.T) (privateKey, publicKey, passphrase string) {
|
|
t.Helper()
|
|
svc := &PGPService{}
|
|
privateKey, publicKey, err := svc.GenerateKeyPair("test@example.com", "test-passphrase")
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeyPair: %v", err)
|
|
}
|
|
return privateKey, publicKey, "test-passphrase"
|
|
}
|
|
|
|
// newTestService creates a PGPService from a freshly generated key.
|
|
func newTestService(t *testing.T) (*PGPService, string, string) {
|
|
t.Helper()
|
|
priv, pub, pass := testKey(t)
|
|
svc, err := NewPGPService(priv)
|
|
if err != nil {
|
|
t.Fatalf("NewPGPService: %v", err)
|
|
}
|
|
return svc, pub, pass
|
|
}
|
|
|
|
// newLockedTestService creates a PGPService with a properly passphrase-locked key.
|
|
// crypto.GenerateKey creates unlocked keys, so we explicitly lock the key after generation.
|
|
func newLockedTestService(t *testing.T) (*PGPService, string, string) {
|
|
t.Helper()
|
|
key, err := crypto.GenerateKey("test@example.com", "", "RSA", 4096)
|
|
if err != nil {
|
|
t.Fatalf("GenerateKey: %v", err)
|
|
}
|
|
lockedKey, err := key.Lock([]byte("test-passphrase"))
|
|
if err != nil {
|
|
t.Fatalf("Lock: %v", err)
|
|
}
|
|
privArmor, err := lockedKey.Armor()
|
|
if err != nil {
|
|
t.Fatalf("Armor: %v", err)
|
|
}
|
|
pubKeyBytes, err := lockedKey.GetPublicKey()
|
|
if err != nil {
|
|
t.Fatalf("GetPublicKey: %v", err)
|
|
}
|
|
pubKey, err := crypto.NewKey(pubKeyBytes)
|
|
if err != nil {
|
|
t.Fatalf("NewKey: %v", err)
|
|
}
|
|
pubArmor, err := pubKey.Armor()
|
|
if err != nil {
|
|
t.Fatalf("Armor public key: %v", err)
|
|
}
|
|
svc, err := NewPGPService(privArmor)
|
|
if err != nil {
|
|
t.Fatalf("NewPGPService: %v", err)
|
|
}
|
|
return svc, pubArmor, "test-passphrase"
|
|
}
|
|
|
|
// ---------- NewPGPService ----------
|
|
|
|
func TestNewPGPService_ValidKey(t *testing.T) {
|
|
priv, _, _ := testKey(t)
|
|
svc, err := NewPGPService(priv)
|
|
if err != nil {
|
|
t.Fatalf("NewPGPService: %v", err)
|
|
}
|
|
if svc.keyRing == nil {
|
|
t.Fatal("keyRing is nil")
|
|
}
|
|
if svc.keyRing.PrivateKey == nil {
|
|
t.Fatal("PrivateKey is nil")
|
|
}
|
|
if len(svc.keyRing.PublicKey) == 0 {
|
|
t.Fatal("PublicKey is empty")
|
|
}
|
|
}
|
|
|
|
func TestNewPGPService_EmptyKey(t *testing.T) {
|
|
_, err := NewPGPService("")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty key")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to parse private key") {
|
|
t.Errorf("unexpected error message: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestNewPGPService_InvalidKey(t *testing.T) {
|
|
_, err := NewPGPService("NOT A PGP KEY")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid key")
|
|
}
|
|
}
|
|
|
|
// ---------- GenerateKeyPair ----------
|
|
|
|
func TestGenerateKeyPair_Success(t *testing.T) {
|
|
svc := &PGPService{}
|
|
priv, pub, err := svc.GenerateKeyPair("alice@example.com", "pass123")
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeyPair: %v", err)
|
|
}
|
|
if !strings.Contains(priv, "BEGIN PGP PRIVATE KEY BLOCK") {
|
|
t.Error("private key missing armored header")
|
|
}
|
|
if !strings.Contains(pub, "BEGIN PGP PUBLIC KEY BLOCK") {
|
|
t.Error("public key missing armored header")
|
|
}
|
|
}
|
|
|
|
func TestGenerateKeyPair_EmptyEmail(t *testing.T) {
|
|
svc := &PGPService{}
|
|
priv, pub, err := svc.GenerateKeyPair("", "pass123")
|
|
if err != nil {
|
|
t.Fatalf("GenerateKeyPair with empty email: %v", err)
|
|
}
|
|
if !strings.Contains(priv, "BEGIN PGP PRIVATE KEY BLOCK") {
|
|
t.Error("private key missing armored header")
|
|
}
|
|
if !strings.Contains(pub, "BEGIN PGP PUBLIC KEY BLOCK") {
|
|
t.Error("public key missing armored header")
|
|
}
|
|
}
|
|
|
|
// ---------- GetFingerprint ----------
|
|
|
|
func TestGetFingerprint_Success(t *testing.T) {
|
|
svc, _, _ := newTestService(t)
|
|
fp, err := svc.GetFingerprint()
|
|
if err != nil {
|
|
t.Fatalf("GetFingerprint: %v", err)
|
|
}
|
|
if len(fp) != 40 {
|
|
t.Errorf("expected 40-char fingerprint, got %d", len(fp))
|
|
}
|
|
}
|
|
|
|
func TestGetFingerprint_NoKeyRing(t *testing.T) {
|
|
svc := &PGPService{}
|
|
_, err := svc.GetFingerprint()
|
|
if err == nil {
|
|
t.Fatal("expected error for nil keyRing")
|
|
}
|
|
if !strings.Contains(err.Error(), "no key ring available") {
|
|
t.Errorf("unexpected error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// ---------- ZeroPrivateKeyData ----------
|
|
|
|
func TestZeroPrivateKeyData_Success(t *testing.T) {
|
|
svc, _, _ := newTestService(t)
|
|
initialLen := len(svc.keyRing.PrivateKeyData)
|
|
svc.ZeroPrivateKeyData()
|
|
for i, b := range svc.keyRing.PrivateKeyData {
|
|
if b != 0 {
|
|
t.Errorf("byte %d not zeroed: %d", i, b)
|
|
}
|
|
}
|
|
if len(svc.keyRing.PrivateKeyData) != initialLen {
|
|
t.Error("PrivateKeyData length changed after zeroing")
|
|
}
|
|
}
|
|
|
|
func TestZeroPrivateKeyData_NilKeyRing(t *testing.T) {
|
|
svc := &PGPService{}
|
|
svc.ZeroPrivateKeyData() // should not panic
|
|
}
|
|
|
|
// ---------- Encrypt / Decrypt roundtrip ----------
|
|
|
|
func TestEncryptDecrypt_Roundtrip(t *testing.T) {
|
|
svc, pubArmor, pass := newTestService(t)
|
|
|
|
recipientKey, err := crypto.NewKeyFromArmored(pubArmor)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient key: %v", err)
|
|
}
|
|
|
|
plaintext := "Hello, encrypted world!"
|
|
encrypted, err := svc.Encrypt(plaintext, recipientKey)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt: %v", err)
|
|
}
|
|
if !strings.Contains(encrypted, "BEGIN PGP MESSAGE") {
|
|
t.Error("encrypted output missing PGP message header")
|
|
}
|
|
|
|
decrypted, err := svc.Decrypt(encrypted, pass)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt: %v", err)
|
|
}
|
|
if decrypted != plaintext {
|
|
t.Errorf("roundtrip mismatch: got %q, want %q", decrypted, plaintext)
|
|
}
|
|
}
|
|
|
|
func TestEncryptDecrypt_LargePayload(t *testing.T) {
|
|
svc, pubArmor, pass := newTestService(t)
|
|
|
|
recipientKey, err := crypto.NewKeyFromArmored(pubArmor)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient key: %v", err)
|
|
}
|
|
|
|
payload := strings.Repeat("ABCDEFGHijklmnop12345678\n", 100)
|
|
encrypted, err := svc.Encrypt(payload, recipientKey)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt: %v", err)
|
|
}
|
|
|
|
decrypted, err := svc.Decrypt(encrypted, pass)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt: %v", err)
|
|
}
|
|
if decrypted != payload {
|
|
t.Errorf("large payload roundtrip mismatch")
|
|
}
|
|
}
|
|
|
|
func TestDecrypt_InvalidMessage(t *testing.T) {
|
|
svc, _, _ := newTestService(t)
|
|
_, err := svc.Decrypt("NOT A PGP MESSAGE", "test-passphrase")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid message")
|
|
}
|
|
}
|
|
|
|
func TestDecrypt_WrongPassphrase(t *testing.T) {
|
|
svc, pubArmor, _ := newLockedTestService(t)
|
|
|
|
recipientKey, err := crypto.NewKeyFromArmored(pubArmor)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient key: %v", err)
|
|
}
|
|
|
|
encrypted, err := svc.Encrypt("secret", recipientKey)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt: %v", err)
|
|
}
|
|
|
|
_, err = svc.Decrypt(encrypted, "wrong-passphrase")
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong passphrase")
|
|
}
|
|
}
|
|
|
|
// ---------- EncryptBody ----------
|
|
|
|
func TestEncryptBody_Success(t *testing.T) {
|
|
svc, _, pass := newTestService(t)
|
|
|
|
plaintext := "Body content to encrypt"
|
|
encrypted, err := svc.EncryptBody(plaintext, pass)
|
|
if err != nil {
|
|
t.Fatalf("EncryptBody: %v", err)
|
|
}
|
|
if !strings.Contains(encrypted, "BEGIN PGP MESSAGE") {
|
|
t.Error("encrypted body missing PGP message header")
|
|
}
|
|
|
|
decrypted, err := svc.Decrypt(encrypted, pass)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt: %v", err)
|
|
}
|
|
if decrypted != plaintext {
|
|
t.Errorf("EncryptBody roundtrip mismatch: got %q, want %q", decrypted, plaintext)
|
|
}
|
|
}
|
|
|
|
func TestEncryptBody_WrongPassphrase(t *testing.T) {
|
|
svc, _, _ := newLockedTestService(t)
|
|
|
|
_, err := svc.EncryptBody("content", "wrong-passphrase")
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong passphrase")
|
|
}
|
|
}
|
|
|
|
// ---------- EncryptAndSign ----------
|
|
|
|
func TestEncryptAndSign_Success(t *testing.T) {
|
|
svc, pubArmor, pass := newTestService(t)
|
|
|
|
recipientKey, err := crypto.NewKeyFromArmored(pubArmor)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient key: %v", err)
|
|
}
|
|
|
|
plaintext := "Signed and encrypted content"
|
|
encrypted, err := svc.EncryptAndSign(plaintext, recipientKey, pass)
|
|
if err != nil {
|
|
t.Fatalf("EncryptAndSign: %v", err)
|
|
}
|
|
if !strings.Contains(encrypted, "BEGIN PGP MESSAGE") {
|
|
t.Error("encrypted+signed output missing PGP message header")
|
|
}
|
|
|
|
decrypted, err := svc.Decrypt(encrypted, pass)
|
|
if err != nil {
|
|
t.Fatalf("Decrypt: %v", err)
|
|
}
|
|
if decrypted != plaintext {
|
|
t.Errorf("EncryptAndSign roundtrip mismatch: got %q, want %q", decrypted, plaintext)
|
|
}
|
|
}
|
|
|
|
func TestEncryptAndSign_WrongPassphrase(t *testing.T) {
|
|
svc, pubArmor, _ := newLockedTestService(t)
|
|
|
|
recipientKey, err := crypto.NewKeyFromArmored(pubArmor)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient key: %v", err)
|
|
}
|
|
|
|
_, err = svc.EncryptAndSign("content", recipientKey, "wrong-passphrase")
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong passphrase")
|
|
}
|
|
}
|
|
|
|
// ---------- SignData ----------
|
|
|
|
func TestSignData_Success(t *testing.T) {
|
|
svc, _, pass := newTestService(t)
|
|
|
|
data := []byte("Data to be signed")
|
|
signed, err := svc.SignData(data, pass)
|
|
if err != nil {
|
|
t.Fatalf("SignData: %v", err)
|
|
}
|
|
if !strings.Contains(signed, "BEGIN PGP SIGNATURE") {
|
|
t.Error("signed output missing PGP signature header")
|
|
}
|
|
}
|
|
|
|
func TestSignData_WrongPassphrase(t *testing.T) {
|
|
svc, _, _ := newLockedTestService(t)
|
|
|
|
_, err := svc.SignData([]byte("data"), "wrong-passphrase")
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong passphrase")
|
|
}
|
|
}
|
|
|
|
func TestSignData_EmptyData(t *testing.T) {
|
|
svc, _, pass := newTestService(t)
|
|
|
|
signed, err := svc.SignData([]byte(""), pass)
|
|
if err != nil {
|
|
t.Fatalf("SignData empty: %v", err)
|
|
}
|
|
if !strings.Contains(signed, "BEGIN PGP SIGNATURE") {
|
|
t.Error("empty data signature missing PGP signature header")
|
|
}
|
|
}
|
|
|
|
// ---------- EncryptAttachment / DecryptAttachment ----------
|
|
|
|
func TestEncryptDecryptAttachment_Roundtrip(t *testing.T) {
|
|
svc, pubArmor, pass := newTestService(t)
|
|
|
|
recipientKey, err := crypto.NewKeyFromArmored(pubArmor)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient key: %v", err)
|
|
}
|
|
|
|
original := []byte("Attachment binary content")
|
|
attachment, err := svc.EncryptAttachment(original, recipientKey)
|
|
if err != nil {
|
|
t.Fatalf("EncryptAttachment: %v", err)
|
|
}
|
|
if attachment == nil {
|
|
t.Fatal("attachment is nil")
|
|
}
|
|
if attachment.DataEnc == "" {
|
|
t.Error("DataEnc is empty")
|
|
}
|
|
if len(attachment.Keys) == 0 {
|
|
t.Error("Keys slice is empty")
|
|
}
|
|
|
|
decrypted, err := svc.DecryptAttachment(attachment, pass)
|
|
if err != nil {
|
|
t.Fatalf("DecryptAttachment: %v", err)
|
|
}
|
|
if string(decrypted) != string(original) {
|
|
t.Errorf("attachment roundtrip mismatch: got %q, want %q", string(decrypted), string(original))
|
|
}
|
|
}
|
|
|
|
func TestEncryptDecryptAttachment_LargeData(t *testing.T) {
|
|
svc, pubArmor, pass := newTestService(t)
|
|
|
|
recipientKey, err := crypto.NewKeyFromArmored(pubArmor)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient key: %v", err)
|
|
}
|
|
|
|
original := make([]byte, 10240)
|
|
for i := range original {
|
|
original[i] = byte(i % 256)
|
|
}
|
|
|
|
attachment, err := svc.EncryptAttachment(original, recipientKey)
|
|
if err != nil {
|
|
t.Fatalf("EncryptAttachment: %v", err)
|
|
}
|
|
|
|
decrypted, err := svc.DecryptAttachment(attachment, pass)
|
|
if err != nil {
|
|
t.Fatalf("DecryptAttachment: %v", err)
|
|
}
|
|
if len(decrypted) != len(original) {
|
|
t.Errorf("size mismatch: got %d, want %d", len(decrypted), len(original))
|
|
}
|
|
for i := range original {
|
|
if decrypted[i] != original[i] {
|
|
t.Errorf("byte %d mismatch: got %d, want %d", i, decrypted[i], original[i])
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecryptAttachment_NoKeys(t *testing.T) {
|
|
svc, _, pass := newTestService(t)
|
|
|
|
attachment := &Attachment{DataEnc: "some-data"}
|
|
_, err := svc.DecryptAttachment(attachment, pass)
|
|
if err == nil {
|
|
t.Fatal("expected error for attachment with no keys")
|
|
}
|
|
if !strings.Contains(err.Error(), "no keys available") {
|
|
t.Errorf("unexpected error: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestDecryptAttachment_WrongPassphrase(t *testing.T) {
|
|
svc, pubArmor, _ := newLockedTestService(t)
|
|
|
|
recipientKey, err := crypto.NewKeyFromArmored(pubArmor)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient key: %v", err)
|
|
}
|
|
|
|
attachment, err := svc.EncryptAttachment([]byte("content"), recipientKey)
|
|
if err != nil {
|
|
t.Fatalf("EncryptAttachment: %v", err)
|
|
}
|
|
|
|
_, err = svc.DecryptAttachment(attachment, "wrong-passphrase")
|
|
if err == nil {
|
|
t.Fatal("expected error for wrong passphrase")
|
|
}
|
|
}
|
|
|
|
// ---------- Cross-key Encrypt/Decrypt ----------
|
|
|
|
func TestEncryptDecrypt_CrossKey(t *testing.T) {
|
|
sender, senderPub, senderPass := newTestService(t)
|
|
_, _, _ = sender, senderPub, senderPass
|
|
|
|
recipientPriv, recipientPub, _ := testKey(t)
|
|
recipientKey, err := crypto.NewKeyFromArmored(recipientPub)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient pub key: %v", err)
|
|
}
|
|
|
|
plaintext := "Cross-key encrypted message"
|
|
encrypted, err := sender.Encrypt(plaintext, recipientKey)
|
|
if err != nil {
|
|
t.Fatalf("Encrypt: %v", err)
|
|
}
|
|
|
|
recipientSVC, err := NewPGPService(recipientPriv)
|
|
if err != nil {
|
|
t.Fatalf("NewPGPService for recipient: %v", err)
|
|
}
|
|
|
|
decrypted, err := recipientSVC.Decrypt(encrypted, "test-passphrase")
|
|
if err != nil {
|
|
t.Fatalf("Decrypt: %v", err)
|
|
}
|
|
if decrypted != plaintext {
|
|
t.Errorf("cross-key roundtrip mismatch: got %q, want %q", decrypted, plaintext)
|
|
}
|
|
}
|
|
|
|
// ---------- EncryptAndSign with cross-key ----------
|
|
|
|
func TestEncryptAndSign_CrossKey(t *testing.T) {
|
|
sender, _, senderPass := newTestService(t)
|
|
|
|
recipientPriv, recipientPub, _ := testKey(t)
|
|
recipientKey, err := crypto.NewKeyFromArmored(recipientPub)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient pub key: %v", err)
|
|
}
|
|
|
|
plaintext := "Cross-key signed+encrypted"
|
|
encrypted, err := sender.EncryptAndSign(plaintext, recipientKey, senderPass)
|
|
if err != nil {
|
|
t.Fatalf("EncryptAndSign: %v", err)
|
|
}
|
|
|
|
recipientSVC, err := NewPGPService(recipientPriv)
|
|
if err != nil {
|
|
t.Fatalf("NewPGPService for recipient: %v", err)
|
|
}
|
|
|
|
decrypted, err := recipientSVC.Decrypt(encrypted, "test-passphrase")
|
|
if err != nil {
|
|
t.Fatalf("Decrypt: %v", err)
|
|
}
|
|
if decrypted != plaintext {
|
|
t.Errorf("cross-key EncryptAndSign mismatch: got %q, want %q", decrypted, plaintext)
|
|
}
|
|
}
|
|
|
|
// ---------- Attachment cross-key ----------
|
|
|
|
func TestEncryptDecryptAttachment_CrossKey(t *testing.T) {
|
|
sender, _, _ := newTestService(t)
|
|
|
|
recipientPriv, recipientPub, _ := testKey(t)
|
|
recipientKey, err := crypto.NewKeyFromArmored(recipientPub)
|
|
if err != nil {
|
|
t.Fatalf("parse recipient pub key: %v", err)
|
|
}
|
|
|
|
original := []byte("Cross-key attachment data")
|
|
attachment, err := sender.EncryptAttachment(original, recipientKey)
|
|
if err != nil {
|
|
t.Fatalf("EncryptAttachment: %v", err)
|
|
}
|
|
|
|
recipientSVC, err := NewPGPService(recipientPriv)
|
|
if err != nil {
|
|
t.Fatalf("NewPGPService for recipient: %v", err)
|
|
}
|
|
|
|
decrypted, err := recipientSVC.DecryptAttachment(attachment, "test-passphrase")
|
|
if err != nil {
|
|
t.Fatalf("DecryptAttachment: %v", err)
|
|
}
|
|
if string(decrypted) != string(original) {
|
|
t.Errorf("cross-key attachment mismatch: got %q, want %q", string(decrypted), string(original))
|
|
}
|
|
}
|