249 lines
7.2 KiB
Go
249 lines
7.2 KiB
Go
package pgp
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||
|
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||
|
|
)
|
||
|
|
|
||
|
|
// serializeUnencryptedPrivate serializes an as-yet-unprotected private key — only
|
||
|
|
// safe to call before EncryptPrivateKeys (see the gotcha documented on
|
||
|
|
// serializePrivateWithoutSigning in identity.go). No production code path needs
|
||
|
|
// this (every stored key goes through EncryptPrivateKeys first); it exists here
|
||
|
|
// purely to simulate a genuinely unprotected "gpg --export-secret-keys" output for
|
||
|
|
// TestImportPrivateKeyUnencrypted.
|
||
|
|
func serializeUnencryptedPrivate(entity *openpgp.Entity) ([]byte, error) {
|
||
|
|
var buf bytes.Buffer
|
||
|
|
w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if err := entity.SerializePrivate(w, nil); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if err := w.Close(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return buf.Bytes(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func testEntity() Entity {
|
||
|
|
return Entity{
|
||
|
|
Headers: []string{"Content-Type: text/plain; charset=utf-8"},
|
||
|
|
Body: []byte("hello world\r\nsecond line\r\n"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGenerateEncryptDecryptRoundTrip(t *testing.T) {
|
||
|
|
pubPEM, privPEM, err := GenerateKeyPair("alice@example.com", "correct horse battery staple")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("GenerateKeyPair: %v", err)
|
||
|
|
}
|
||
|
|
if bytes.Contains(privPEM, []byte("correct horse")) {
|
||
|
|
t.Fatal("stored private key armor should not contain the plaintext passphrase")
|
||
|
|
}
|
||
|
|
|
||
|
|
recipient, err := ParsePublicKey(pubPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ParsePublicKey: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
orig := testEntity()
|
||
|
|
encrypted, err := EncryptEntity(orig, []*openpgp.Entity{recipient})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("EncryptEntity: %v", err)
|
||
|
|
}
|
||
|
|
if ct := headerValue(encrypted.Headers, "Content-Type"); ct == "" {
|
||
|
|
t.Fatal("expected a Content-Type header on the encrypted entity")
|
||
|
|
}
|
||
|
|
if bytes.Contains(encrypted.Body, orig.Body) {
|
||
|
|
t.Fatal("encrypted body should not contain the plaintext")
|
||
|
|
}
|
||
|
|
|
||
|
|
identity, err := ParsePrivateKey(privPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ParsePrivateKey: %v", err)
|
||
|
|
}
|
||
|
|
if err := UnlockPrivateKey(identity, "correct horse battery staple"); err != nil {
|
||
|
|
t.Fatalf("UnlockPrivateKey: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
decrypted, err := DecryptEntity(encrypted, identity)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("DecryptEntity: %v", err)
|
||
|
|
}
|
||
|
|
if !bytes.Equal(decrypted.Body, orig.Body) {
|
||
|
|
t.Fatalf("body mismatch: got %q want %q", decrypted.Body, orig.Body)
|
||
|
|
}
|
||
|
|
if headerValue(decrypted.Headers, "Content-Type") != headerValue(orig.Headers, "Content-Type") {
|
||
|
|
t.Fatalf("header mismatch: got %v want %v", decrypted.Headers, orig.Headers)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUnlockPrivateKeyWrongPassphraseFails(t *testing.T) {
|
||
|
|
_, privPEM, err := GenerateKeyPair("alice@example.com", "right passphrase")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
identity, err := ParsePrivateKey(privPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := UnlockPrivateKey(identity, "wrong passphrase"); err == nil {
|
||
|
|
t.Fatal("expected the wrong passphrase to fail")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestEncryptMultipleRecipientsBothCanDecrypt(t *testing.T) {
|
||
|
|
senderPub, senderPriv, err := GenerateKeyPair("sender@example.com", "sender pass")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
recipPub, recipPriv, err := GenerateKeyPair("recipient@example.com", "recipient pass")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
senderPubEntity, _ := ParsePublicKey(senderPub)
|
||
|
|
recipPubEntity, _ := ParsePublicKey(recipPub)
|
||
|
|
|
||
|
|
orig := testEntity()
|
||
|
|
encrypted, err := EncryptEntity(orig, []*openpgp.Entity{senderPubEntity, recipPubEntity})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
senderIdentity, _ := ParsePrivateKey(senderPriv)
|
||
|
|
if err := UnlockPrivateKey(senderIdentity, "sender pass"); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
senderCopy, err := DecryptEntity(encrypted, senderIdentity)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("sender DecryptEntity: %v", err)
|
||
|
|
}
|
||
|
|
if !bytes.Equal(senderCopy.Body, orig.Body) {
|
||
|
|
t.Fatal("sender's own copy did not decrypt to the original body")
|
||
|
|
}
|
||
|
|
|
||
|
|
recipIdentity, _ := ParsePrivateKey(recipPriv)
|
||
|
|
if err := UnlockPrivateKey(recipIdentity, "recipient pass"); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
recipCopy, err := DecryptEntity(encrypted, recipIdentity)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("recipient DecryptEntity: %v", err)
|
||
|
|
}
|
||
|
|
if !bytes.Equal(recipCopy.Body, orig.Body) {
|
||
|
|
t.Fatal("recipient's copy did not decrypt to the original body")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestImportPrivateKeyUnencrypted(t *testing.T) {
|
||
|
|
// Simulate a raw, not-yet-passphrase-protected export by generating a key and
|
||
|
|
// serializing it before EncryptPrivateKeys is ever called.
|
||
|
|
entity, err := openpgp.NewEntity("bob@example.com", "", "bob@example.com", defaultConfig())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
unprotectedArmor, err := serializeUnencryptedPrivate(entity)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
pubPEM, privPEM, err := ImportPrivateKey(unprotectedArmor, "new passphrase")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ImportPrivateKey: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
identity, err := ParsePrivateKey(privPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !identity.PrivateKey.Encrypted {
|
||
|
|
t.Fatal("expected the imported key to be encrypted after import")
|
||
|
|
}
|
||
|
|
if err := UnlockPrivateKey(identity, "new passphrase"); err != nil {
|
||
|
|
t.Fatalf("expected the new passphrase to unlock the imported key: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
recipient, err := ParsePublicKey(pubPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
orig := testEntity()
|
||
|
|
encrypted, err := EncryptEntity(orig, []*openpgp.Entity{recipient})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
decrypted, err := DecryptEntity(encrypted, identity)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("DecryptEntity after import: %v", err)
|
||
|
|
}
|
||
|
|
if !bytes.Equal(decrypted.Body, orig.Body) {
|
||
|
|
t.Fatal("round trip through an imported unencrypted key failed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestImportPrivateKeyAlreadyEncrypted(t *testing.T) {
|
||
|
|
_, existingArmor, err := GenerateKeyPair("carol@example.com", "original passphrase")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if _, _, err := ImportPrivateKey(existingArmor, "wrong passphrase"); err == nil {
|
||
|
|
t.Fatal("expected import with the wrong passphrase for an already-encrypted key to fail")
|
||
|
|
}
|
||
|
|
|
||
|
|
pubPEM, privPEM, err := ImportPrivateKey(existingArmor, "original passphrase")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ImportPrivateKey with the correct passphrase: %v", err)
|
||
|
|
}
|
||
|
|
identity, err := ParsePrivateKey(privPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := UnlockPrivateKey(identity, "original passphrase"); err != nil {
|
||
|
|
t.Fatalf("expected the original passphrase to still unlock after re-import: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
recipient, err := ParsePublicKey(pubPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
orig := testEntity()
|
||
|
|
encrypted, err := EncryptEntity(orig, []*openpgp.Entity{recipient})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
decrypted, err := DecryptEntity(encrypted, identity)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("DecryptEntity after re-import: %v", err)
|
||
|
|
}
|
||
|
|
if !bytes.Equal(decrypted.Body, orig.Body) {
|
||
|
|
t.Fatal("round trip through a re-imported already-encrypted key failed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFingerprintIsStableAndNonEmpty(t *testing.T) {
|
||
|
|
pubPEM, _, err := GenerateKeyPair("alice@example.com", "pass")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
entity, err := ParsePublicKey(pubPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
fp := Fingerprint(entity)
|
||
|
|
if len(fp) == 0 {
|
||
|
|
t.Fatal("expected a non-empty fingerprint")
|
||
|
|
}
|
||
|
|
entity2, err := ParsePublicKey(pubPEM)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if Fingerprint(entity2) != fp {
|
||
|
|
t.Fatal("expected the fingerprint to be stable across re-parses of the same key")
|
||
|
|
}
|
||
|
|
}
|