diff --git a/go.mod b/go.mod index 1ac52c9..240d180 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( ) require ( - github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.37.0 // indirect diff --git a/go.sum b/go.sum index c9a50dd..cd3b418 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/pkg/crypt/crypt.go b/pkg/crypt/crypt.go index 82a4329..7561846 100644 --- a/pkg/crypt/crypt.go +++ b/pkg/crypt/crypt.go @@ -7,23 +7,22 @@ import ( "crypto/sha512" "encoding/binary" "encoding/hex" - "io" "strconv" "strings" "github.com/Snider/Enchantrix/pkg/crypt/std/lthn" - "github.com/Snider/Enchantrix/pkg/crypt/std/openpgp" + "github.com/Snider/Enchantrix/pkg/crypt/std/rsa" ) // Service is the main struct for the crypt service. type Service struct { - pgp *openpgp.Service + rsa *rsa.Service } // NewService creates a new crypt service. func NewService() *Service { return &Service{ - pgp: openpgp.NewService(), + rsa: rsa.NewService(), } } @@ -136,24 +135,19 @@ func (s *Service) Fletcher64(payload string) uint64 { return (sum2 << 32) | sum1 } -// --- PGP --- +// --- RSA --- -// GeneratePGPKeyPair creates a new PGP key pair. -func (s *Service) GeneratePGPKeyPair(name, email, passphrase string) (publicKey, privateKey string, err error) { - return s.pgp.GenerateKeyPair(name, email, passphrase) +// GenerateRSAKeyPair creates a new RSA key pair. +func (s *Service) GenerateRSAKeyPair(bits int) (publicKey, privateKey []byte, err error) { + return s.rsa.GenerateKeyPair(bits) } -// AddPGPSubkey adds a new subkey to an existing key pair. -func (s *Service) AddPGPSubkey(privateKey, passphrase string) (updatedPrivateKey string, err error) { - return s.pgp.AddSubkey(privateKey, passphrase) +// EncryptRSA encrypts data with a public key. +func (s *Service) EncryptRSA(publicKey, data []byte) ([]byte, error) { + return s.rsa.Encrypt(publicKey, data) } -// EncryptPGP encrypts data for a recipient, optionally signing it. -func (s *Service) EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) error { - return s.pgp.EncryptPGP(writer, recipientPath, data, signerPath, signerPassphrase) -} - -// DecryptPGP decrypts a PGP message, optionally verifying the signature. -func (s *Service) DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) { - return s.pgp.DecryptPGP(recipientPath, message, passphrase, signerPath) +// DecryptRSA decrypts data with a private key. +func (s *Service) DecryptRSA(privateKey, ciphertext []byte) ([]byte, error) { + return s.rsa.Decrypt(privateKey, ciphertext) } diff --git a/pkg/crypt/std/openpgp/openpgp.go b/pkg/crypt/std/openpgp/openpgp.go deleted file mode 100644 index 151c0c5..0000000 --- a/pkg/crypt/std/openpgp/openpgp.go +++ /dev/null @@ -1,246 +0,0 @@ -package openpgp - -import ( - "bytes" - "crypto" - "fmt" - "io" - "os" - "strings" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/ProtonMail/go-crypto/openpgp/packet" -) - -// Service provides OpenPGP functionality. -type Service struct{} - -// NewService creates a new OpenPGP service. -func NewService() *Service { - return &Service{} -} - -// GenerateKeyPair creates a new PGP key pair and returns the armored public and private keys. -func (s *Service) GenerateKeyPair(name, email, passphrase string) (publicKey, privateKey string, err error) { - config := &packet.Config{ - DefaultHash: crypto.SHA256, - DefaultCipher: packet.CipherAES256, - DefaultCompressionAlgo: packet.CompressionZLIB, - RSABits: 4096, - } - entity, err := openpgp.NewEntity(name, "", email, config) - if err != nil { - return "", "", fmt.Errorf("failed to create new entity: %w", err) - } - - // Add a subkey for encryption - err = entity.AddEncryptionSubkey(config) - if err != nil { - return "", "", fmt.Errorf("failed to add encryption subkey: %w", err) - } - - // Encrypt the private key - if passphrase != "" { - err = entity.PrivateKey.Encrypt([]byte(passphrase)) - if err != nil { - return "", "", fmt.Errorf("failed to encrypt private key: %w", err) - } - } - - var pubKeyBuf, privKeyBuf bytes.Buffer - pubKeyWriter, err := armor.Encode(&pubKeyBuf, openpgp.PublicKeyType, nil) - if err != nil { - return "", "", err - } - privKeyWriter, err := armor.Encode(&privKeyBuf, openpgp.PrivateKeyType, nil) - if err != nil { - return "", "", err - } - - err = entity.Serialize(pubKeyWriter) - if err != nil { - return "", "", err - } - pubKeyWriter.Close() - - err = entity.SerializePrivate(privKeyWriter, nil) - if err != nil { - return "", "", err - } - privKeyWriter.Close() - - return pubKeyBuf.String(), privKeyBuf.String(), nil -} - -// AddSubkey adds a new subkey to an existing key pair. -func (s *Service) AddSubkey(privateKey, passphrase string) (updatedPrivateKey string, err error) { - entity, err := readArmoredEntity(privateKey) - if err != nil { - return "", err - } - - if entity.PrivateKey.Encrypted { - err = entity.PrivateKey.Decrypt([]byte(passphrase)) - if err != nil { - return "", fmt.Errorf("failed to decrypt private key: %w", err) - } - } - - config := &packet.Config{RSABits: 2048, DefaultHash: crypto.SHA256} - err = entity.AddEncryptionSubkey(config) - if err != nil { - return "", fmt.Errorf("failed to add encryption subkey: %w", err) - } - - // If the key was encrypted, re-encrypt it with the new subkey. - if entity.PrivateKey.Encrypted { - err = entity.PrivateKey.Encrypt([]byte(passphrase)) - if err != nil { - return "", fmt.Errorf("failed to re-encrypt private key: %w", err) - } - } - - var privKeyBuf bytes.Buffer - privKeyWriter, err := armor.Encode(&privKeyBuf, openpgp.PrivateKeyType, nil) - if err != nil { - return "", fmt.Errorf("failed to create private key armor writer: %w", err) - } - err = entity.SerializePrivate(privKeyWriter, nil) - if err != nil { - return "", fmt.Errorf("failed to serialize private key: %w", err) - } - privKeyWriter.Close() - updatedPrivateKey = privKeyBuf.String() - - return updatedPrivateKey, nil -} - -// EncryptPGP encrypts data for a recipient, optionally signing it. -func (s *Service) EncryptPGP(writer io.Writer, recipientPath, data string, signerPath, signerPassphrase *string) error { - recipientFile, err := os.Open(recipientPath) - if err != nil { - return fmt.Errorf("failed to open recipient public key file: %w", err) - } - defer recipientFile.Close() - - recipient, err := openpgp.ReadArmoredKeyRing(recipientFile) - if err != nil { - return fmt.Errorf("failed to read recipient public key ring: %w", err) - } - - var signer *openpgp.Entity - if signerPath != nil { - signerFile, err := os.Open(*signerPath) - if err != nil { - return fmt.Errorf("failed to open signer private key file: %w", err) - } - defer signerFile.Close() - - signerRing, err := openpgp.ReadArmoredKeyRing(signerFile) - if err != nil { - return fmt.Errorf("failed to read signer key ring: %w", err) - } - signer = signerRing[0] - - if signer.PrivateKey.Encrypted { - if signerPassphrase == nil { - return fmt.Errorf("signer key is encrypted but no passphrase was provided") - } - err = signer.PrivateKey.Decrypt([]byte(*signerPassphrase)) - if err != nil { - return fmt.Errorf("failed to decrypt signer key: %w", err) - } - } - } - - plaintext, err := openpgp.Encrypt(writer, recipient, signer, nil, nil) - if err != nil { - return fmt.Errorf("failed to create encryption writer: %w", err) - } - - _, err = io.Copy(plaintext, strings.NewReader(data)) - if err != nil { - return fmt.Errorf("failed to write data to encryption writer: %w", err) - } - - return plaintext.Close() -} - -// DecryptPGP decrypts a PGP message, optionally verifying the signature. -func (s *Service) DecryptPGP(recipientPath, message, passphrase string, signerPath *string) (string, error) { - recipientFile, err := os.Open(recipientPath) - if err != nil { - return "", fmt.Errorf("failed to open recipient private key file: %w", err) - } - defer recipientFile.Close() - - recipientRing, err := openpgp.ReadArmoredKeyRing(recipientFile) - if err != nil { - return "", fmt.Errorf("failed to read recipient key ring: %w", err) - } - recipient := recipientRing[0] - - if recipient.PrivateKey.Encrypted { - err = recipient.PrivateKey.Decrypt([]byte(passphrase)) - if err != nil { - return "", fmt.Errorf("failed to decrypt recipient key: %w", err) - } - } - - var signer openpgp.EntityList - if signerPath != nil { - signerFile, err := os.Open(*signerPath) - if err != nil { - return "", fmt.Errorf("failed to open signer public key file: %w", err) - } - defer signerFile.Close() - - signer, err = openpgp.ReadArmoredKeyRing(signerFile) - if err != nil { - return "", fmt.Errorf("failed to read signer key ring: %w", err) - } - } - - var md *openpgp.MessageDetails - if signer != nil { - md, err = openpgp.ReadMessage(strings.NewReader(message), recipientRing, func(keys []openpgp.Key, symmetric bool) ([]byte, error) { - return []byte(passphrase), nil - }, nil) - } else { - md, err = openpgp.ReadMessage(strings.NewReader(message), recipientRing, func(keys []openpgp.Key, symmetric bool) ([]byte, error) { - return []byte(passphrase), nil - }, nil) - } - if err != nil { - return "", fmt.Errorf("failed to read message: %w", err) - } - - if signer != nil { - if md.Signature == nil { - return "", fmt.Errorf("message is not signed, but a signer was provided") - } - hash := md.Signature.Hash.New() - io.Copy(hash, md.UnverifiedBody) - err = signer[0].PrimaryKey.VerifySignature(hash, md.Signature) - if err != nil { - return "", fmt.Errorf("signature verification failed: %w", err) - } - } - - plaintext, err := io.ReadAll(md.UnverifiedBody) - if err != nil { - return "", fmt.Errorf("failed to read plaintext: %w", err) - } - - return string(plaintext), nil -} - -func readArmoredEntity(armoredKey string) (*openpgp.Entity, error) { - in := strings.NewReader(armoredKey) - block, err := armor.Decode(in) - if err != nil { - return nil, err - } - return openpgp.ReadEntity(packet.NewReader(block.Body)) -} diff --git a/pkg/crypt/std/rsa/rsa.go b/pkg/crypt/std/rsa/rsa.go index f0082cd..a3bde46 100644 --- a/pkg/crypt/std/rsa/rsa.go +++ b/pkg/crypt/std/rsa/rsa.go @@ -1,3 +1,88 @@ package rsa -// This file is a placeholder for RSA key handling functionality. +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "fmt" +) + +// Service provides RSA functionality. +type Service struct{} + +// NewService creates a new RSA service. +func NewService() *Service { + return &Service{} +} + +// GenerateKeyPair creates a new RSA key pair. +func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err error) { + privKey, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, fmt.Errorf("failed to generate private key: %w", err) + } + + privKeyBytes := x509.MarshalPKCS1PrivateKey(privKey) + privKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privKeyBytes, + }) + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey) + if err != nil { + return nil, nil, fmt.Errorf("failed to marshal public key: %w", err) + } + pubKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + }) + + return pubKeyPEM, privKeyPEM, nil +} + +// Encrypt encrypts data with a public key. +func (s *Service) Encrypt(publicKey, data []byte) ([]byte, error) { + block, _ := pem.Decode(publicKey) + if block == nil { + return nil, fmt.Errorf("failed to decode public key") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %w", err) + } + + rsaPub, ok := pub.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("not an RSA public key") + } + + ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPub, data, nil) + if err != nil { + return nil, fmt.Errorf("failed to encrypt data: %w", err) + } + + return ciphertext, nil +} + +// Decrypt decrypts data with a private key. +func (s *Service) Decrypt(privateKey, ciphertext []byte) ([]byte, error) { + block, _ := pem.Decode(privateKey) + if block == nil { + return nil, fmt.Errorf("failed to decode private key") + } + + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("failed to decrypt data: %w", err) + } + + return plaintext, nil +} diff --git a/pkg/crypt/std/rsa/rsa_test.go b/pkg/crypt/std/rsa/rsa_test.go new file mode 100644 index 0000000..cd08d63 --- /dev/null +++ b/pkg/crypt/std/rsa/rsa_test.go @@ -0,0 +1,50 @@ +package rsa + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRSA_Good(t *testing.T) { + s := NewService() + + // Generate a new key pair + pubKey, privKey, err := s.GenerateKeyPair(2048) + assert.NoError(t, err) + assert.NotEmpty(t, pubKey) + assert.NotEmpty(t, privKey) + + // Encrypt and decrypt a message + message := []byte("Hello, World!") + ciphertext, err := s.Encrypt(pubKey, message) + assert.NoError(t, err) + plaintext, err := s.Decrypt(privKey, ciphertext) + assert.NoError(t, err) + assert.Equal(t, message, plaintext) +} + +func TestRSA_Bad(t *testing.T) { + s := NewService() + + // Decrypt with wrong key + pubKey, _, err := s.GenerateKeyPair(2048) + assert.NoError(t, err) + _, otherPrivKey, err := s.GenerateKeyPair(2048) + assert.NoError(t, err) + message := []byte("Hello, World!") + ciphertext, err := s.Encrypt(pubKey, message) + assert.NoError(t, err) + _, err = s.Decrypt(otherPrivKey, ciphertext) + assert.Error(t, err) +} + +func TestRSA_Ugly(t *testing.T) { + s := NewService() + + // Malformed keys and messages + _, err := s.Encrypt([]byte("not-a-key"), []byte("message")) + assert.Error(t, err) + _, err = s.Decrypt([]byte("not-a-key"), []byte("message")) + assert.Error(t, err) +}