From 2f89b9fa0e46965374a6738acadf30094c9e3cad Mon Sep 17 00:00:00 2001 From: wussler Date: Tue, 1 Sep 2020 10:02:13 +0200 Subject: [PATCH] Add KeyIDs public API functionality (#76) * Add public KeyIDs functions * Add signature keyIDs functions * Lint code --- CHANGELOG.md | 47 ++++++++++++++-- crypto/key.go | 7 ++- crypto/message.go | 74 +++++++++++++++++++++++++- crypto/message_test.go | 48 ++++++++++++++++- crypto/sessionkey_test.go | 2 +- crypto/testdata/message_multipleKeyID | 20 +++++++ crypto/testdata/message_plainSignature | 23 ++++++++ 7 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 crypto/testdata/message_multipleKeyID create mode 100644 crypto/testdata/message_plainSignature diff --git a/CHANGELOG.md b/CHANGELOG.md index 53fe77d..09e79fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (key *Key) ArmorWithCustomHeaders(comment, version string) (string, error) (key *Key) GetArmoredPublicKeyWithCustomHeaders(comment, version string) (string, error) ``` + - Message armoring with custom headers ```go (msg *PGPMessage) GetArmoredWithCustomHeaders(comment, version string) (string, error) @@ -18,7 +19,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extraction of encryption key IDs from a PGP message, i.e. the IDs of the keys used in the encryption of the session key ```go -(msg *PGPMessage) getEncryptionKeyIDs() ([]uint64, bool) +(msg *PGPMessage) GetEncryptionKeyIDs() ([]uint64, bool) +(msg *PGPMessage) GetHexEncryptionKeyIDs() ([]uint64, bool) +``` + +- Extraction of signing key IDs from a PGP message, i.e. the IDs of the keys used in the signature of the message + (of all the readable, unencrypted signature packets) +```go +(msg *PGPMessage) GetSignatureKeyIDs() ([]uint64, bool) +(msg *PGPMessage) GetHexSignatureKeyIDs() ([]string, bool) ``` - Getter for the x/crypto Entity (internal components of an OpenPGP key) from Key struct @@ -37,17 +46,49 @@ DecryptBinaryMessageArmored(privateKey string, passphrase []byte, ciphertext str (key *Key) ToPublic() (publicKey *Key, err error) ``` +- Helpers to handle detached signatures +```go +EncryptSignArmoredDetached( + publicKey, privateKey string, + passphrase, plainData []byte, +) (ciphertext, signature string, err error) + +DecryptVerifyArmoredDetached( + publicKey, privateKey string, + passphrase []byte, + ciphertext string, + armoredSignature string, +) (plainData []byte, err error) +``` + +- `EncryptSignArmoredDetachedMobileResult` Struct (with its helper) to allow detached signature + encryption in one pass +```go +type EncryptSignArmoredDetachedMobileResult struct { + Ciphertext, Signature string +} + +EncryptSignArmoredDetachedMobile( + publicKey, privateKey string, + passphrase, plainData []byte, +) (wrappedTuple *EncryptSignArmoredDetachedMobileResult, err error) +``` + ### Changed - Improved key and message armoring testing - `EncryptSessionKey` now creates encrypted key packets for each valid encryption key in the provided keyring. Returns a byte slice with all the concatenated key packets. -- Use aes256 chiper for message encryption with password. +- Use aes256 cipher for password-encrypted messages. +- The helpers `EncryptSignMessageArmored`, `DecryptVerifyMessageArmored`, `DecryptVerifyAttachment`, and`DecryptBinaryMessageArmored` + now accept private keys as public keys and perform automatic casting if the keys are locked. ### Fixed - Public key armoring headers - `EncryptSessionKey` throws an error when invalid encryption keys are provided - Session keys' size is now checked against the expected value to prevent panics - +- Hex Key IDs returned from `(key *Key) GetHexKeyID() string` are now correctly padded +- Avoid panics in `(msg *PGPMessage) GetEncryptionKeyIDs() ([]uint64, bool)` by breaking the packet.next cycle on specific packet types +- Prevent the server time from going backwards in `UpdateTime` + ## [2.0.1] - 2020-05-01 ### Security - Updated underlying crypto library diff --git a/crypto/key.go b/crypto/key.go index 1ede809..f135fd6 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -332,7 +332,7 @@ func (key *Key) PrintFingerprints() { // GetHexKeyID returns the key ID, hex encoded as a string. func (key *Key) GetHexKeyID() string { - return strconv.FormatUint(key.GetKeyID(), 16) + return keyIDToHex(key.GetKeyID()) } // GetKeyID returns the key ID, encoded as 8-byte int. @@ -465,3 +465,8 @@ func generateKey( return &Key{newEntity}, nil } + +// keyIDToHex casts a keyID to hex with the correct padding. +func keyIDToHex(keyID uint64) string { + return fmt.Sprintf("%016v", strconv.FormatUint(keyID, 16)) +} diff --git a/crypto/message.go b/crypto/message.go index f738d54..c43e70a 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -223,8 +223,8 @@ func (msg *PGPMessage) GetArmoredWithCustomHeaders(comment, version string) (str return armor.ArmorWithTypeAndCustomHeaders(msg.Data, constants.PGPMessageHeader, version, comment) } -// getEncryptionKeyIds Returns the key IDs of the keys to which the session key is encrypted. -func (msg *PGPMessage) getEncryptionKeyIDs() ([]uint64, bool) { +// GetEncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted. +func (msg *PGPMessage) GetEncryptionKeyIDs() ([]uint64, bool) { packets := packet.NewReader(bytes.NewReader(msg.Data)) var err error var ids []uint64 @@ -252,6 +252,21 @@ Loop: return ids, false } +// GetHexEncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted. +func (msg *PGPMessage) GetHexEncryptionKeyIDs() ([]string, bool) { + return getHexKeyIDs(msg.GetEncryptionKeyIDs()) +} + +// GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to. +func (msg *PGPMessage) GetSignatureKeyIDs() ([]uint64, bool) { + return getSignatureKeyIDs(msg.Data) +} + +// GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted. +func (msg *PGPMessage) GetHexSignatureKeyIDs() ([]string, bool) { + return getHexKeyIDs(msg.GetSignatureKeyIDs()) +} + // GetBinaryDataPacket returns the unarmored binary datapacket as a []byte. func (msg *PGPSplitMessage) GetBinaryDataPacket() []byte { return msg.DataPacket @@ -386,6 +401,16 @@ func (msg *PGPSignature) GetArmored() (string, error) { return armor.ArmorWithType(msg.Data, constants.PGPSignatureHeader) } +// GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to. +func (msg *PGPSignature) GetSignatureKeyIDs() ([]uint64, bool) { + return getSignatureKeyIDs(msg.Data) +} + +// GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted. +func (msg *PGPSignature) GetHexSignatureKeyIDs() ([]string, bool) { + return getHexKeyIDs(msg.GetSignatureKeyIDs()) +} + // GetBinary returns the unarmored signed data as a []byte. func (msg *ClearTextMessage) GetBinary() []byte { return msg.Data @@ -425,3 +450,48 @@ func IsPGPMessage(data string) bool { constants.PGPMessageHeader + "-----") return re.MatchString(data) } + +func getSignatureKeyIDs(data []byte) ([]uint64, bool) { + packets := packet.NewReader(bytes.NewReader(data)) + var err error + var ids []uint64 + var onePassSignaturePacket *packet.OnePassSignature + var signaturePacket *packet.Signature + +Loop: + for { + var p packet.Packet + if p, err = packets.Next(); err == io.EOF { + break + } + switch p := p.(type) { + case *packet.OnePassSignature: + onePassSignaturePacket = p + ids = append(ids, onePassSignaturePacket.KeyId) + case *packet.Signature: + signaturePacket = p + if signaturePacket.IssuerKeyId != nil { + ids = append(ids, *signaturePacket.IssuerKeyId) + } + case *packet.SymmetricallyEncrypted, + *packet.AEADEncrypted, + *packet.Compressed, + *packet.LiteralData: + break Loop + } + } + if len(ids) > 0 { + return ids, true + } + return ids, false +} + +func getHexKeyIDs(keyIDs []uint64, ok bool) ([]string, bool) { + hexIDs := make([]string, len(keyIDs)) + + for i, id := range keyIDs { + hexIDs[i] = keyIDToHex(id) + } + + return hexIDs, ok +} diff --git a/crypto/message_test.go b/crypto/message_test.go index 32c752b..aa55570 100644 --- a/crypto/message_test.go +++ b/crypto/message_test.go @@ -228,7 +228,7 @@ func TestMultipleKeyMessageEncryption(t *testing.T) { assert.Exactly(t, message.GetString(), decrypted.GetString()) } -func TestMessagegetGetEncryptionKeyIDs(t *testing.T) { +func TestMessageGetEncryptionKeyIDs(t *testing.T) { var message = NewPlainMessageFromString("plain text") assert.Exactly(t, 3, len(keyRingTestMultiple.entities)) @@ -236,7 +236,7 @@ func TestMessagegetGetEncryptionKeyIDs(t *testing.T) { if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } - ids, ok := ciphertext.getEncryptionKeyIDs() + ids, ok := ciphertext.GetEncryptionKeyIDs() assert.Exactly(t, 3, len(ids)) assert.True(t, ok) encKey, ok := keyRingTestMultiple.entities[0].EncryptionKey(time.Now()) @@ -244,6 +244,50 @@ func TestMessagegetGetEncryptionKeyIDs(t *testing.T) { assert.Exactly(t, encKey.PublicKey.KeyId, ids[0]) } +func TestMessageGetHexGetEncryptionKeyIDs(t *testing.T) { + ciphertext, err := NewPGPMessageFromArmored(readTestFile("message_multipleKeyID", false)) + if err != nil { + t.Fatal("Expected no error when reading message, got:", err) + } + + ids, ok := ciphertext.GetHexEncryptionKeyIDs() + assert.Exactly(t, 2, len(ids)) + assert.True(t, ok) + + assert.Exactly(t, "76ad736fa7e0e83c", ids[0]) + assert.Exactly(t, "0f65b7ae456a9ceb", ids[1]) +} + +func TestMessageGetSignatureKeyIDs(t *testing.T) { + var message = NewPlainMessageFromString("plain text") + + signature, err := keyRingTestPrivate.SignDetached(message) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + ids, ok := signature.GetSignatureKeyIDs() + assert.Exactly(t, 1, len(ids)) + assert.True(t, ok) + signingKey, ok := keyRingTestPrivate.entities[0].SigningKey(time.Now()) + assert.True(t, ok) + assert.Exactly(t, signingKey.PublicKey.KeyId, ids[0]) +} + +func TestMessageGetHexSignatureKeyIDs(t *testing.T) { + ciphertext, err := NewPGPMessageFromArmored(readTestFile("message_plainSignature", false)) + if err != nil { + t.Fatal("Expected no error when reading message, got:", err) + } + + ids, ok := ciphertext.GetHexSignatureKeyIDs() + assert.Exactly(t, 2, len(ids)) + assert.True(t, ok) + + assert.Exactly(t, "3eb6259edf21df24", ids[0]) + assert.Exactly(t, "d05b722681936ad0", ids[1]) +} + func TestMessageGetArmoredWithCustomHeaders(t *testing.T) { var message = NewPlainMessageFromString("plain text") diff --git a/crypto/sessionkey_test.go b/crypto/sessionkey_test.go index b090085..974f694 100644 --- a/crypto/sessionkey_test.go +++ b/crypto/sessionkey_test.go @@ -143,7 +143,7 @@ func TestDataPacketEncryption(t *testing.T) { if err != nil { t.Fatal("Unable to unarmor pgp message, got:", err) } - ids, ok := pgpMessage.getEncryptionKeyIDs() + ids, ok := pgpMessage.GetEncryptionKeyIDs() assert.True(t, ok) assert.Exactly(t, 3, len(ids)) diff --git a/crypto/testdata/message_multipleKeyID b/crypto/testdata/message_multipleKeyID new file mode 100644 index 0000000..c7191f8 --- /dev/null +++ b/crypto/testdata/message_multipleKeyID @@ -0,0 +1,20 @@ +-----BEGIN PGP MESSAGE----- +Version: OpenPGP.js v3.1.3 +Comment: https://openpgpjs.org + +wcBMA3atc2+n4Og8AQf+PiFMi7BkL/Ppe6kILs0E/ik0jR4sAXaNA0kpJYYE +FhWBZ8RWOFLeriyABaY2gQD86kQ7TZSgJcOoBYWLEIRdnHP0ss4niG1WuEPB +gyKXdyyS99URdYtxCV0ErxPHObdO61vxwsE7Fx9nC3Lk8mnktzfqWQelA9Ej +TLj9pO3M+uo2H5baZ8ylErcTRmgyQlSy+gcjCDeLs94kDpbdJlZX1x+79Zub +2iWnOSmdmW2ZhbBq7w8az5qhPUfMMNIRcfieOAQwkKz5dBoCO81mwHcd+LWY +EiiNDNVrD5uYV4t0PqHp9o5537JV//tlG1UEWvNhQ35tqXgsmND7MQkb2ct3 +6cHATAMPZbeuRWqc6wEH/2wKL4h9QOOxBAJKHf1MJlVbMN8qgwtKr7q/YcdH +gooTF5asSMq91qKCcW38NuFXkdzf5sGZsOA0yLTQnjbu+42RzBfty10qTZg0 +v1zmgKErACFpsztYNOdsJh3aGJ4WjytpZWKL+PqHnX4/HR+zbEMEGfDjhTBZ +eL8TLa3F3OK533F2oNAO5ITYsdnipVmiy89FL5yt/9ZpvFRRuj8lOwJYTe4O +pQ+ZbenRZ0sXyEIV6xZguqvFICOELoy4LM3kHpQfY4Gi1JPT/buWxnDFugEW +u5JQ6qix0Y1KunuWSogEjfaJ8BgLSBs/U7RxxjLYETFB15VxyEaQJx9wx3Id +uU7SRQG+UkFbDn9ghZoF7ROPTXAUnHlqODGxdgnPhJJQaPSkNOMALkBI6I4Q +lqsO2LprVTVeCGo+Qd3WrE5MqGunvDli5VK37A== +=QeVN +-----END PGP MESSAGE----- diff --git a/crypto/testdata/message_plainSignature b/crypto/testdata/message_plainSignature new file mode 100644 index 0000000..dab02aa --- /dev/null +++ b/crypto/testdata/message_plainSignature @@ -0,0 +1,23 @@ +-----BEGIN PGP MESSAGE----- +Comment: https://gopenpgp.org +Version: GopenPGP 2.0.1 + +wcBMAw9lt65FapzrAQf/bAoviH1A47EEAkod/UwmVVsw3yqDC0qvur9hx0eCihMX +lqxIyr3WooJxbfw24VeR3N/mwZmw4DTItNCeNu77jZHMF+3LXSpNmDS/XOaAoSsA +IWmzO1g052wmHdoYnhaPK2llYov4+oedfj8dH7NsQwQZ8OOFMFl4vxMtrcXc4rnf +cXag0A7khNix2eKlWaLLz0UvnK3/1mm8VFG6PyU7AlhN7g6lD5lt6dFnSxfIQhXr +FmC6q8UgI4QujLgszeQelB9jgaLUk9P9u5bGcMW6ARa7klDqqLHRjUq6e5ZKiASN +9onwGAtIGz9TtHHGMtgRMUHXlXHIRpAnH3DHch25TsLAXAQAAQoAEAUCXNlzAwkQ +PrYlnt8h3yQAAMuZCACipXp2GmSo26JgRJADNan01cBu6nVbzpNHNqKqUNLDnCvZ +L4HjXeUQ/o+vl8GSpy51kvcXmNsD36d4agnzDf7OjiIdcLns/ARSUESQyrprf+oF ++OYTeRXufxCoiG35Kn82g4ML2ifj52c+E/mS7ZTupQgSZrXPcS7XNAEuAuOnjC8O +5TpzlA3hKwirVzRVmyn2wlTVWQWWMNoci8esnLH/eZVt/3DFCBwVG3D+avsnKXN3 +CI6kAFtiRA9drDHXT56AR4lZGotTltG2g5D3exAzczuHpxA4Qshp/03dDADBAm4T +3IJ6p/7rZ3wgmeaRfcg9sAxEJ4y1pME1ma8jBrB8wpwEAAEKABAFAlzZcwMJENBb +ciaBk2rQAADeRAP+LCDF4zEXVQYhQXBnvVGEK1Ar3R/0lj3GSWczb3+QYbcmwAZR +n+ll3xlrgCqls4BfVBvXQ/hyABF3HPlkFRNodHLonq+fuvjCgEnsdJG18/yzfeP6 +Ox0w2vHRE3ad78dhJvyuWqL7Wd8L2EG9MsCdzx5MQnfWShzQE4EcSnbCtprSRQG+ +UkFbDn9ghZoF7ROPTXAUnHlqODGxdgnPhJJQaPSkNOMALkBI6I4QlqsO2LprVTVe +CGo+Qd3WrE5MqGunvDli5VK37A== +=cPmR +-----END PGP MESSAGE-----