Co-authored-by: Claude <developers@lethean.io> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c122e89f40
commit
dfd7c3ab2d
8 changed files with 930 additions and 0 deletions
60
pkg/crypt/chachapoly/chachapoly.go
Normal file
60
pkg/crypt/chachapoly/chachapoly.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Package chachapoly provides XChaCha20-Poly1305 authenticated encryption.
|
||||||
|
//
|
||||||
|
// Encrypt prepends a random nonce to the ciphertext; Decrypt extracts it.
|
||||||
|
// The key must be 32 bytes (256 bits).
|
||||||
|
//
|
||||||
|
// Ported from Enchantrix (github.com/Snider/Enchantrix/pkg/crypt/std/chachapoly).
|
||||||
|
package chachapoly
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt encrypts plaintext using XChaCha20-Poly1305.
|
||||||
|
// The key must be exactly 32 bytes. A random 24-byte nonce is generated
|
||||||
|
// and prepended to the returned ciphertext.
|
||||||
|
func Encrypt(plaintext, key []byte) ([]byte, error) {
|
||||||
|
aead, err := chacha20poly1305.NewX(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("chachapoly: failed to create AEAD: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(plaintext)+aead.Overhead())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return nil, fmt.Errorf("chachapoly: failed to generate nonce: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aead.Seal(nonce, nonce, plaintext, nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts ciphertext produced by Encrypt using XChaCha20-Poly1305.
|
||||||
|
// The key must be exactly 32 bytes. The nonce is extracted from the first
|
||||||
|
// 24 bytes of the ciphertext.
|
||||||
|
func Decrypt(ciphertext, key []byte) ([]byte, error) {
|
||||||
|
aead, err := chacha20poly1305.NewX(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("chachapoly: failed to create AEAD: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
minLen := aead.NonceSize() + aead.Overhead()
|
||||||
|
if len(ciphertext) < minLen {
|
||||||
|
return nil, fmt.Errorf("chachapoly: ciphertext too short: got %d bytes, need at least %d bytes", len(ciphertext), minLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, ciphertext := ciphertext[:aead.NonceSize()], ciphertext[aead.NonceSize():]
|
||||||
|
|
||||||
|
decrypted, err := aead.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("chachapoly: decryption failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decrypted) == 0 {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return decrypted, nil
|
||||||
|
}
|
||||||
93
pkg/crypt/chachapoly/chachapoly_test.go
Normal file
93
pkg/crypt/chachapoly/chachapoly_test.go
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
package chachapoly
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateKey(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
key := make([]byte, 32)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Good(t *testing.T) {
|
||||||
|
key := generateKey(t)
|
||||||
|
plaintext := []byte("hello, XChaCha20-Poly1305!")
|
||||||
|
|
||||||
|
ciphertext, err := Encrypt(plaintext, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, plaintext, ciphertext)
|
||||||
|
// Ciphertext should be longer than plaintext (nonce + overhead)
|
||||||
|
assert.Greater(t, len(ciphertext), len(plaintext))
|
||||||
|
|
||||||
|
decrypted, err := Decrypt(ciphertext, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Bad(t *testing.T) {
|
||||||
|
key1 := generateKey(t)
|
||||||
|
key2 := generateKey(t)
|
||||||
|
plaintext := []byte("secret data")
|
||||||
|
|
||||||
|
ciphertext, err := Encrypt(plaintext, key1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Decrypting with a different key should fail
|
||||||
|
_, err = Decrypt(ciphertext, key2)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Ugly(t *testing.T) {
|
||||||
|
// Invalid key length should fail
|
||||||
|
shortKey := []byte("too-short")
|
||||||
|
_, err := Encrypt([]byte("data"), shortKey)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
_, err = Decrypt([]byte("data"), shortKey)
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Ciphertext too short should fail
|
||||||
|
key := generateKey(t)
|
||||||
|
_, err = Decrypt([]byte("short"), key)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecryptEmpty_Good(t *testing.T) {
|
||||||
|
key := generateKey(t)
|
||||||
|
plaintext := []byte{}
|
||||||
|
|
||||||
|
ciphertext, err := Encrypt(plaintext, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := Decrypt(ciphertext, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptNonDeterministic_Good(t *testing.T) {
|
||||||
|
key := generateKey(t)
|
||||||
|
plaintext := []byte("same input")
|
||||||
|
|
||||||
|
ct1, err := Encrypt(plaintext, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ct2, err := Encrypt(plaintext, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Different nonces mean different ciphertexts
|
||||||
|
assert.NotEqual(t, ct1, ct2, "each encryption should produce unique ciphertext due to random nonce")
|
||||||
|
|
||||||
|
// Both should decrypt to the same plaintext
|
||||||
|
d1, err := Decrypt(ct1, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
d2, err := Decrypt(ct2, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, d1, d2)
|
||||||
|
}
|
||||||
94
pkg/crypt/lthn/lthn.go
Normal file
94
pkg/crypt/lthn/lthn.go
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
// Package lthn implements the LTHN quasi-salted hash algorithm.
|
||||||
|
//
|
||||||
|
// LTHN produces deterministic, verifiable hashes without requiring separate salt
|
||||||
|
// storage. The salt is derived from the input itself through:
|
||||||
|
// 1. Reversing the input string
|
||||||
|
// 2. Applying "leet speak" style character substitutions
|
||||||
|
//
|
||||||
|
// The final hash is: SHA256(input || derived_salt)
|
||||||
|
//
|
||||||
|
// This is suitable for content identifiers, cache keys, and deduplication.
|
||||||
|
// NOT suitable for password hashing - use bcrypt, Argon2, or scrypt instead.
|
||||||
|
//
|
||||||
|
// Ported from Enchantrix (github.com/Snider/Enchantrix/pkg/crypt/std/lthn).
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// hash := lthn.Hash("hello")
|
||||||
|
// valid := lthn.Verify("hello", hash) // true
|
||||||
|
package lthn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// keyMap defines the character substitutions for quasi-salt derivation.
|
||||||
|
// These are inspired by "leet speak" conventions for letter-number substitution.
|
||||||
|
// The mapping is bidirectional for most characters but NOT fully symmetric.
|
||||||
|
var keyMap = map[rune]rune{
|
||||||
|
'o': '0', // letter O -> zero
|
||||||
|
'l': '1', // letter L -> one
|
||||||
|
'e': '3', // letter E -> three
|
||||||
|
'a': '4', // letter A -> four
|
||||||
|
's': 'z', // letter S -> Z
|
||||||
|
't': '7', // letter T -> seven
|
||||||
|
'0': 'o', // zero -> letter O
|
||||||
|
'1': 'l', // one -> letter L
|
||||||
|
'3': 'e', // three -> letter E
|
||||||
|
'4': 'a', // four -> letter A
|
||||||
|
'7': 't', // seven -> letter T
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeyMap replaces the default character substitution map.
|
||||||
|
// Use this to customize the quasi-salt derivation for specific applications.
|
||||||
|
// Changes affect all subsequent Hash and Verify calls.
|
||||||
|
func SetKeyMap(newKeyMap map[rune]rune) {
|
||||||
|
keyMap = newKeyMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyMap returns the current character substitution map.
|
||||||
|
func GetKeyMap() map[rune]rune {
|
||||||
|
return keyMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash computes the LTHN hash of the input string.
|
||||||
|
//
|
||||||
|
// The algorithm:
|
||||||
|
// 1. Derive a quasi-salt by reversing the input and applying character substitutions
|
||||||
|
// 2. Concatenate: input + salt
|
||||||
|
// 3. Compute SHA-256 of the concatenated string
|
||||||
|
// 4. Return the hex-encoded digest (64 characters, lowercase)
|
||||||
|
//
|
||||||
|
// The same input always produces the same hash, enabling verification
|
||||||
|
// without storing a separate salt value.
|
||||||
|
func Hash(input string) string {
|
||||||
|
salt := createSalt(input)
|
||||||
|
hash := sha256.Sum256([]byte(input + salt))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify checks if an input string produces the given hash.
|
||||||
|
// Returns true if Hash(input) equals the provided hash value.
|
||||||
|
func Verify(input string, hash string) bool {
|
||||||
|
return Hash(input) == hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSalt derives a quasi-salt by reversing the input and applying substitutions.
|
||||||
|
// For example: "hello" -> reversed "olleh" -> substituted "011eh"
|
||||||
|
func createSalt(input string) string {
|
||||||
|
if input == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
runes := []rune(input)
|
||||||
|
salt := make([]rune, len(runes))
|
||||||
|
for i := 0; i < len(runes); i++ {
|
||||||
|
char := runes[len(runes)-1-i]
|
||||||
|
if replacement, ok := keyMap[char]; ok {
|
||||||
|
salt[i] = replacement
|
||||||
|
} else {
|
||||||
|
salt[i] = char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(salt)
|
||||||
|
}
|
||||||
99
pkg/crypt/lthn/lthn_test.go
Normal file
99
pkg/crypt/lthn/lthn_test.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package lthn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHash_Good(t *testing.T) {
|
||||||
|
hash := Hash("hello")
|
||||||
|
assert.Len(t, hash, 64, "SHA-256 hex digest should be 64 characters")
|
||||||
|
assert.NotEmpty(t, hash)
|
||||||
|
|
||||||
|
// Same input should always produce the same hash (deterministic)
|
||||||
|
hash2 := Hash("hello")
|
||||||
|
assert.Equal(t, hash, hash2, "same input must produce the same hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHash_Bad(t *testing.T) {
|
||||||
|
// Different inputs should produce different hashes
|
||||||
|
hash1 := Hash("hello")
|
||||||
|
hash2 := Hash("world")
|
||||||
|
assert.NotEqual(t, hash1, hash2, "different inputs must produce different hashes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHash_Ugly(t *testing.T) {
|
||||||
|
// Empty string should still produce a valid hash
|
||||||
|
hash := Hash("")
|
||||||
|
assert.Len(t, hash, 64)
|
||||||
|
assert.NotEmpty(t, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify_Good(t *testing.T) {
|
||||||
|
input := "test-data-123"
|
||||||
|
hash := Hash(input)
|
||||||
|
assert.True(t, Verify(input, hash), "Verify must return true for matching input")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify_Bad(t *testing.T) {
|
||||||
|
input := "test-data-123"
|
||||||
|
hash := Hash(input)
|
||||||
|
assert.False(t, Verify("wrong-input", hash), "Verify must return false for non-matching input")
|
||||||
|
assert.False(t, Verify(input, "0000000000000000000000000000000000000000000000000000000000000000"),
|
||||||
|
"Verify must return false for wrong hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify_Ugly(t *testing.T) {
|
||||||
|
// Empty input round-trip
|
||||||
|
hash := Hash("")
|
||||||
|
assert.True(t, Verify("", hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetKeyMap_Good(t *testing.T) {
|
||||||
|
// Save original map
|
||||||
|
original := GetKeyMap()
|
||||||
|
|
||||||
|
// Set a custom key map
|
||||||
|
custom := map[rune]rune{
|
||||||
|
'a': 'b',
|
||||||
|
'b': 'a',
|
||||||
|
}
|
||||||
|
SetKeyMap(custom)
|
||||||
|
|
||||||
|
// Hash should use new key map
|
||||||
|
hash1 := Hash("abc")
|
||||||
|
|
||||||
|
// Restore original and hash again
|
||||||
|
SetKeyMap(original)
|
||||||
|
hash2 := Hash("abc")
|
||||||
|
|
||||||
|
assert.NotEqual(t, hash1, hash2, "different key maps should produce different hashes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetKeyMap_Good(t *testing.T) {
|
||||||
|
km := GetKeyMap()
|
||||||
|
require.NotNil(t, km)
|
||||||
|
assert.Equal(t, '0', km['o'])
|
||||||
|
assert.Equal(t, '1', km['l'])
|
||||||
|
assert.Equal(t, '3', km['e'])
|
||||||
|
assert.Equal(t, '4', km['a'])
|
||||||
|
assert.Equal(t, 'z', km['s'])
|
||||||
|
assert.Equal(t, '7', km['t'])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSalt_Good(t *testing.T) {
|
||||||
|
// "hello" reversed is "olleh", with substitutions: o->0, l->1, l->1, e->3, h->h => "011eh" ... wait
|
||||||
|
// Actually: reversed "olleh" => o->0, l->1, l->1, e->3, h->h => "0113h"
|
||||||
|
// Let's verify by checking the hash is deterministic
|
||||||
|
hash1 := Hash("hello")
|
||||||
|
hash2 := Hash("hello")
|
||||||
|
assert.Equal(t, hash1, hash2, "salt derivation must be deterministic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateSalt_Ugly(t *testing.T) {
|
||||||
|
// Unicode input should not panic
|
||||||
|
hash := Hash("\U0001f600\U0001f601\U0001f602")
|
||||||
|
assert.Len(t, hash, 64)
|
||||||
|
}
|
||||||
230
pkg/crypt/pgp/pgp.go
Normal file
230
pkg/crypt/pgp/pgp.go
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
// Package pgp provides OpenPGP key generation, encryption, decryption,
|
||||||
|
// signing, and verification using the ProtonMail go-crypto library.
|
||||||
|
//
|
||||||
|
// Ported from Enchantrix (github.com/Snider/Enchantrix/pkg/crypt/std/pgp).
|
||||||
|
package pgp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyPair holds armored PGP public and private keys.
|
||||||
|
type KeyPair struct {
|
||||||
|
PublicKey string
|
||||||
|
PrivateKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateKeyPair generates a new PGP key pair for the given identity.
|
||||||
|
// If password is non-empty, the private key is encrypted with it.
|
||||||
|
// Returns a KeyPair with armored public and private keys.
|
||||||
|
func CreateKeyPair(name, email, password string) (*KeyPair, error) {
|
||||||
|
entity, err := openpgp.NewEntity(name, "", email, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to create entity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign all the identities
|
||||||
|
for _, id := range entity.Identities {
|
||||||
|
_ = id.SelfSignature.SignUserId(id.UserId.Id, entity.PrimaryKey, entity.PrivateKey, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt private key with password if provided
|
||||||
|
if password != "" {
|
||||||
|
err = entity.PrivateKey.Encrypt([]byte(password))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to encrypt private key: %w", err)
|
||||||
|
}
|
||||||
|
for _, subkey := range entity.Subkeys {
|
||||||
|
err = subkey.PrivateKey.Encrypt([]byte(password))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to encrypt subkey: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize public key
|
||||||
|
pubKeyBuf := new(bytes.Buffer)
|
||||||
|
pubKeyWriter, err := armor.Encode(pubKeyBuf, openpgp.PublicKeyType, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to create armored public key writer: %w", err)
|
||||||
|
}
|
||||||
|
if err := entity.Serialize(pubKeyWriter); err != nil {
|
||||||
|
pubKeyWriter.Close()
|
||||||
|
return nil, fmt.Errorf("pgp: failed to serialize public key: %w", err)
|
||||||
|
}
|
||||||
|
pubKeyWriter.Close()
|
||||||
|
|
||||||
|
// Serialize private key
|
||||||
|
privKeyBuf := new(bytes.Buffer)
|
||||||
|
privKeyWriter, err := armor.Encode(privKeyBuf, openpgp.PrivateKeyType, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to create armored private key writer: %w", err)
|
||||||
|
}
|
||||||
|
if password != "" {
|
||||||
|
// Manual serialization to avoid re-signing encrypted keys
|
||||||
|
if err := serializeEncryptedEntity(privKeyWriter, entity); err != nil {
|
||||||
|
privKeyWriter.Close()
|
||||||
|
return nil, fmt.Errorf("pgp: failed to serialize private key: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := entity.SerializePrivate(privKeyWriter, nil); err != nil {
|
||||||
|
privKeyWriter.Close()
|
||||||
|
return nil, fmt.Errorf("pgp: failed to serialize private key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
privKeyWriter.Close()
|
||||||
|
|
||||||
|
return &KeyPair{
|
||||||
|
PublicKey: pubKeyBuf.String(),
|
||||||
|
PrivateKey: privKeyBuf.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializeEncryptedEntity manually serializes an entity with encrypted private keys
|
||||||
|
// to avoid the panic from re-signing encrypted keys.
|
||||||
|
func serializeEncryptedEntity(w io.Writer, e *openpgp.Entity) error {
|
||||||
|
if err := e.PrivateKey.Serialize(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, ident := range e.Identities {
|
||||||
|
if err := ident.UserId.Serialize(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ident.SelfSignature.Serialize(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, subkey := range e.Subkeys {
|
||||||
|
if err := subkey.PrivateKey.Serialize(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := subkey.Sig.Serialize(w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts data for the recipient identified by their armored public key.
|
||||||
|
// Returns the encrypted data as armored PGP output.
|
||||||
|
func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) {
|
||||||
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(publicKeyArmor)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to read public key ring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
armoredWriter, err := armor.Encode(buf, "PGP MESSAGE", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to create armor encoder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := openpgp.Encrypt(armoredWriter, keyring, nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
armoredWriter.Close()
|
||||||
|
return nil, fmt.Errorf("pgp: failed to create encryption writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.Write(data); err != nil {
|
||||||
|
w.Close()
|
||||||
|
armoredWriter.Close()
|
||||||
|
return nil, fmt.Errorf("pgp: failed to write data: %w", err)
|
||||||
|
}
|
||||||
|
w.Close()
|
||||||
|
armoredWriter.Close()
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts armored PGP data using the given armored private key.
|
||||||
|
// If the private key is encrypted, the password is used to decrypt it first.
|
||||||
|
func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) {
|
||||||
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(privateKeyArmor)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to read private key ring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt the private key if it is encrypted
|
||||||
|
for _, entity := range keyring {
|
||||||
|
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
|
||||||
|
if err := entity.PrivateKey.Decrypt([]byte(password)); err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to decrypt private key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, subkey := range entity.Subkeys {
|
||||||
|
if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted {
|
||||||
|
_ = subkey.PrivateKey.Decrypt([]byte(password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode armored message
|
||||||
|
block, err := armor.Decode(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to decode armored message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
md, err := openpgp.ReadMessage(block.Body, keyring, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to read message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := io.ReadAll(md.UnverifiedBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to read plaintext: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign creates an armored detached signature for the given data using
|
||||||
|
// the armored private key. If the key is encrypted, the password is used
|
||||||
|
// to decrypt it first.
|
||||||
|
func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) {
|
||||||
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(privateKeyArmor)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to read private key ring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := keyring[0]
|
||||||
|
if signer.PrivateKey == nil {
|
||||||
|
return nil, fmt.Errorf("pgp: private key not found in keyring")
|
||||||
|
}
|
||||||
|
|
||||||
|
if signer.PrivateKey.Encrypted {
|
||||||
|
if err := signer.PrivateKey.Decrypt([]byte(password)); err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to decrypt private key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
config := &packet.Config{}
|
||||||
|
err = openpgp.ArmoredDetachSign(buf, signer, bytes.NewReader(data), config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgp: failed to sign message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify verifies an armored detached signature against the given data
|
||||||
|
// and armored public key. Returns nil if the signature is valid.
|
||||||
|
func Verify(data, signature []byte, publicKeyArmor string) error {
|
||||||
|
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader([]byte(publicKeyArmor)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pgp: failed to read public key ring: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(data), bytes.NewReader(signature), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pgp: signature verification failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
164
pkg/crypt/pgp/pgp_test.go
Normal file
164
pkg/crypt/pgp/pgp_test.go
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
package pgp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateKeyPair_Good(t *testing.T) {
|
||||||
|
kp, err := CreateKeyPair("Test User", "test@example.com", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, kp)
|
||||||
|
assert.Contains(t, kp.PublicKey, "-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
||||||
|
assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateKeyPair_Bad(t *testing.T) {
|
||||||
|
// Empty name still works (openpgp allows it), but test with password
|
||||||
|
kp, err := CreateKeyPair("Secure User", "secure@example.com", "strong-password")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, kp)
|
||||||
|
assert.Contains(t, kp.PublicKey, "-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
||||||
|
assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateKeyPair_Ugly(t *testing.T) {
|
||||||
|
// Minimal identity
|
||||||
|
kp, err := CreateKeyPair("", "", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, kp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Good(t *testing.T) {
|
||||||
|
kp, err := CreateKeyPair("Test User", "test@example.com", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext := []byte("hello, OpenPGP!")
|
||||||
|
ciphertext, err := Encrypt(plaintext, kp.PublicKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, ciphertext)
|
||||||
|
assert.Contains(t, string(ciphertext), "-----BEGIN PGP MESSAGE-----")
|
||||||
|
|
||||||
|
decrypted, err := Decrypt(ciphertext, kp.PrivateKey, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Bad(t *testing.T) {
|
||||||
|
kp1, err := CreateKeyPair("User One", "one@example.com", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
kp2, err := CreateKeyPair("User Two", "two@example.com", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext := []byte("secret data")
|
||||||
|
ciphertext, err := Encrypt(plaintext, kp1.PublicKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Decrypting with wrong key should fail
|
||||||
|
_, err = Decrypt(ciphertext, kp2.PrivateKey, "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Ugly(t *testing.T) {
|
||||||
|
// Invalid public key for encryption
|
||||||
|
_, err := Encrypt([]byte("data"), "not-a-pgp-key")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Invalid private key for decryption
|
||||||
|
_, err = Decrypt([]byte("data"), "not-a-pgp-key", "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecryptWithPassword_Good(t *testing.T) {
|
||||||
|
password := "my-secret-passphrase"
|
||||||
|
kp, err := CreateKeyPair("Secure User", "secure@example.com", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext := []byte("encrypted with password-protected key")
|
||||||
|
ciphertext, err := Encrypt(plaintext, kp.PublicKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := Decrypt(ciphertext, kp.PrivateKey, password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignVerify_Good(t *testing.T) {
|
||||||
|
kp, err := CreateKeyPair("Signer", "signer@example.com", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data := []byte("message to sign")
|
||||||
|
signature, err := Sign(data, kp.PrivateKey, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, signature)
|
||||||
|
assert.Contains(t, string(signature), "-----BEGIN PGP SIGNATURE-----")
|
||||||
|
|
||||||
|
err = Verify(data, signature, kp.PublicKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignVerify_Bad(t *testing.T) {
|
||||||
|
kp, err := CreateKeyPair("Signer", "signer@example.com", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data := []byte("original message")
|
||||||
|
signature, err := Sign(data, kp.PrivateKey, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify with tampered data should fail
|
||||||
|
err = Verify([]byte("tampered message"), signature, kp.PublicKey)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignVerify_Ugly(t *testing.T) {
|
||||||
|
// Invalid key for signing
|
||||||
|
_, err := Sign([]byte("data"), "not-a-key", "")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Invalid key for verification
|
||||||
|
kp, err := CreateKeyPair("Signer", "signer@example.com", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data := []byte("message")
|
||||||
|
sig, err := Sign(data, kp.PrivateKey, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = Verify(data, sig, "not-a-key")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignVerifyWithPassword_Good(t *testing.T) {
|
||||||
|
password := "signing-password"
|
||||||
|
kp, err := CreateKeyPair("Signer", "signer@example.com", password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data := []byte("signed with password-protected key")
|
||||||
|
signature, err := Sign(data, kp.PrivateKey, password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = Verify(data, signature, kp.PublicKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFullRoundTrip_Good(t *testing.T) {
|
||||||
|
// Generate keys, encrypt, decrypt, sign, and verify - full round trip
|
||||||
|
kp, err := CreateKeyPair("Full Test", "full@example.com", "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
original := []byte("full round-trip test data")
|
||||||
|
|
||||||
|
// Encrypt then decrypt
|
||||||
|
ciphertext, err := Encrypt(original, kp.PublicKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
decrypted, err := Decrypt(ciphertext, kp.PrivateKey, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, original, decrypted)
|
||||||
|
|
||||||
|
// Sign then verify
|
||||||
|
signature, err := Sign(original, kp.PrivateKey, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = Verify(original, signature, kp.PublicKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
101
pkg/crypt/rsa/rsa.go
Normal file
101
pkg/crypt/rsa/rsa.go
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Package rsa provides RSA key generation, encryption, and decryption
|
||||||
|
// using OAEP with SHA-256.
|
||||||
|
//
|
||||||
|
// Ported from Enchantrix (github.com/Snider/Enchantrix/pkg/crypt/std/rsa).
|
||||||
|
package rsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyPair holds PEM-encoded RSA public and private keys.
|
||||||
|
type KeyPair struct {
|
||||||
|
PublicKey string
|
||||||
|
PrivateKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateKeyPair creates a new RSA key pair of the given bit size.
|
||||||
|
// The minimum accepted key size is 2048 bits.
|
||||||
|
// Returns a KeyPair with PEM-encoded public and private keys.
|
||||||
|
func GenerateKeyPair(bits int) (*KeyPair, error) {
|
||||||
|
if bits < 2048 {
|
||||||
|
return nil, fmt.Errorf("rsa: key size too small: %d (minimum 2048)", bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
privKey, err := rsa.GenerateKey(rand.Reader, bits)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("rsa: 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, fmt.Errorf("rsa: failed to marshal public key: %w", err)
|
||||||
|
}
|
||||||
|
pubKeyPEM := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "PUBLIC KEY",
|
||||||
|
Bytes: pubKeyBytes,
|
||||||
|
})
|
||||||
|
|
||||||
|
return &KeyPair{
|
||||||
|
PublicKey: string(pubKeyPEM),
|
||||||
|
PrivateKey: string(privKeyPEM),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts data with the given PEM-encoded public key using RSA-OAEP
|
||||||
|
// with SHA-256.
|
||||||
|
func Encrypt(data []byte, publicKeyPEM string) ([]byte, error) {
|
||||||
|
block, _ := pem.Decode([]byte(publicKeyPEM))
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("rsa: failed to decode public key PEM")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("rsa: failed to parse public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rsaPub, ok := pub.(*rsa.PublicKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("rsa: not an RSA public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaPub, data, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("rsa: failed to encrypt data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data with the given PEM-encoded private key using RSA-OAEP
|
||||||
|
// with SHA-256.
|
||||||
|
func Decrypt(data []byte, privateKeyPEM string) ([]byte, error) {
|
||||||
|
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||||
|
if block == nil {
|
||||||
|
return nil, fmt.Errorf("rsa: failed to decode private key PEM")
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("rsa: failed to parse private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, priv, data, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("rsa: failed to decrypt data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
89
pkg/crypt/rsa/rsa_test.go
Normal file
89
pkg/crypt/rsa/rsa_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
package rsa
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateKeyPair_Good(t *testing.T) {
|
||||||
|
kp, err := GenerateKeyPair(2048)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, kp)
|
||||||
|
assert.Contains(t, kp.PublicKey, "-----BEGIN PUBLIC KEY-----")
|
||||||
|
assert.Contains(t, kp.PrivateKey, "-----BEGIN RSA PRIVATE KEY-----")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateKeyPair_Bad(t *testing.T) {
|
||||||
|
// Key size too small
|
||||||
|
_, err := GenerateKeyPair(1024)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "key size too small")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateKeyPair_Ugly(t *testing.T) {
|
||||||
|
// Zero bits
|
||||||
|
_, err := GenerateKeyPair(0)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Good(t *testing.T) {
|
||||||
|
kp, err := GenerateKeyPair(2048)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext := []byte("hello, RSA-OAEP with SHA-256!")
|
||||||
|
ciphertext, err := Encrypt(plaintext, kp.PublicKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, plaintext, ciphertext)
|
||||||
|
|
||||||
|
decrypted, err := Decrypt(ciphertext, kp.PrivateKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, decrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Bad(t *testing.T) {
|
||||||
|
kp1, err := GenerateKeyPair(2048)
|
||||||
|
require.NoError(t, err)
|
||||||
|
kp2, err := GenerateKeyPair(2048)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
plaintext := []byte("secret data")
|
||||||
|
ciphertext, err := Encrypt(plaintext, kp1.PublicKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Decrypting with wrong private key should fail
|
||||||
|
_, err = Decrypt(ciphertext, kp2.PrivateKey)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecrypt_Ugly(t *testing.T) {
|
||||||
|
// Invalid PEM for encryption
|
||||||
|
_, err := Encrypt([]byte("data"), "not-a-pem-key")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Invalid PEM for decryption
|
||||||
|
_, err = Decrypt([]byte("data"), "not-a-pem-key")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecryptRoundTrip_Good(t *testing.T) {
|
||||||
|
kp, err := GenerateKeyPair(2048)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
messages := []string{
|
||||||
|
"",
|
||||||
|
"a",
|
||||||
|
"short message",
|
||||||
|
"a slightly longer message with some special chars: !@#$%^&*()",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, msg := range messages {
|
||||||
|
ciphertext, err := Encrypt([]byte(msg), kp.PublicKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
decrypted, err := Decrypt(ciphertext, kp.PrivateKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, msg, string(decrypted), "round-trip failed for: %q", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue