From d3e5c3236c5866099741a7c9776d81fa0aea0aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naoriel=20Sa=27=20Roc=C3=AD?= Date: Fri, 10 May 2024 22:24:14 +0200 Subject: [PATCH] First commit. --- .gitignore | 2 + CHANGELOG.md | 8 + CreateOptions.go | 71 +++++++++ CreateOptions_test.go | 138 +++++++++++++++++ Creation.go | 87 +++++++++++ Creation_test.go | 112 ++++++++++++++ DeCryption.go | 165 ++++++++++++++++++++ DeCryption_test.go | 168 +++++++++++++++++++++ Export.go | 101 +++++++++++++ Export_test.go | 138 +++++++++++++++++ Import.go | 46 ++++++ Import_test.go | 110 ++++++++++++++ LICENSE | 9 ++ PGPError.go | 15 ++ PGPError_test.go | 31 ++++ README.md | 3 + Signatures.go | 104 +++++++++++++ Signatures_test.go | 76 ++++++++++ go.mod | 11 ++ go.sum | 51 +++++++ go_pgp.code-workspace | 9 ++ testdata/testReceiverEntity.pgp | Bin 0 -> 4824 bytes testdata/testReceiverEntityArmored.pgp | 105 +++++++++++++ testdata/testReceiverEntityArmored_bad.pgp | 101 +++++++++++++ testdata/testReceiverEntity_bad.pgp | Bin 0 -> 8533 bytes testdata/testSenderEntity.pgp | Bin 0 -> 4822 bytes testdata/testSenderEntityArmored.pgp | 105 +++++++++++++ 27 files changed, 1766 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CreateOptions.go create mode 100644 CreateOptions_test.go create mode 100644 Creation.go create mode 100644 Creation_test.go create mode 100644 DeCryption.go create mode 100644 DeCryption_test.go create mode 100644 Export.go create mode 100644 Export_test.go create mode 100644 Import.go create mode 100644 Import_test.go create mode 100644 LICENSE create mode 100644 PGPError.go create mode 100644 PGPError_test.go create mode 100644 README.md create mode 100644 Signatures.go create mode 100644 Signatures_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 go_pgp.code-workspace create mode 100644 testdata/testReceiverEntity.pgp create mode 100644 testdata/testReceiverEntityArmored.pgp create mode 100644 testdata/testReceiverEntityArmored_bad.pgp create mode 100644 testdata/testReceiverEntity_bad.pgp create mode 100644 testdata/testSenderEntity.pgp create mode 100644 testdata/testSenderEntityArmored.pgp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b76cbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.log +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6cdef88 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# git.sa-roci.de/oss/go_pgp Release notes + +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## v. 0.0.1 + +- Initial Release. diff --git a/CreateOptions.go b/CreateOptions.go new file mode 100644 index 0000000..ef2fbd1 --- /dev/null +++ b/CreateOptions.go @@ -0,0 +1,71 @@ +package go_pgp + +import ( + "crypto" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +const ( + // The minimal number of RSA bits + MinRSABits = 3072 +) + +type createOption func(cfg *packet.Config) + +func HashOption(h crypto.Hash) createOption { + if !h.Available() { + h = 0 + } + return func(cfg *packet.Config) { + cfg.DefaultHash = h + } +} + +func CipherOption(cipher packet.CipherFunction) createOption { + if !cipher.IsSupported() { + cipher = 0 + } + return func(cfg *packet.Config) { + cfg.DefaultCipher = cipher + } +} + +func RSABitsOption(numBits int) createOption { + if numBits < MinRSABits { + numBits = MinRSABits + } + return func(cfg *packet.Config) { + cfg.RSABits = numBits + } +} + +type CompressionLevel int + +const ( + DefaultCompression CompressionLevel = iota - 1 + NoCompression + CompressionLevel1 + CompressionLevel2 + CompressionLevel3 + CompressionLevel4 + CompressionLevel5 + CompressionLevel6 + CompressionLevel7 + CompressionLevel8 + CompressionLevel9 + compressionLevelOutOfRange +) + +const CompressionLevelMax = compressionLevelOutOfRange - 1 + +func CompressionOption(level CompressionLevel) createOption { + if level < DefaultCompression || level > CompressionLevelMax { + level = CompressionLevel5 + } + return func(cfg *packet.Config) { + cfg.CompressionConfig = &packet.CompressionConfig{ + Level: int(level), + } + } +} diff --git a/CreateOptions_test.go b/CreateOptions_test.go new file mode 100644 index 0000000..f4ed1c5 --- /dev/null +++ b/CreateOptions_test.go @@ -0,0 +1,138 @@ +package go_pgp + +import ( + "crypto" + "math/rand" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +func TestHashOption(t *testing.T) { + testCases := []struct { + in crypto.Hash + }{ + {}, + {crypto.MD4}, + {crypto.MD5}, + {crypto.SHA1}, + {crypto.SHA224}, + {crypto.SHA256}, + {crypto.SHA384}, + {crypto.SHA512}, + {crypto.MD5SHA1}, + {crypto.RIPEMD160}, + {crypto.SHA3_224}, + {crypto.SHA3_256}, + {crypto.SHA3_384}, + {crypto.SHA3_512}, + {crypto.SHA512_224}, + {crypto.SHA512_256}, + {crypto.BLAKE2b_256}, + {crypto.BLAKE2b_384}, + {crypto.BLAKE2b_512}, + } + + for i, testCase := range testCases { + testConfig := defaultConfig + optionFunc := HashOption(testCase.in) + optionFunc(&testConfig) + if testCase.in.Available() { + if testConfig.DefaultHash != testCase.in { + t.Errorf("TestHashOption: test case %d: expected hash %d but got %d", i+1, testCase.in, testConfig.DefaultHash) + } + } else if testConfig.DefaultHash != 0 { + t.Errorf("TestHashOption: test case %d: expected hash 0 but got %d", i+1, testConfig.DefaultHash) + } + } +} + +func TestCipherOption(t *testing.T) { + testCases := []struct { + in packet.CipherFunction + }{ + {}, + {packet.Cipher3DES}, + {packet.CipherCAST5}, + {packet.CipherAES128}, + {packet.CipherAES192}, + {packet.CipherAES256}, + } + + for i, testCase := range testCases { + testConfig := defaultConfig + optionFunc := CipherOption(testCase.in) + optionFunc(&testConfig) + if testCase.in.IsSupported() { + if testConfig.DefaultCipher != testCase.in { + t.Errorf("TestCipherOption: test case %d: expected cipher %d but got %d", i+1, testCase.in, testConfig.DefaultCipher) + } + } else if testConfig.DefaultCipher != 0 { + t.Errorf("TestCipherOption: test case %d: expected cipher 0 but got %d", i+1, testConfig.DefaultCipher) + } + } +} + +func TestRSABitsOption(t *testing.T) { + testCases := []struct { + in int + }{ + {}, + {1}, + {MinRSABits - 1}, + {MinRSABits}, + {MinRSABits + 1}, + {rand.Intn(10480)}, + {rand.Intn(10480)}, + {rand.Intn(10480)}, + {rand.Intn(10480)}, + {rand.Intn(10480)}, + } + + for i, testCase := range testCases { + testConfig := defaultConfig + optionFunc := RSABitsOption(testCase.in) + optionFunc(&testConfig) + if testCase.in > MinRSABits { + if testConfig.RSABits != testCase.in { + t.Errorf("TestRSABitsOption: test case %d: expected %d RSA bits but got %d", i+1, testCase.in, testConfig.DefaultCipher) + } + } else if testConfig.RSABits != MinRSABits { + t.Errorf("TestRSABitsOption: test case %d: expected %d RSA bits but got %d", i+1, MinRSABits, testConfig.DefaultCipher) + } + } +} + +func TestCompressionOption(t *testing.T) { + testCases := []struct { + in int + }{ + {-1}, + {0}, + {1}, + {2}, + {3}, + {4}, + {5}, + {6}, + {7}, + {8}, + {9}, + {10}, + } + + for i, testCase := range testCases { + testConfig := defaultConfig + optionFunc := CompressionOption(CompressionLevel(testCase.in)) + optionFunc(&testConfig) + if testCase.in >= int(DefaultCompression) && testCase.in <= int(CompressionLevelMax) { + if testConfig.CompressionConfig == nil { + t.Errorf("TestCompressionOption: test case %d: the compression is not set", i+1) + } else if testConfig.CompressionConfig.Level != testCase.in { + t.Errorf("TestCompressionOption: test case %d: expected compression level %d but got %d", i+1, testCase.in, testConfig.CompressionConfig.Level) + } + } else if testConfig.CompressionConfig.Level != int(CompressionLevel5) { + t.Errorf("TestCompressionOption: test case %d: expected compression level %d but got %d", i+1, int(CompressionLevel5), testConfig.CompressionConfig.Level) + } + } +} diff --git a/Creation.go b/Creation.go new file mode 100644 index 0000000..91e4d65 --- /dev/null +++ b/Creation.go @@ -0,0 +1,87 @@ +package go_pgp + +import ( + "crypto" + "errors" + "fmt" + + // required in order to use the crypto.SHA256- and -SHA512-Hashes + _ "crypto/sha256" + _ "crypto/sha512" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +var ( + defaultConfig = packet.Config{ + RSABits: 4096, + DefaultHash: crypto.SHA512, + DefaultCompressionAlgo: packet.CompressionZIP, + DefaultCipher: packet.CipherAES256, + CompressionConfig: &packet.CompressionConfig{ + Level: 5, + }, + } + + // ErrUndefinedPGPEntity defines the error for a null-reference to an openpgp.Entity + ErrUndefinedPGPEntity = errors.New("pgp entity undefined") + + // ErrNoData defines the error for missing processable data + ErrNoData = errors.New("no data to process") +) + +type Entity struct { + openpgp.Entity + cfg *packet.Config +} + +// SetPassword encrypts all contained unencrypted private keys +// using the given passphrase. +func (e *Entity) SetPassword(passphrase []byte) error { + return e.Entity.EncryptPrivateKeys(passphrase, e.cfg) +} + +// CreatePGPEntity creates an OpenPGP Entity with only a name given +func CreatePGPEntity(name string, options ...createOption) (*Entity, error) { + return createPGPEntity(name, "", "", options...) +} + +// CreatePGPEntityEmail creates an OpenPGP Entity with a name and email given +func CreatePGPEntityEmail(name, email string, options ...createOption) (*Entity, error) { + return createPGPEntity(name, "", email, options...) +} + +// CreateCommentedPGPEntity creates an OpenPGP Entity with a name and comment given +func CreateCommentedPGPEntity(name, comment string, options ...createOption) (entity *Entity, err error) { + return createPGPEntity(name, comment, "", options...) +} + +// CreateCommentedPGPEntity creates an OpenPGP Entity with a name and comment given +func createPGPEntity(name, comment, email string, options ...createOption) (entity *Entity, err error) { + if name == "" && email == "" { + return nil, fmt.Errorf("name or email must be specified") + } + + cfg := defaultConfig + for _, option := range options { + if option != nil { + option(&cfg) + } + } + + var e *openpgp.Entity + e, err = openpgp.NewEntity(name, comment, email, &cfg) + if nil == err { + for _, identity := range e.Identities { + if nil != identity && nil != identity.SelfSignature { + identity.SelfSignature.PreferredHash = []uint8{8, 9, 10} //cf. "golang.org/x/crypto/openpgp/s2k" -> s2k.HashIdToHash + } + } + entity = &Entity{ + Entity: *e, + cfg: &cfg, + } + } + return +} diff --git a/Creation_test.go b/Creation_test.go new file mode 100644 index 0000000..95489fc --- /dev/null +++ b/Creation_test.go @@ -0,0 +1,112 @@ +package go_pgp + +import ( + "testing" +) + +const ( + testPassword = "p4ssw0rd" +) + +func TestCreatePGPEntity(t *testing.T) { + entity, err := CreatePGPEntity("") + if err == nil || entity != nil { + t.Error("TestCreatePGPEntity: test case 1: unexpectedly succeeded in creating an unnamed entity") + } + + entity, err = CreatePGPEntity("Me", RSABitsOption(0)) + if err != nil { + t.Fatal("TestCreatePGPEntity: test case 2.1: received an error, trying to create a named entity") + } else if entity == nil { + t.Fatal("TestCreatePGPEntity: test case 2.1: failed to create a named entity") + } + if entity.PrimaryIdentity().Name != "Me" { + t.Errorf("TestCreatePGPEntity: test case 2.2: expected identity name 'Me' but got '%s'", entity.PrimaryIdentity().Name) + } +} + +func TestSetPassword(t *testing.T) { + entity, _ := CreatePGPEntity("Me", RSABitsOption(0)) + if entity.PrivateKey.Encrypted { + t.Fatal("TestSetPassword: test case 1: private key should not be encrypted yet") + } + + err := entity.SetPassword([]byte(testPassword)) + if err != nil || !entity.PrivateKey.Encrypted { + t.Fatal("TestSetPassword: test case 2: encryption of private key failed") + } + + err = entity.SetPassword([]byte(testPassword + "2")) + if err != nil || !entity.PrivateKey.Encrypted { + t.Fatal("TestSetPassword: test case 3: second encryption of private key failed") + } +} + +func TestCreatePGPEntityEmail(t *testing.T) { + entity, err := CreatePGPEntityEmail("", "") + if err == nil || entity != nil { + t.Error("TestCreatePGPEntityEmail: test case 1: unexpectedly succeeded in creating an unnamed entity") + } + + expectedName := "" + entity, err = CreatePGPEntityEmail("", "Jenkins@heye-international.com") + if err != nil { + t.Fatal("TestCreatePGPEntityEmail: test case 2.1: received an error, trying to create a named entity") + } else if entity == nil { + t.Fatal("TestCreatePGPEntityEmail: test case 2.1: failed to create a named entity") + } + if name := entity.PrimaryIdentity().Name; name != expectedName { + t.Errorf("TestCreatePGPEntityEmail: test case 2.2: expected identity name '%s' but got '%s'", expectedName, name) + } + + expectedName = "Me " + entity, err = CreatePGPEntityEmail("Me", "Jenkins@heye-international.com") + if err != nil { + t.Fatal("TestCreatePGPEntityEmail: test case 3.1: received an error, trying to create a named entity") + } else if entity == nil { + t.Fatal("TestCreatePGPEntityEmail: test case 3.1: failed to create a named entity") + } + if name := entity.PrimaryIdentity().Name; name != expectedName { + t.Errorf("TestCreatePGPEntityEmail: test case 3.2: expected identity name '%s' but got '%s'", expectedName, name) + } + + expectedName = "Me" + entity, err = CreatePGPEntityEmail("Me", "") + if err != nil { + t.Fatal("TestCreatePGPEntityEmail: test case 4.1: received an error, trying to create a named entity") + } else if entity == nil { + t.Fatal("TestCreatePGPEntityEmail: test case 4.1: failed to create a named entity") + } + if name := entity.PrimaryIdentity().Name; name != expectedName { + t.Errorf("TestCreatePGPEntityEmail: test case 4.2: expected identity name '%s' but got '%s'", expectedName, name) + } +} + +func TestCreateCommentedPGPEntity(t *testing.T) { + entity, err := CreateCommentedPGPEntity("", "myComment") + if err == nil || entity != nil { + t.Error("TestCreateCommentedPGPEntity: test case 1: unexpectedly succeeded in creating an unnamed entity") + } + + expectedName := "Me (myComment)" + entity, err = CreateCommentedPGPEntity("Me", "myComment") + if err != nil { + t.Fatal("TestCreateCommentedPGPEntity: test case 2.1: received an error, trying to create a named entity") + } else if entity == nil { + t.Fatal("TestCreateCommentedPGPEntity: test case 2.1: failed to create a named entity") + } + if name := entity.PrimaryIdentity().Name; name != expectedName { + t.Errorf("TestCreateCommentedPGPEntity: test case 2.2: expected identity name '%s' but got '%s'", expectedName, name) + } + + expectedName = "Me" + entity, err = CreateCommentedPGPEntity("Me", "") + if err != nil { + t.Fatal("TestCreateCommentedPGPEntity: test case 3.1: received an error, trying to create a named entity") + } else if entity == nil { + t.Fatal("TestCreateCommentedPGPEntity: test case 3.1: failed to create a named entity") + } + if name := entity.PrimaryIdentity().Name; name != expectedName { + t.Errorf("TestCreateCommentedPGPEntity: test case 3.2: expected identity name '%s' but got '%s'", expectedName, name) + } +} diff --git a/DeCryption.go b/DeCryption.go new file mode 100644 index 0000000..56d7506 --- /dev/null +++ b/DeCryption.go @@ -0,0 +1,165 @@ +package go_pgp + +import ( + "bytes" + "crypto" + "errors" + "hash" + "io" + "strconv" + + // required in order to use the crypto.SHA256- and -SHA512-Hashes + _ "crypto/sha256" + _ "crypto/sha512" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +type FileHints struct { + openpgp.FileHints +} + +type EntityList struct { + openpgp.EntityList +} + +// EncryptData encrypts a given payload with the public key of the target entity and optionally signs the data with the private key of the source entity +func EncryptData(data []byte, source, target *Entity, hints *FileHints) (encryptedData []byte, err error) { + if nil == target { + err = ErrUndefinedPGPEntity + return + } + if len(data) == 0 { + return data, nil + } + out := new(bytes.Buffer) + for _, identity := range target.Identities { + if nil != identity && nil != identity.SelfSignature { + identity.SelfSignature.PreferredHash = []uint8{8, 9, 10} //cf. "golang.org/x/crypto/openpgp/s2k" -> s2k.HashIdToHash + } + } + if hints == nil { + hints = &FileHints{} + } + var writer io.WriteCloser + if source != nil { + writer, err = openpgp.Encrypt(out, []*openpgp.Entity{&target.Entity}, &source.Entity, &hints.FileHints, target.cfg) + } else { + writer, err = openpgp.Encrypt(out, []*openpgp.Entity{&target.Entity}, nil, &hints.FileHints, target.cfg) + } + if nil != err { + return + } + _, err = writer.Write(data) + if nil != err { + return + } + writer.Close() + encryptedData = out.Bytes() + return +} + +// DecryptData decrypts a given payload with the private key of the given entity +func DecryptData(data []byte, entity *Entity, entityPassword *string) (decryptedData []byte, err error) { + return DecryptDataVerify(data, entity, nil, entityPassword) +} + +// DecryptDataVerify decrypts a given payload with the private key of the given entity +func DecryptDataVerify(data []byte, entity *Entity, remoteKeys *EntityList, entityPassword *string) (decryptedData []byte, err error) { + if nil == entity { + return nil, ErrUndefinedPGPEntity + } + if len(data) == 0 { + return nil, ErrNoData + } + if nil != entityPassword && 0 < len(*entityPassword) { + passphraseByte := []byte(*entityPassword) + if entity.PrivateKey.Encrypted { + entity.PrivateKey.Decrypt(passphraseByte) + } + if nil != entity.Subkeys { + for _, subkey := range entity.Subkeys { + if subkey.PrivateKey.Encrypted { + subkey.PrivateKey.Decrypt(passphraseByte) + } + } + } + } + keyring := openpgp.EntityList{&entity.Entity} + if remoteKeys != nil { + if el := remoteKeys.EntityList; len(el) > 0 { + keyring = append(keyring, el...) + } + } + message, err := openpgp.ReadMessage(bytes.NewBuffer(data), keyring, nil, entity.cfg) + if nil != err { + return + } + unverifiedBody, bodyErr := io.ReadAll(message.UnverifiedBody) + if bodyErr == nil && remoteKeys != nil && len(remoteKeys.EntityList) > 0 && message.IsSigned { + var keys []openpgp.Key + var signatureType packet.SignatureType + var hashFunc crypto.Hash + packetType := 0 + if nil != message.Signature { + if nil != message.Signature.IssuerKeyId { + keys = remoteKeys.KeysById(*message.Signature.IssuerKeyId) + hashFunc = message.Signature.Hash + signatureType = message.Signature.SigType + if 0 < len(keys) { + packetType = 1 + } + } + } + if hashFunc == 0 && packetType == 0 && len(keys) == 0 { + if keys = remoteKeys.KeysById(message.SignedByKeyId); len(keys) > 0 { + return unverifiedBody, nil + } + } + h, wrappedHash, err := hashForSignature(hashFunc, signatureType) + if err != nil { + return nil, &PGPError{err} + } + + if _, err := wrappedHash.Write(unverifiedBody); err != nil && err != io.EOF { + return nil, &PGPError{err} + } + for _, key := range keys { + switch packetType { + case 1: + err = key.PublicKey.VerifySignature(h, message.Signature) + default: + return nil, &PGPError{errors.New("bad signature")} + } + + if err == nil { + return unverifiedBody, nil + } + } + + return nil, &PGPError{err} + } + return unverifiedBody, bodyErr +} + +// hashForSignature returns a pair of hashes that can be used to verify a +// signature. The signature may specify that the contents of the signed message +// should be preprocessed (i.e. to normalize line endings). Thus this function +// returns two hashes. The second should be used to hash the message itself and +// performs any needed preprocessing. +func hashForSignature(hashID crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) { + if !hashID.Available() { + return nil, nil, errors.New("hash not available: " + strconv.Itoa(int(hashID))) + } + h := hashID.New() + + switch sigType { + case packet.SigTypeBinary: + return h, h, nil + case packet.SigTypeText: + return h, openpgp.NewCanonicalTextHash(h), nil + } + + return nil, nil, errors.New("unsupported signature type: " + strconv.Itoa(int(sigType))) +} diff --git a/DeCryption_test.go b/DeCryption_test.go new file mode 100644 index 0000000..feb4527 --- /dev/null +++ b/DeCryption_test.go @@ -0,0 +1,168 @@ +package go_pgp + +import ( + "bytes" + "crypto" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/packet" +) + +var ( + initialised bool + initMutex sync.Mutex + testSenderEntity *Entity + testReceiverEntity *Entity +) + +func readTestFile(name string) ([]byte, error) { + folder, err := filepath.Abs("testdata") + if err != nil { + return nil, err + } + return os.ReadFile(filepath.Join(folder, name)) +} + +func initTest() { + initMutex.Lock() + defer initMutex.Unlock() + + if !initialised { + armoredBinary, _ := readTestFile("testReceiverEntityArmored.pgp") + list, _ := ReadArmoredKeyRingBinary(armoredBinary) + receiverCfg := defaultConfig + testReceiverEntity = &Entity{*list.EntityList[0], &receiverCfg} + + armoredBinary, _ = readTestFile("testSenderEntityArmored.pgp") + list, _ = ReadArmoredKeyRingBinary(armoredBinary) + senderCfg := defaultConfig + testSenderEntity = &Entity{*list.EntityList[0], &senderCfg} + + initialised = true + } +} + +func TestEncryptData(t *testing.T) { + initTest() + decryptionPassword := testPassword + testData := []byte("myTestData") + encrypted, err := EncryptData(testData, testSenderEntity, testSenderEntity, nil) + if err != nil || len(encrypted) == 0 { + t.Errorf("TestEncryptData: test case 1: failed to encrypt data") + } + + decrypted, err := DecryptData(encrypted, testSenderEntity, &decryptionPassword) + if err != nil || len(decrypted) == 0 { + t.Errorf("TestEncryptData: test case 2.1: failed to decrypt data") + } else if !bytes.Equal(testData, decrypted) { + t.Errorf("TestEncryptData: test case 2.2: expected decrypted data '%s' but got '%s'", string(testData), string(decrypted)) + } + + encrypted, err = EncryptData(testData, testSenderEntity, testReceiverEntity, nil) + if err != nil || len(encrypted) == 0 { + t.Errorf("TestEncryptData: test case 3: failed to encrypt data") + } + + decrypted, err = DecryptData(encrypted, testReceiverEntity, &decryptionPassword) + if err != nil || len(decrypted) == 0 { + t.Errorf("TestEncryptData: test case 4.1: failed to decrypt data") + } else if !bytes.Equal(testData, decrypted) { + t.Errorf("TestEncryptData: test case 4.2: expected decrypted data '%s' but got '%s'", string(testData), string(decrypted)) + } + + decrypted, err = DecryptDataVerify(encrypted, testReceiverEntity, &EntityList{EntityList: openpgp.EntityList{&testSenderEntity.Entity}}, &decryptionPassword) + if err != nil || len(decrypted) == 0 { + t.Errorf("TestEncryptData: test case 5.1: failed to decrypt data") + } else if !bytes.Equal(testData, decrypted) { + t.Errorf("TestEncryptData: test case 5.2: expected decrypted data '%s' but got '%s'", string(testData), string(decrypted)) + } + + decrypted, err = DecryptData(encrypted, testSenderEntity, &decryptionPassword) + if err == nil || len(decrypted) > 0 { + t.Errorf("TestEncryptData: test case 6: unexpectedly succeeded to decrypt data") + } + + decrypted, err = DecryptDataVerify(encrypted, testSenderEntity, &EntityList{EntityList: openpgp.EntityList{&testReceiverEntity.Entity}}, &decryptionPassword) + if err == nil || len(decrypted) > 0 { + t.Errorf("TestEncryptData: test case 7: unexpectedly succeeded to decrypt data") + } + + encrypted, err = EncryptData(testData, nil, testReceiverEntity, nil) + if err != nil || len(encrypted) == 0 { + t.Errorf("TestEncryptData: test case 8: failed to encrypt data") + } + + decrypted, err = DecryptData(encrypted, testReceiverEntity, &decryptionPassword) + if err != nil || len(decrypted) == 0 { + t.Errorf("TestEncryptData: test case 9.1: failed to decrypt data") + } else if !bytes.Equal(testData, decrypted) { + t.Errorf("TestEncryptData: test case 9.2: expected decrypted data '%s' but got '%s'", string(testData), string(decrypted)) + } + + decrypted, err = DecryptDataVerify(encrypted, testReceiverEntity, &EntityList{EntityList: openpgp.EntityList{&testSenderEntity.Entity}}, &decryptionPassword) + if err != nil || len(decrypted) == 0 { + t.Errorf("TestEncryptData: test case 10.1: failed to decrypt data") + } else if !bytes.Equal(testData, decrypted) { + t.Errorf("TestEncryptData: test case 10.2: expected decrypted data '%s' but got '%s'", string(testData), string(decrypted)) + } + + // Error cases + _, err = EncryptData(testData, nil, nil, nil) + if err == nil { + t.Errorf("TestEncryptData: test case 11: should fail to encrypt data for undefined entity") + } + + encrypted, err = EncryptData(nil, nil, testReceiverEntity, nil) + if err != nil || encrypted != nil { + t.Errorf("TestEncryptData: test case 12: should not encrypt nil data and return no error") + } + + encrypted, err = EncryptData([]byte{}, nil, testReceiverEntity, nil) + if err != nil || encrypted == nil || len(encrypted) != 0 { + t.Errorf("TestEncryptData: test case 13: should return empty data and no error") + } +} + +func TestHashForSignature(t *testing.T) { + testCases := []struct { + hashID crypto.Hash + signatureType packet.SignatureType + expectError bool + }{ + { + hashID: crypto.MD4, + signatureType: 0, + expectError: true, + }, + { + hashID: crypto.SHA256, + signatureType: packet.SigTypeBinary, + expectError: false, + }, + { + hashID: crypto.SHA256, + signatureType: packet.SigTypeText, + expectError: false, + }, + { + hashID: crypto.SHA256, + signatureType: packet.SigTypeCasualCert, + expectError: true, + }, + } + + for i, testCase := range testCases { + _, _, err := hashForSignature(testCase.hashID, testCase.signatureType) + if err != nil { + if !testCase.expectError { + t.Errorf("TestHashForSignature: test case %d: unexpected error '%s'", i+1, err) + } + } else if testCase.expectError { + t.Errorf("TestHashForSignature: test case %d: expected error did not occur", i+1) + } + } +} diff --git a/Export.go b/Export.go new file mode 100644 index 0000000..0dc4054 --- /dev/null +++ b/Export.go @@ -0,0 +1,101 @@ +package go_pgp + +import ( + "bytes" + "io" + "os" + + // required in order to use the crypto.SHA256- and -SHA512-Hashes + _ "crypto/sha256" + _ "crypto/sha512" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" +) + +func writeKeyDataToFile(data []byte, filePath, dataType string, armored bool) error { + out, err := os.Create(filePath) + if nil != err { + return err + } + defer out.Close() + if !armored { + _, err = out.Write(data) + if nil == err { + out.Sync() + } + } else { + var armored io.WriteCloser + armored, err = armor.Encode(out, dataType, nil) + if nil != err { + return err + } + + _, err = armored.Write(data) + if nil == err { + armored.Close() + out.Sync() + } + } + return err +} + +// WritePublicEntityData will write the entitys complete public data to a file +func WritePublicEntityData(entity *Entity, filePath string) error { + data, err := ExtractPublicEntityData(entity) + if nil != err { + return err + } + + return writeKeyDataToFile(data, filePath, openpgp.PublicKeyType, false) +} + +// WriteArmoredPublicEntityData will write the entitys complete public data to an armored file +func WriteArmoredPublicEntityData(entity *Entity, filePath string) error { + data, err := ExtractPublicEntityData(entity) + if nil != err { + return err + } + + return writeKeyDataToFile(data, filePath, openpgp.PublicKeyType, true) +} + +// WritePrivateEntityData will write the entitys public and private keys to a file +func WritePrivateEntityData(entity *Entity, filePath string) error { + data, err := ExtractPrivateEntityData(entity) + if nil != err { + return err + } + + return writeKeyDataToFile(data, filePath, openpgp.PrivateKeyType, false) +} + +// WriteArmoredPrivateEntityData will write the entitys public and private keys to an armored file +func WriteArmoredPrivateEntityData(entity *Entity, filePath string) error { + data, err := ExtractPrivateEntityData(entity) + if nil != err { + return err + } + + return writeKeyDataToFile(data, filePath, openpgp.PrivateKeyType, true) +} + +// ExtractPublicEntityData will write the entitys complete public data to a []byte +func ExtractPublicEntityData(entity *Entity) ([]byte, error) { + if nil == entity { + return nil, ErrUndefinedPGPEntity + } + out := new(bytes.Buffer) + entity.Serialize(out) + return out.Bytes(), nil +} + +// ExtractPrivateEntityData will write the entitys public and private keys to a []byte +func ExtractPrivateEntityData(entity *Entity) ([]byte, error) { + if nil == entity { + return nil, ErrUndefinedPGPEntity + } + out := new(bytes.Buffer) + entity.SerializePrivate(out, &defaultConfig) + return out.Bytes(), nil +} diff --git a/Export_test.go b/Export_test.go new file mode 100644 index 0000000..80431c4 --- /dev/null +++ b/Export_test.go @@ -0,0 +1,138 @@ +package go_pgp + +import ( + "path/filepath" + "testing" +) + +func TestWritePublicEntityData(t *testing.T) { + initTest() + err := WritePublicEntityData(testSenderEntity, "ä/ö/ü.@") + if err == nil { + t.Errorf("TestWritePublicEntityData: test case 1: the file 'ä/ö/ü.@' should not have been written") + t.FailNow() + } + + filePath := filepath.Join(t.TempDir(), "public.pgp") + err = WritePublicEntityData(testSenderEntity, filePath) + if err != nil { + t.Errorf("TestWritePublicEntityData: test case 2: failed to write public entity data") + t.FailNow() + } + + el, err := ReadKeyRingFromFile(filePath) + if err != nil || len(el.EntityList) == 0 { + t.Errorf("TestWritePublicEntityData: test case 3: failed to read public entity data") + t.FailNow() + } + + importedIdentity := el.EntityList[0].PrimaryIdentity() + originalIdentity := testSenderEntity.PrimaryIdentity() + if importedIdentity.Name != originalIdentity.Name { + t.Errorf("TestWritePublicEntityData: test case 4: expected primary identity name '%s' but got '%s'", originalIdentity.Name, importedIdentity.Name) + } + + if err = WritePublicEntityData(nil, filePath); err == nil { + t.Error("TestWritePublicEntityData: test case 5: an undefined entity shoud not be exportable") + } +} + +func TestWritePrivateEntityData(t *testing.T) { + initTest() + filePath := filepath.Join(t.TempDir(), "private.pgp") + err := WritePrivateEntityData(testSenderEntity, filePath) + if err != nil { + t.Errorf("TestWritePrivateEntityData: test case 1: failed to write public entity data") + t.FailNow() + } + + el, err := ReadKeyRingFromFile(filePath) + if err != nil || len(el.EntityList) == 0 { + t.Errorf("TestWritePrivateEntityData: test case 2: failed to read public entity data") + t.FailNow() + } + + entity := el.EntityList[0] + importedIdentity := entity.PrimaryIdentity() + originalIdentity := testSenderEntity.PrimaryIdentity() + if importedIdentity.Name != originalIdentity.Name { + t.Errorf("TestWritePrivateEntityData: test case 3: expected primary identity name '%s' but got '%s'", originalIdentity.Name, importedIdentity.Name) + } + + if entity.PrivateKey == nil { + t.Errorf("TestWritePrivateEntityData: test case 4: private key is missing") + } + + if err = WritePrivateEntityData(nil, filePath); err == nil { + t.Error("TestWritePrivateEntityData: test case 5: an undefined entity shoud not be exportable") + } +} + +func TestWriteArmoredPublicEntityData(t *testing.T) { + initTest() + filePath := filepath.Join(t.TempDir(), "public.pgp") + err := WriteArmoredPublicEntityData(testSenderEntity, filePath) + if err != nil { + t.Errorf("TestWriteArmoredPublicEntityData: test case 1: failed to write public entity data") + t.FailNow() + } + + el, err := ReadArmoredKeyRingFromFile(filePath) + if err != nil || len(el.EntityList) == 0 { + t.Errorf("TestWriteArmoredPublicEntityData: test case 2: failed to read public entity data") + t.FailNow() + } + + importedIdentity := el.EntityList[0].PrimaryIdentity() + originalIdentity := testSenderEntity.PrimaryIdentity() + if importedIdentity.Name != originalIdentity.Name { + t.Errorf("TestWriteArmoredPublicEntityData: test case 3: expected primary identity name '%s' but got '%s'", originalIdentity.Name, importedIdentity.Name) + } + + if err = WriteArmoredPublicEntityData(nil, filePath); err == nil { + t.Error("TestWriteArmoredPublicEntityData: test case 4: an undefined entity shoud not be exportable") + } +} + +func TestWriteArmoredPrivateEntityData(t *testing.T) { + initTest() + filePath := filepath.Join(t.TempDir(), "private.pgp") + err := WriteArmoredPrivateEntityData(testSenderEntity, filePath) + if err != nil { + t.Errorf("TestWriteArmoredPrivateEntityData: test case 1: failed to write public entity data") + t.FailNow() + } + + el, err := ReadArmoredKeyRingFromFile(filePath) + if err != nil || len(el.EntityList) == 0 { + t.Errorf("TestWriteArmoredPrivateEntityData: test case 2: failed to read public entity data") + t.FailNow() + } + + entity := el.EntityList[0] + importedIdentity := entity.PrimaryIdentity() + originalIdentity := testSenderEntity.PrimaryIdentity() + if importedIdentity.Name != originalIdentity.Name { + t.Errorf("TestWriteArmoredPrivateEntityData: test case 3: expected primary identity name '%s' but got '%s'", originalIdentity.Name, importedIdentity.Name) + } + + if entity.PrivateKey == nil { + t.Errorf("TestWriteArmoredPrivateEntityData: test case 4: private key is missing") + } + + if err = WriteArmoredPrivateEntityData(nil, filePath); err == nil { + t.Error("TestWriteArmoredPrivateEntityData: test case 5: an undefined entity shoud not be exportable") + } +} + +func TestExtractPublicEntityData(t *testing.T) { + if _, err := ExtractPublicEntityData(nil); err == nil { + t.Errorf("TestExtractPublicEntityData: test case 1: no key should be extractable from a 'nil' entity") + } +} + +func TestExtractPrivateEntityData(t *testing.T) { + if _, err := ExtractPrivateEntityData(nil); err == nil { + t.Errorf("TestExtractPrivateEntityData: test case 1: no key should be extractable from a 'nil' entity") + } +} diff --git a/Import.go b/Import.go new file mode 100644 index 0000000..4d702b7 --- /dev/null +++ b/Import.go @@ -0,0 +1,46 @@ +package go_pgp + +import ( + "bytes" + "os" + + "github.com/ProtonMail/go-crypto/openpgp" +) + +func convertImportResult(el openpgp.EntityList, err error) (EntityList, error) { + if err != nil { + return EntityList{}, err + } + result := EntityList{ + EntityList: el, + } + return result, nil +} + +func ReadArmoredKeyRingBinary(data []byte) (EntityList, error) { + r := bytes.NewReader(data) + return convertImportResult(openpgp.ReadArmoredKeyRing(r)) +} + +func ReadKeyRingBinary(data []byte) (EntityList, error) { + r := bytes.NewReader(data) + return convertImportResult(openpgp.ReadKeyRing(r)) +} + +func ReadArmoredKeyRingFromFile(filepath string) (EntityList, error) { + file, err := os.Open(filepath) + if err != nil { + return EntityList{}, err + } + defer file.Close() + return convertImportResult(openpgp.ReadArmoredKeyRing(file)) +} + +func ReadKeyRingFromFile(filepath string) (EntityList, error) { + file, err := os.Open(filepath) + if err != nil { + return EntityList{}, err + } + defer file.Close() + return convertImportResult(openpgp.ReadKeyRing(file)) +} diff --git a/Import_test.go b/Import_test.go new file mode 100644 index 0000000..5d082ee --- /dev/null +++ b/Import_test.go @@ -0,0 +1,110 @@ +package go_pgp + +import ( + "path/filepath" + "testing" +) + +func TestReadArmoredKeyRingBinary(t *testing.T) { + binary, err := readTestFile("testReceiverEntityArmored.pgp") + if err != nil { + t.Fatal("'testdata/testReceiverEntityArmored.pgp' could not be found") + } + + entity, err := ReadArmoredKeyRingBinary(binary) + if err != nil { + t.Fatal("'testdata/testReceiverEntityArmored.pgp' could not be parsed") + } + + if len(entity.EntityList) == 0 { + t.Fatal("no entities could be retrieved from 'testdata/testReceiverEntityArmored.pgp'") + } + + binary, err = readTestFile("testReceiverEntityArmored_bad.pgp") + if err != nil { + t.Fatal("'testdata/testReceiverEntityArmored_bad.pgp' could not be found") + } + + entity, err = ReadArmoredKeyRingBinary(binary) + if err == nil { + t.Fatal("'testdata/testReceiverEntityArmored_bad.pgp' should not be parseable") + } +} + +func TestReadKeyRingBinary(t *testing.T) { + binary, err := readTestFile("testReceiverEntity.pgp") + if err != nil { + t.Fatal("'testdata/testReceiverEntity.pgp' could not be found") + } + + entity, err := ReadKeyRingBinary(binary) + if err != nil { + t.Fatal("'testdata/testReceiverEntity.pgp' could not be parsed") + } + + if len(entity.EntityList) == 0 { + t.Fatal("no entities could be retrieved from 'testdata/testReceiverEntity.pgp'") + } + + binary, err = readTestFile("testReceiverEntity_bad.pgp") + if err != nil { + t.Fatal("'testdata/testReceiverEntity_bad.pgp' could not be found") + } + + entity, err = ReadKeyRingBinary(binary) + if err == nil { + t.Fatal("'testdata/testReceiverEntity_bad.pgp' should not be parseable") + } +} + +func TestReadArmoredKeyRingFromFile(t *testing.T) { + folder, err := filepath.Abs("testdata") + if err != nil { + t.Fatal("absolute path to 'testdata' folder could not be determined") + } + + entity, err := ReadArmoredKeyRingFromFile(filepath.Join(folder, "testReceiverEntityArmored.pgp")) + if err != nil { + t.Fatal("'testdata/testReceiverEntityArmored.pgp' could not be parsed") + } + + if len(entity.EntityList) == 0 { + t.Fatal("no entities could be retrieved from 'testdata/testReceiverEntityArmored.pgp'") + } + + _, err = ReadArmoredKeyRingFromFile(filepath.Join(folder, "testReceiverEntityArmored_bad.pgp")) + if err == nil { + t.Error("'testdata/testReceiverEntityArmored_bad.pgp' should not be parseable") + } + + _, err = ReadArmoredKeyRingFromFile(filepath.Join(folder, "doesNotExist.pgp")) + if err == nil { + t.Error("'testdata/doesNotExist.pgp' should not be found") + } +} + +func TestReadKeyRingFromFile(t *testing.T) { + folder, err := filepath.Abs("testdata") + if err != nil { + t.Fatal("absolute path to 'testdata' folder could not be determined") + } + + entity, err := ReadKeyRingFromFile(filepath.Join(folder, "testReceiverEntity.pgp")) + if err != nil { + t.Fatal("'testdata/testReceiverEntity.pgp' could not be parsed") + } + + if len(entity.EntityList) == 0 { + t.Fatal("no entities could be retrieved from 'testdata/testReceiverEntity.pgp'") + } + + _, err = ReadKeyRingFromFile(filepath.Join(folder, "testReceiverEntity_bad.pgp")) + if err == nil { + t.Error("'testdata/testReceiverEntity_bad.pgp' should not be parseable") + } + + _, err = ReadKeyRingFromFile(filepath.Join(folder, "doesNotExist.pgp")) + if err == nil { + t.Error("'testdata/doesNotExist.pgp' should not be found") + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..973b652 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2024 Sa Rocí Solutions + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/PGPError.go b/PGPError.go new file mode 100644 index 0000000..3cc642f --- /dev/null +++ b/PGPError.go @@ -0,0 +1,15 @@ +package go_pgp + +const pgpErrorPrefix = "failed to verify PGP signature: " + +type PGPError struct { + InternalError error +} + +func (err *PGPError) Error() string { + return pgpErrorPrefix + err.InternalError.Error() +} + +func (err *PGPError) Unwrap() error { + return err.InternalError +} diff --git a/PGPError_test.go b/PGPError_test.go new file mode 100644 index 0000000..901bcfd --- /dev/null +++ b/PGPError_test.go @@ -0,0 +1,31 @@ +package go_pgp + +import ( + "errors" + "fmt" + "io" + "testing" +) + +func TestPGPErrorError(t *testing.T) { + testCases := []struct{ err error }{ + { + err: fmt.Errorf("aarrr!!!"), + }, + } + + for i, testCase := range testCases { + err := PGPError{testCase.err} + expected := fmt.Sprintf("%s%s", pgpErrorPrefix, testCase.err.Error()) + if result := err.Error(); result != expected { + t.Errorf("TestPGPErrorError: test case %d: expected '%s' but got '%s'", i+1, expected, result) + } + } +} + +func TestPGPErrorUnwrap(t *testing.T) { + tester := &PGPError{io.EOF} + if !errors.Is(tester, io.EOF) { + t.Error("TestPGPErrorError: test case 1: Unwrapping failed") + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..616c22b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# git.sa-roci.de/oss/go_pgp + +A convenience wrapper module for the Golang PGP implementation of github.com/ProtonMail/go-crypto, which is published under the BSD-3-Clause license. diff --git a/Signatures.go b/Signatures.go new file mode 100644 index 0000000..3b0325e --- /dev/null +++ b/Signatures.go @@ -0,0 +1,104 @@ +package go_pgp + +import ( + "bytes" + "errors" + "io" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" +) + +func CreateSignature(data []byte, signer *Entity) ([]byte, error) { + if signer == nil { + return nil, errors.New("signer undefined") + } + var result []byte + var err error + + buf := bytes.NewBuffer([]byte{}) + + err = openpgp.ArmoredDetachSign(buf, &signer.Entity, bytes.NewBuffer(data), signer.cfg) + + if err == nil { + result = buf.Bytes() + } + + return result, err +} + +func CreateTextSignature(text string, signer *Entity) ([]byte, error) { + if signer == nil { + return nil, errors.New("signer undefined") + } + var result []byte + var err error + + buf := bytes.NewBuffer([]byte{}) + + err = openpgp.DetachSignText(buf, &signer.Entity, bytes.NewBufferString(text), signer.cfg) + + if err == nil { + result = buf.Bytes() + } + + return result, err +} + +func CreateArmoredSignature(data []byte, signer *Entity) (string, error) { + if signer == nil { + return "", errors.New("signer undefined") + } + result := "" + var err error + + buf := bytes.NewBufferString("") + + err = openpgp.ArmoredDetachSign(buf, &signer.Entity, bytes.NewBuffer(data), signer.cfg) + + if err == nil { + result = buf.String() + } + + return result, err +} + +func CreateArmoredTextSignature(text string, signer *Entity) (string, error) { + if signer == nil { + return "", errors.New("signer undefined") + } + result := "" + var err error + + buf := bytes.NewBufferString("") + + err = openpgp.ArmoredDetachSignText(buf, &signer.Entity, bytes.NewBufferString(text), signer.cfg) + + if err == nil { + result = buf.String() + } + + return result, err +} + +func CheckSignature(keyring EntityList, signed, signature []byte) (*Entity, error) { + var _signed io.Reader + var _signature io.Reader + + // Default readers + _signed = bytes.NewBuffer(signed) + _signature = bytes.NewBuffer(signature) + + // Unarmor signature if possible + if unarmored, err := armor.Decode(_signature); err == nil { + _signature = unarmored.Body + } + + // Check signature + signer, err := openpgp.CheckDetachedSignature(keyring, _signed, _signature, nil) + + if signer != nil { + return &Entity{*signer, nil}, err + } + return nil, err +} diff --git a/Signatures_test.go b/Signatures_test.go new file mode 100644 index 0000000..28c3a48 --- /dev/null +++ b/Signatures_test.go @@ -0,0 +1,76 @@ +package go_pgp + +import ( + "testing" + + "github.com/ProtonMail/go-crypto/openpgp" +) + +const ( + testPayload = `test + data` +) + +func TestCreateSignature(t *testing.T) { + initTest() + + if sig, err := CreateSignature([]byte(testPayload), nil); err == nil || len(sig) > 0 { + t.Error("TestCreateSignature: test case 1: if the signer entity is undefined the function should not succeed") + } + + if sig, err := CreateSignature([]byte(testPayload), testSenderEntity); err != nil || len(sig) == 0 { + t.Error("TestCreateSignature: test case 2: if the signer entity is defined the function should succeed") + } +} + +func TestCreateTextSignature(t *testing.T) { + initTest() + + if sig, err := CreateTextSignature(testPayload, nil); err == nil || len(sig) > 0 { + t.Error("TestCreateTextSignature: test case 1: if the signer entity is undefined the function should not succeed") + } + + if sig, err := CreateTextSignature(testPayload, testSenderEntity); err != nil || len(sig) == 0 { + t.Error("TestCreateTextSignature: test case 2: if the signer entity is defined the function should succeed") + } +} + +func TestCreateArmoredSignature(t *testing.T) { + initTest() + + if sig, err := CreateArmoredSignature([]byte(testPayload), nil); err == nil || len(sig) > 0 { + t.Error("TestCreateArmoredSignature: test case 1: if the signer entity is undefined the function should not succeed") + } + + if sig, err := CreateArmoredSignature([]byte(testPayload), testSenderEntity); err != nil || len(sig) == 0 { + t.Error("TestCreateArmoredSignature: test case 2: if the signer entity is defined the function should succeed") + } +} + +func TestCreateArmoredTextSignature(t *testing.T) { + initTest() + + if sig, err := CreateArmoredTextSignature(testPayload, nil); err == nil || len(sig) > 0 { + t.Error("TestCreateArmoredTextSignature: test case 1: if the signer entity is undefined the function should not succeed") + } + + if sig, err := CreateArmoredTextSignature(testPayload, testSenderEntity); err != nil || len(sig) == 0 { + t.Error("TestCreateArmoredTextSignature: test case 2: if the signer entity is defined the function should succeed") + } +} + +func TestCheckSignature(t *testing.T) { + initTest() + + armoredSignature, _ := CreateArmoredSignature([]byte(testPayload), testSenderEntity) + + e, err := CheckSignature(EntityList{EntityList: openpgp.EntityList{&testSenderEntity.Entity}}, []byte(testPayload), []byte(armoredSignature)) + if e == nil || err != nil { + t.Error("TestCheckSignature: test case 1: verification of the signature should have succeeded") + } + + e, err = CheckSignature(EntityList{EntityList: openpgp.EntityList{&testReceiverEntity.Entity}}, []byte(testPayload), []byte(armoredSignature)) + if e != nil || err == nil { + t.Error("TestCheckSignature: test case 2: verification of the signature should not have succeeded") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..be78769 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module git.sa-roci.de/oss/go_pgp + +go 1.20 + +require github.com/ProtonMail/go-crypto v1.0.0 + +require ( + github.com/cloudflare/circl v1.3.7 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..17d2c7a --- /dev/null +++ b/go.sum @@ -0,0 +1,51 @@ +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/go_pgp.code-workspace b/go_pgp.code-workspace new file mode 100644 index 0000000..88b95fb --- /dev/null +++ b/go_pgp.code-workspace @@ -0,0 +1,9 @@ +{ + "folders": [ + { + "name": "git.sa-roci.de/oss/go_pgp", + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/testdata/testReceiverEntity.pgp b/testdata/testReceiverEntity.pgp new file mode 100644 index 0000000000000000000000000000000000000000..e3b87ab7b7798e22cc2e0d6546c9aaf14845e58d GIT binary patch literal 4824 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 literal 0 HcmV?d00001 diff --git a/testdata/testReceiverEntityArmored.pgp b/testdata/testReceiverEntityArmored.pgp new file mode 100644 index 0000000..779569f --- /dev/null +++ b/testdata/testReceiverEntityArmored.pgp @@ -0,0 +1,105 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +8bhGpKfbM+WOa+BicpXvFs82Cac5EFFw +=s85P +-----END PGP PRIVATE KEY BLOCK----- \ No newline at end of file diff --git a/testdata/testReceiverEntityArmored_bad.pgp b/testdata/testReceiverEntityArmored_bad.pgp new file mode 100644 index 0000000..7775d6e --- /dev/null +++ b/testdata/testReceiverEntityArmored_bad.pgp @@ -0,0 +1,101 @@ +xcZYBGW2Q1YBEADWf+kUKy9mWyRrOPYs+WoBrzABncFLNYl65zl8DvIo2cef+P7b +coZy+V/BAZyjKChnaUWcf85jpGhapmiohj5DaeELd+8YdtDRuCjrWAKY3PatGjal +oKiODL/rvaxq41FneZAnD0geeCAefLIaCxaBQo+2zZ9DvqtV038V7msrBBO1HZjw +qIo9UzyhFWQ3h8FlPQ6BRCHum3zD3RtnE8TYwANBhgjEYgpVxKYbK+KEqcXPAMzd +uwsg0xeuhRNhdKzIYP0a7hNcMW5uChiyNnrxYMkSqZKCSiK+A6Kzx81HeTCaA2gG +jOOxHX5m3SpnR6r+fCUFxEOvEMfpQbXI4Y8GHFh6d6gNbvgfpKTkvY7/63LRo+ag +DP/Yeosvr3pqym7g9TOaSANFgYjkfmaRrt/nt7nSivzYehBoTRKiJJv5hfHHu+tt +1Oey32zEZXy+KrE9dStmwiUM0oqsKmRuwD2S+OMcx4kWfNojtPvsRlZ4wnzC1EDc +LhmfPberNpf3dPTxz8up95s3UZJlQ+/gkPuZ8RAL32FIdB7zM/k8EMHrdHM/QRMz +1LwNt5JBlkoUu3Fu2U/Jkkyu4J+IAA0zVzqlDH5GXynllw4+lVCJgqUwgBBTEAmD +gdZrByLhU5vRl5Fq9LMDIRmnpnOCWCLbiqXqJnOuPNO4Jkms1rrQw6iccQARAQAB +ABAAy1lD69zs+MpbAlNj/ksNVj9w8XdO8i/0/9EJTDUXGecsOtphMkWmdIU5Y85V +YtAFTdQdLDp1vTz1uUqdWXa6vEo6ERdIUhcB7G/8SvCKtcR1DrIVMHvYj6JCqAiO +1l5epAw4vu7b3hbfzEdGyS3Nzgj+Gb1hyWWPJLR2fKIkcwlQbZl7jlsZgv3QMp4G +/iXzEDkL8TxYNs6tPzn0aonxSdLuw0WANj0Dsz/UgVnfSwlv/8iwb1yNNjco8kgy +kWd9DhH0oX0mWXZ7TFLCCuFfOEobFVgxDtedjCoCKxG16JZZnwivq9YRXa2qUXZG +Ztcook2rA4aG7AaqntWuspwza0lQ0B16NQs1mYnf4pgI2PEi57W73KY18erg4KWc +CEBMdXi3ZLuo11iH51759A5OdvZNv+BZMxvkph1y6jlOtJ6AUy+4lTLdBGr2i+DE +biqXuxKwI1pEthugRJC7dtuRcrMvsPNoBK8dtvZmjEX5gid+48xvaC0XEXxkTTXS +SRulLEqsbHb7XoqKFbJBEoLem8BFPmyZpABTiAbsNv+338ffOG+PmecURncQyW45 +xqTq1MSc5ExuwwiD5EkEZNJXAr1GdOQJjQJ2vcrByJ9/GHPeNBlsdC/ZWsH7dLqg +EDUJjH5ndMlBXDbPE2Lf6n0MpPZ3Gf/mSRnkS4mnTlbueOEIAOrUKyfY5J6uY/7F +jmik1XGWycJlkItNzn7TngH84z/mmCvBl/NGy6RjEzOVb3zSGSlP3EFMThPp1bgg +rDinS8t8R739p9JX0P60xs0okO8OiHWF/cCQ/iVoXIlFu2N21VPeZP6J6VgUhLPm +vBhrIGiWCDW+4hWnKHB7+pf9an0Nv2lA9bJEywRTF5LJUBbbm11Cc69WIjSCgPq+ +UsFnvkfvqmrKueOe0/Y2+vOa2YLHxLGd5TGzitdeunGe5GLs7jBVW3kPHdVk79D4 +0bpsiMKh6Yq074b8wg1Z/x5GFl4fQyn/Syjh4JasMTas5LuaJkR/Pju020S7AZA8 +xGDueQUIAOnWjAU6kVNfHWCpEdnh0GErtRuYX0bxx+v3NNOmj5dNeqFsrflW5V4T +8ZWSZoTQYhAturs10jJM92QUz/Bir2eNg9DA9uvds/oJ5opG6dXpZ/ZHlJcKjM7x +xaQ8HRqaAB7S6PhJYl2/QkdHoJTfUj+1MDOUHMVK33p2rdmicQYu6JBR1G/kyNq4 +9hMW1nl6Qu5ZR/sSTuRlUnYxx+kMK9cFG9nReS5PO4m7/NwcDB05HQJZW4q4geS3 +ydHOBH7sKeZymQZMrqN1vFz9DA4rlecm1TaDYyboSPLWOxOeWpz2cD63TGhumxA5 +/JSYfuBrzaB9YYeI6uf6t2Gb/LgMgX0H/2ooEAo6+fD/3FYSH9229QVrLEM2v5UP +ET4+E4NkvfIFRUxdrl8oN7JTVpHNehF3/AJdcd0+LS/Y7TKDsxTX0lV0UjwTzPVG +f45DCOyXInhrChxBwyVMfhtlwKCFBG6GNlB7mjcKe9ewXqzIAXLqiG7jnJ2Uxu3r +AlajefCSv8GMQCtO3eccaMQhBu+f+rbFk+lhmpQpTH/AzRNk6mYtMFHMg29nbAxB +TZJOm67Se/Ne8gjnMZupHFA2upGr445p6JLapdFfQYmxaZGQsHkI6munzUzo8edr +7FhaMFedvRtjSBnfp5geD2szgDjW/RZPThCvxlQC6fj+EsdmGO8iazyIgM0VcGdw +VGVzdFJlY2VpdmVyRW50aXR5wsGPBBMBCgBDBQJltkNWCRBV1MhzGg9oXxYhBC+L +bnEkY2CPAlwLhlXUyHMaD2hfAhsDAh4BAhkBAwsJBwQVCAkKAxYAAQUnCQIHAgAA +CgQP/0RXQSIqc3NjFKjuEbXpe2rrwYVjXYRIDlOobdkotcq7pYGwT3EOywZ/LwNt +81YVo6TaV6i0CYXo0DiJ5/M8xd3bt9GgBeG6xnibvbpQleIHnwW/5F2sGPH6KGC2 +ObJRYjxHgPoyGF9VR3XiVUA+Nul5X95kncAFUw+Vzn1e60sU2Jw4v4nvy5GNuTl+ +z926hcky7EM3IM6IjDiu5IbiRzDlPDWcN1zBsEv06j6cbhVd/L75aftkeUSxMD6l +8HiwUrNz/ZbSgtbaHflGa8DxWbkyQl317WaIT1+bN+VT/uAWOSaX3rU/Vid+B3J4 +XQmnPgCkAKSUhmmrEm/TfHMW9CnKsqEsMYMsfDFelj1yePyJN8crn7rLHoY0C+/6 +EClJ+pAgYszj2oWVVn5WoulAy05wXLG/FnsuP/YnAw9+teQQe1buFdd5JQPe/8mx +vHXNRjozb/q4BfrQXmHuOLQxlYo0C7oBEOWv7UnqB1ciDnuaEwaE9AUYWAs+sP4P +xjE9hipwRDdEkttvNDaVJV7gWtLg4l3VfbUBsCW1nnb5x+BgTPHRztIYRvp32GS+ +7nnVzYDP8dY7gLmTONi/o/d9M6u3zr4a1+RMwNVOaeBIUrjROMj0kCIh0Iw1mDhX +Dxdb7Kmh/vUISm0LSWdO4vxEQgwBsJ79S4qaMamiPabbv/nXx8ZYBGW2Q1YBEAC+ +isMICcV3fBY9xtGidh21iZqP/VDU5XFojo2QLEXmV2InM1xUiwlditCJIAvF63xM +Dto+fFpt37xsALXLeFzyHFEEinWJhlRck4Oj3Liwy80mFD7FQ77A3N8rPOIUkfdM +5Wlc4oybiQzxTeI3zVz2huPb4xNFtmcRusagNix04Rse+QTRERbHp8VD/4EpMGOq +s0HHJacIfXa760+Tu/sfcvZ4SA+Y/Z/cWL3hD/XF7c9eZIjHmzoFf+qouLcHq6XZ +5vbMnTeTKYDPr6A2k9vTwaWYeqjUFmDqov98OKJ/GgnHq+QNo89a1p46KVZmR0VS +MpUMFekld0ab68kkfP5g+Dr4QbYDApPopC3mG/GlVUDpvHXj9Xvdc1YE2Xk9OIc5 +xqMbm4O2vRBxwcHths/JgdHHdHRnmFahGgRcnxTulhr/JDvVrg5OhYhr756GhwWd +W1NSdbSRZpsnydFxQsj65IOTaqP5eaGT3ai98cufQvrddUQzlLw//2NKdZ6jse8n +XJGQXPgO+yg0g8xAlUnpavqbRE44ErA3nw3iI1dJSj7UVnQdIHWiZaODxTinYytu +Sg4TZgmZkdXP2vTeagZVki0vN+Xd5ni552q7l2k1uj3aH8+QAIPVFYhwOJarkBbM +OAjnMnrC8G28Uj14hOTin8Yd+BHknDqJn0hJBo+GOwARAQABABAAnJDyvvkgfBTH +Rc3H1gHOWawPB//zWmyKKorwQaZPbX0iFun6FTIF6Qo2XmappeDgyrJtnGib+aqd +bfWLa/ykCwE/hUasW+u4CDXiNlQYopVkJcT+6yLGbD1RV3r4nkRue44KbJtvRCJy +Mxl3J7kkiSG/u5+z51WBDa12ppC9KPELUwD1d0DeggIWctBQ9mJfkxUmmJgUz0Ig +vTsWsWMGbwoNAjrcLi9BG1MD+xodLp1BBuP/DF09aOV6EVdudewSJKHG68zHrLGu +uXkrYY1PVYnKIyeu8E7PJh33ZsA2rc/cd2iDxL10lcTCUTJEX1hy2p7boAmlAPco +Wth89/GJJwIlzJQ5cWWkmcF9gs9EBokyJ1jS5BMPYSW/FaDcnMb/RtCg7pzCh/iQ +OXuVIuYrrf3pc8/nSmmZ81h0Ceul/+Sev+nrTxub/XYtCbu+sHBjQakGTdELdngA +GwBzsfFBHittiTVe9jYkDpcwyJN4g+KwBmsrxdfZqSODHGwDrIUfE4YjaKTbcb66 +ZDA/D1kH27KVm6W/6CPBEF2bUy9sTTTfx+zu9T/E5Ws3wqHb0VFDLJPluL65rikQ +lh/Nzr9X+kJbjMLXbsN8FzT3wfUmtlRc/X72pO7Y1tsYqRoeqwM4BTkUJmNBquLp +clZtEAdxlGcldBY53BtA0BwkIKyZAAkIAMe8N4DutY5L+h9tneHvQS6nZ25zibkl +CJPbnnWYGy9PDnDuQjAVSlZRSjL2dMzYFLZpLoHCj8FvnC4nQvnexPUI907G1VVb +b9HCZhiqLATO0JdmHbF7iKuKqa0rECSAFhnCvy+hgTy8kN/t7zh5O8uPKwQSTeX8 +IcLd5AVT2yrTiJN6wgfWmk6A1TUFo9jqrnaP2IpNZlExJs4vgqzoqlnx9qijtQsm +u3wecwHWXM825StKzQCUXnzzdUGFBbsBDkBNpieRiRy3WQVOizne/vdNIMucXLl8 +VZlqXAXFQEQcl85pVmtdvH+zbAFPdHloWvcd2JViTAGD8ktN7t2WWM8IAPQ3lZBq +3F+xKUVj8k+p+GwsgB4450ra9ZL7kevmgavVmVNDoCHjc1AhtN80EE0+fsJoTJ0X +bAvW85FeDmnZvEwI/bDO+QVrj806YifmtFA7wz31UluL2Aet+7EiI7GmOC1E0lWL +fMNXh1UliZjLpfP2cofGWvi6RCNaXSFHHZR7f2xcU9MJQ+iVxOd1hKNZ+UwP/Uhr +4HOeIHeXuNiKOACnUYvNAH967TSsNGealLbHlrJkdgN6mTMdfHpuvHRdM/0xkUHw +lAAj5OSRaidaLIQ9fDMDPTcnCVeeovQBpEIPh0gH4JHBfSuBWhEgVoWa+95Aw6Fm +KWReFjpNHOwSvtUH/ijuxCmnotoW/1JqIGcq0XZHlGVIIjlzHgrkQ2WulK3WuSJj +Yu2Yx7bN421JiTAnQlyjSqpS4FLCeOa3/9qEa/G7Bk8f6gUaRYFbZot9XTwmDD9O +h+UUZNR762aUVneJ08Dafcuzw7xihgo5yubLdPZfqsD9XAwV7xaoEc1k9uI1h1uW +qu/GQtQ3rJAGhGKQ+l4zdYd/Vz0HuQSgsu1UfOm00i9rPWvpnlFp7hQEBjw7LkKw +8nM5o3km3jXKrWXtVs02sNK17du5fcVYqgBI74oCHJnqOWh6bc93ooxoe3X1Hhp3 +sFfcVRJI0YfH8mw06mvFZ81ouWDq0S80xDiJzLdyxcLBdgQYAQoAKgUCZbZDVgkQ +VdTIcxoPaF8WIQQvi25xJGNgjwJcC4ZV1MhzGg9oXwIbDAAASkIQAIJwrvjiCjX1 +LdYRvNMFK2QbXpAUHexSBnmi3TD8CCiHz85DahpWhJciM4Yg+8oUyVlppCY1dmtc +dRUNRblb2hI7nfJNFdXfYiS/sQe5RaGJHHhRB+Nz5lxuxcYVPfl+kDEcbk4tmeqB +MDe21a9uY3io75ASa/mKTI+/ousuoDhoTdfQuqeU7TQdliDzRh8TlIkLTvnmhWCH +T1fPNeBIppBT0Qdz4uBAKYR1LVzvCkHWumpQzs0G59YBba/Amm4wbm52QZNRkQU3 +Vxmglh8fTyR3f/6Agpf1WwYckEAudggvenDxQh/jB1HZekYudTPRfgifBjQqipXQ +IvHu4Q20/XBT4WFVnjsDFOgZ1ZaHUHmnMQiBdLexsiUGijtk5PCn56Og1YJ9SVUq +a9NTX3+eI04oKACCg86ljzvoYqAAl6OZG6n0evFUUpKs+2co+p/19WhCJRj5OEJ/ +Zl8VZ12xw2IHdTl9EpBas2aq+mprlhph5DDi9HH9o8bzfgdDcEqXTUG7rFWDg/mJ +TLMQ7C/ahmGrSKinVsnb6V0ONOvBF4qJvtGZAZ+EqcHylP1SORplWyvOo/x3BedD +lEP34fJWPSPeUdihaeP7bkBMI1hft5lulzm5iX280b+dQuSaojCWa5DqmHJSJU+g +8bhGpKfbM+WOa+BicpXvFs82Cac5EFFw \ No newline at end of file diff --git a/testdata/testReceiverEntity_bad.pgp b/testdata/testReceiverEntity_bad.pgp new file mode 100644 index 0000000000000000000000000000000000000000..947f5e5266a1f0424dfdd147a7d567e619d111d8 GIT binary patch literal 8533 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 R!>XOlu!{pgHdEW&{{n<4LHPgx literal 0 HcmV?d00001 diff --git a/testdata/testSenderEntity.pgp b/testdata/testSenderEntity.pgp new file mode 100644 index 0000000000000000000000000000000000000000..cab69ead66185aece1d022034d4e5352755973c5 GIT binary patch literal 4822 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