commit d3e5c3236c5866099741a7c9776d81fa0aea0aee Author: Naoriel Sa' Rocí Date: Fri May 10 22:24:14 2024 +0200 First commit. 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 0000000..e3b87ab Binary files /dev/null and b/testdata/testReceiverEntity.pgp differ 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 @@ +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 +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 0000000..947f5e5 Binary files /dev/null and b/testdata/testReceiverEntity_bad.pgp differ diff --git a/testdata/testSenderEntity.pgp b/testdata/testSenderEntity.pgp new file mode 100644 index 0000000..cab69ea Binary files /dev/null and b/testdata/testSenderEntity.pgp differ diff --git a/testdata/testSenderEntityArmored.pgp b/testdata/testSenderEntityArmored.pgp new file mode 100644 index 0000000..26788fc --- /dev/null +++ b/testdata/testSenderEntityArmored.pgp @@ -0,0 +1,105 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +j2cS6+ZAFwEKAh8MaTL51D08ND0TYQ== +=RZn6 +-----END PGP PRIVATE KEY BLOCK----- \ No newline at end of file