diff --git a/.golangci.yml b/.golangci.yml index 251bd2b..c9bf546 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -21,6 +21,7 @@ linters: - gochecknoglobals # Checks that no globals are present in Go code [fast: true, auto-fix: false] - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] - golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: true, auto-fix: false] + - goerr113 # Golang linter to check the errors handling expressions [fast: true, auto-fix: false] - gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] - lll # Reports long lines [fast: true, auto-fix: false] - testpackage # Makes you use a separate _test package [fast: true, auto-fix: false] diff --git a/CHANGELOG.md b/CHANGELOG.md index a7564b3..20d0da8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- Key Armoring with custom headers +```go +(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) +``` + +### Changed +- Improved key and message armoring testing + +### Fixed +- Public key armoring headers + ## [2.0.1] - 2020-05-01 ### Security - Updated underlying crypto library diff --git a/armor/armor.go b/armor/armor.go index 9641470..1af945e 100644 --- a/armor/armor.go +++ b/armor/armor.go @@ -26,9 +26,35 @@ func ArmorWithTypeBuffered(w io.Writer, armorType string) (io.WriteCloser, error // ArmorWithType armors input with the given armorType. func ArmorWithType(input []byte, armorType string) (string, error) { + return armorWithTypeAndHeaders(input, armorType, internal.ArmorHeaders) +} + +// ArmorWithTypeAndCustomHeaders armors input with the given armorType and +// headers. +func ArmorWithTypeAndCustomHeaders(input []byte, armorType, version, comment string) (string, error) { + headers := make(map[string]string) + if version != "" { + headers["Version"] = version + } + if comment != "" { + headers["Comment"] = comment + } + return armorWithTypeAndHeaders(input, armorType, headers) +} + +// Unarmor unarmors an armored input into a byte array. +func Unarmor(input string) ([]byte, error) { + b, err := internal.Unarmor(input) + if err != nil { + return nil, err + } + return ioutil.ReadAll(b.Body) +} + +func armorWithTypeAndHeaders(input []byte, armorType string, headers map[string]string) (string, error) { var b bytes.Buffer - w, err := armor.Encode(&b, armorType, internal.ArmorHeaders) + w, err := armor.Encode(&b, armorType, headers) if err != nil { return "", err @@ -41,12 +67,3 @@ func ArmorWithType(input []byte, armorType string) (string, error) { } return b.String(), nil } - -// Unarmor unarmors an armored key. -func Unarmor(input string) ([]byte, error) { - b, err := internal.Unarmor(input) - if err != nil { - return nil, err - } - return ioutil.ReadAll(b.Body) -} diff --git a/crypto/key.go b/crypto/key.go index f70c164..d516316 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -16,7 +16,6 @@ import ( "github.com/pkg/errors" openpgp "golang.org/x/crypto/openpgp" - xarmor "golang.org/x/crypto/openpgp/armor" packet "golang.org/x/crypto/openpgp/packet" ) @@ -190,6 +189,7 @@ func (key *Key) Serialize() ([]byte, error) { return buffer.Bytes(), err } +// Armor returns the armored key as a string with default gopenpgp headers. func (key *Key) Armor() (string, error) { serialized, err := key.Serialize() if err != nil { @@ -199,21 +199,36 @@ func (key *Key) Armor() (string, error) { return armor.ArmorWithType(serialized, constants.PrivateKeyHeader) } -// GetArmoredPublicKey returns the armored public keys from this keyring. -func (key *Key) GetArmoredPublicKey() (s string, err error) { - var outBuf bytes.Buffer - aw, err := xarmor.Encode(&outBuf, openpgp.PublicKeyType, nil) +// ArmorWithCustomHeaders returns the armored key as a string, with +// the given headers. Empty parameters are omitted from the headers. +func (key *Key) ArmorWithCustomHeaders(comment, version string) (string, error) { + serialized, err := key.Serialize() if err != nil { return "", err } - if err = key.entity.Serialize(aw); err != nil { - _ = aw.Close() + return armor.ArmorWithTypeAndCustomHeaders(serialized, constants.PrivateKeyHeader, version, comment) +} + +// GetArmoredPublicKey returns the armored public keys from this keyring. +func (key *Key) GetArmoredPublicKey() (s string, err error) { + serialized, err := key.GetPublicKey() + if err != nil { return "", err } - err = aw.Close() - return outBuf.String(), err + return armor.ArmorWithType(serialized, constants.PublicKeyHeader) +} + +// GetArmoredPublicKeyWithCustomHeaders returns the armored public key as a string, with +// the given headers. Empty parameters are omitted from the headers. +func (key *Key) GetArmoredPublicKeyWithCustomHeaders(comment, version string) (string, error) { + serialized, err := key.GetPublicKey() + if err != nil { + return "", err + } + + return armor.ArmorWithTypeAndCustomHeaders(serialized, constants.PublicKeyHeader, version, comment) } // GetPublicKey returns the unarmored public keys from this keyring. diff --git a/crypto/key_test.go b/crypto/key_test.go index 0d09e9b..3505587 100644 --- a/crypto/key_test.go +++ b/crypto/key_test.go @@ -73,13 +73,25 @@ func TestArmorKeys(t *testing.T) { t.Fatal("Cannot armor unprotected EC key:" + err.Error()) } - rTest := regexp.MustCompile("(?s)^-----BEGIN PGP PRIVATE KEY BLOCK-----.*-----END PGP PRIVATE KEY BLOCK-----$") + rTest := regexp.MustCompile(`(?s)^-----BEGIN PGP PRIVATE KEY BLOCK-----.*Version: GopenPGP [0-9]+\.[0-9]+\.[0-9]+.*-----END PGP PRIVATE KEY BLOCK-----$`) assert.Regexp(t, rTest, noPasswordRSA) assert.Regexp(t, rTest, noPasswordEC) assert.Regexp(t, rTest, keyTestArmoredRSA) assert.Regexp(t, rTest, keyTestArmoredEC) } +func TestArmorKeysWithCustomHeader(t *testing.T) { + comment := "User-defined private key comment" + version := "User-defined private key version" + armored, err := keyTestRSA.ArmorWithCustomHeaders(comment, version) + if err != nil { + t.Fatal("Could not armor the private key:", err) + } + + assert.Contains(t, armored, "Comment: "+comment) + assert.Contains(t, armored, "Version: "+version) +} + func TestLockUnlockKeys(t *testing.T) { testLockUnlockKey(t, keyTestArmoredRSA, keyTestPassphrase) testLockUnlockKey(t, keyTestArmoredEC, keyTestPassphrase) @@ -257,7 +269,7 @@ func TestFailCheckIntegrity(t *testing.T) { assert.Exactly(t, false, isVerified) } -func TestArmorPublicKey(t *testing.T) { +func TestGetPublicKey(t *testing.T) { publicKey, err := keyTestRSA.GetPublicKey() if err != nil { t.Fatal("Expected no error while obtaining public key, got:", err) @@ -265,19 +277,21 @@ func TestArmorPublicKey(t *testing.T) { decodedKey, err := NewKey(publicKey) if err != nil { - t.Fatal("Expected no error while creating public key ring, got:", err) + t.Fatal("Expected no error while creating public key, got:", err) } privateFingerprint := keyTestRSA.GetFingerprint() publicFingerprint := decodedKey.GetFingerprint() + assert.False(t, decodedKey.IsPrivate()) + assert.True(t, keyTestRSA.IsPrivate()) assert.Exactly(t, privateFingerprint, publicFingerprint) } func TestGetArmoredPublicKey(t *testing.T) { privateKey, err := NewKeyFromArmored(readTestFile("keyring_privateKey", false)) if err != nil { - t.Fatal("Expected no error while unarmouring private key, got:", err) + t.Fatal("Expected no error while unarmoring private key, got:", err) } s, err := privateKey.GetArmoredPublicKey() @@ -309,6 +323,47 @@ func TestGetArmoredPublicKey(t *testing.T) { } assert.Exactly(t, eb, b) + + publicKey, err := keyTestRSA.GetArmoredPublicKey() + if err != nil { + t.Fatal("Expected no error while obtaining armored public key, got:", err) + } + + decodedKey, err := NewKeyFromArmored(publicKey) + if err != nil { + t.Fatal("Expected no error while creating public key from armored, got:", err) + } + + assert.False(t, decodedKey.IsPrivate()) + assert.True(t, keyTestRSA.IsPrivate()) + assert.Contains(t, publicKey, "Version: GopenPGP") + + privateFingerprint := keyTestRSA.GetFingerprint() + publicFingerprint := decodedKey.GetFingerprint() + + assert.Exactly(t, privateFingerprint, publicFingerprint) +} + +func TestGetArmoredPublicKeyWithCustomHeaders(t *testing.T) { + comment := "User-defined public key comment" + version := "User-defined public key version" + armored, err := keyTestRSA.GetArmoredPublicKeyWithCustomHeaders(comment, version) + if err != nil { + t.Fatal("Could not armor the public key:", err) + } + + assert.Contains(t, armored, "Comment: "+comment) + assert.Contains(t, armored, "Version: "+version) +} + +func TestGetArmoredPublicKeyWithEmptyCustomHeaders(t *testing.T) { + armored, err := keyTestRSA.GetArmoredPublicKeyWithCustomHeaders("", "") + if err != nil { + t.Fatal("Could not armor the public key:", err) + } + + assert.NotContains(t, armored, "Version") + assert.NotContains(t, armored, "Comment") } func TestGetSHA256FingerprintsV4(t *testing.T) { diff --git a/crypto/keyring_test.go b/crypto/keyring_test.go index 226cd20..b4d1636 100644 --- a/crypto/keyring_test.go +++ b/crypto/keyring_test.go @@ -12,12 +12,10 @@ import ( var testSymmetricKey []byte -// Corresponding key in testdata/keyring_privateKey +// Password for key in testdata/keyring_privateKeyLegacy: "123". +// Corresponding key in testdata/keyring_privateKey. var testMailboxPassword = []byte("apple") -// Corresponding key in testdata/keyring_privateKeyLegacy -// const testMailboxPasswordLegacy = [][]byte{ []byte("123") } - var ( keyRingTestPrivate *KeyRing keyRingTestPublic *KeyRing diff --git a/crypto/message.go b/crypto/message.go index 8ecda6f..1471fda 100644 --- a/crypto/message.go +++ b/crypto/message.go @@ -48,7 +48,7 @@ type PGPSplitMessage struct { } // A ClearTextMessage is a signed but not encrypted PGP message, -// i.e. the ones beginning with -----BEGIN PGP SIGNED MESSAGE----- +// i.e. the ones beginning with -----BEGIN PGP SIGNED MESSAGE-----. type ClearTextMessage struct { Data []byte Signature []byte @@ -217,6 +217,12 @@ func (msg *PGPMessage) GetArmored() (string, error) { return armor.ArmorWithType(msg.Data, constants.PGPMessageHeader) } +// GetArmoredWithCustomHeaders returns the armored message as a string, with +// the given headers. Empty parameters are omitted from the headers. +func (msg *PGPMessage) GetArmoredWithCustomHeaders(comment, version string) (string, error) { + return armor.ArmorWithTypeAndCustomHeaders(msg.Data, constants.PGPMessageHeader, version, comment) +} + // GetBinaryDataPacket returns the unarmored binary datapacket as a []byte. func (msg *PGPSplitMessage) GetBinaryDataPacket() []byte { return msg.DataPacket diff --git a/crypto/message_test.go b/crypto/message_test.go index b38c171..9dce8cf 100644 --- a/crypto/message_test.go +++ b/crypto/message_test.go @@ -178,3 +178,39 @@ func TestMultipleKeyMessageEncryption(t *testing.T) { } assert.Exactly(t, message.GetString(), decrypted.GetString()) } + +func TestMessageGetArmoredWithCustomHeaders(t *testing.T) { + var message = NewPlainMessageFromString("plain text") + + ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + comment := "User-defined comment" + version := "User-defined version" + armored, err := ciphertext.GetArmoredWithCustomHeaders(comment, version) + if err != nil { + t.Fatal("Could not armor the ciphertext:", err) + } + + assert.Contains(t, armored, "Comment: "+comment) + assert.Contains(t, armored, "Version: "+version) +} + +func TestMessageGetArmoredWithEmptyHeaders(t *testing.T) { + var message = NewPlainMessageFromString("plain text") + + ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + comment := "" + version := "" + armored, err := ciphertext.GetArmoredWithCustomHeaders(comment, version) + if err != nil { + t.Fatal("Could not armor the ciphertext:", err) + } + + assert.NotContains(t, armored, "Version") + assert.NotContains(t, armored, "Comment") +} diff --git a/crypto/mime_test.go b/crypto/mime_test.go index a8c1966..5b3577e 100644 --- a/crypto/mime_test.go +++ b/crypto/mime_test.go @@ -6,10 +6,9 @@ import ( "github.com/stretchr/testify/assert" ) -// Corresponding key in testdata/mime_privateKey +// Corresponding key in testdata/mime_privateKey. var MIMEKeyPassword = []byte("test") -// define call back interface type Callbacks struct { Testing *testing.T } diff --git a/helper/base_test.go b/helper/base_test.go index f520c9d..d3fd8c1 100644 --- a/helper/base_test.go +++ b/helper/base_test.go @@ -20,7 +20,7 @@ func readTestFile(name string, trimNewlines bool) string { return string(data) } -// Corresponding key in ../crypto/testdata/keyring_privateKey +// Corresponding key in ../crypto/testdata/keyring_privateKey. var testMailboxPassword = []byte("apple") func init() {