Merge pull request #33 from Snider/feature-pgp-implementation

Feature pgp implementation
This commit is contained in:
Snider 2025-11-13 20:32:41 +00:00 committed by GitHub
commit d649e9e69e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 839 additions and 342 deletions

View file

@ -1,8 +1,13 @@
# Enchantrix
[![Go Report Card](https://goreportcard.com/badge/github.com/Snider/Enchantrix)](https://goreportcard.com/report/github.com/Snider/Enchantrix)
[![GoDoc](https://godoc.org/github.com/Snider/Enchantrix?status.svg)](https://godoc.org/github.com/Snider/Enchantrix)
[![Build Status](https://github.com/Snider/Enchantrix/actions/workflows/go.yml/badge.svg)](https://github.com/Snider/Enchantrix/actions/workflows/go.yml)
[![codecov](https://codecov.io/github/Snider/Enchantrix/branch/main/graph/badge.svg?token=2E1QWEDFUW)](https://codecov.io/github/Snider/Enchantrix)
[![Release](https://img.shields.io/github/release/Snider/Enchantrix.svg)](https://github.com/Snider/Enchantrix/releases/latest)
[![License](https://img.shields.io/github/license/Snider/Enchantrix)](https://github.com/Snider/Enchantrix/blob/main/LICENCE)
Enchantrix is a Go-based encryption library for the Core framework, designed to provide a secure and easy-to-use framework for handling sensitive data in Web3 applications. It will feature Poly-ChaCha stream proxying and a custom `.trix` file format for encrypted data.
Enchantrix is a Go-based encryption library designed to provide a secure and easy-to-use framework for handling sensitive data in Web3 applications. It will feature Poly-ChaCha stream proxying and a custom `.trix` file format for encrypted data.
## Documentation

View file

@ -2,105 +2,148 @@ package main
import (
"bytes"
"errors"
"io"
"os"
"os/exec"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
// executeCommand executes the root command with the given arguments and returns the output.
func executeCommand(args ...string) (string, error) {
b := new(bytes.Buffer)
rootCmd.SetOut(b)
rootCmd.SetErr(b)
rootCmd.SetArgs(args)
err := rootCmd.Execute()
return b.String(), err
func TestMain_Good(t *testing.T) {
// Redirect stdout to a buffer
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Run the main function
main()
// Restore stdout
w.Close()
os.Stdout = old
// Read the output from the buffer
var buf bytes.Buffer
io.Copy(&buf, r)
// Check that the output contains the help message
assert.Contains(t, buf.String(), "Usage:")
}
// executeCommandWithStdin executes the root command with the given arguments and stdin,
// and returns the output.
func executeCommandWithStdin(stdin string, args ...string) (string, error) {
b := new(bytes.Buffer)
rootCmd.SetOut(b)
rootCmd.SetErr(b)
rootCmd.SetIn(strings.NewReader(stdin))
rootCmd.SetArgs(args)
err := rootCmd.Execute()
// reset stdin
rootCmd.SetIn(os.Stdin)
return b.String(), err
}
func TestRootCommand(t *testing.T) {
output, err := executeCommand()
assert.NoError(t, err)
assert.Contains(t, output, "trix [command]")
}
func TestEncodeDecodeCommand(t *testing.T) {
// 1. Create original payload
originalPayload := "hello world"
inputFile, _ := os.CreateTemp("", "input")
defer os.Remove(inputFile.Name())
inputFile.Write([]byte(originalPayload))
inputFile.Close()
// 2. Encode it to a file
encodedFile, _ := os.CreateTemp("", "encoded")
defer os.Remove(encodedFile.Name())
_, err := executeCommand("encode", "-i", inputFile.Name(), "-o", encodedFile.Name(), "-m", "magc", "reverse")
assert.NoError(t, err)
// 3. Decode it back
decodedFile, _ := os.CreateTemp("", "decoded")
defer os.Remove(decodedFile.Name())
_, err = executeCommand("decode", "-i", encodedFile.Name(), "-o", decodedFile.Name(), "-m", "magc", "reverse")
assert.NoError(t, err)
// 4. Verify content
finalPayload, err := os.ReadFile(decodedFile.Name())
assert.NoError(t, err)
assert.Equal(t, originalPayload, string(finalPayload))
}
func TestHashCommand(t *testing.T) {
// Test with input file
inputFile, _ := os.CreateTemp("", "input")
defer os.Remove(inputFile.Name())
inputFile.Write([]byte("hello"))
inputFile.Close()
output, err := executeCommand("hash", "md5", "-i", inputFile.Name())
assert.NoError(t, err)
assert.Equal(t, "5d41402abc4b2a76b9719d911017c592", strings.TrimSpace(output))
// Test with stdin
output, err = executeCommandWithStdin("hello", "hash", "md5")
assert.NoError(t, err)
assert.Equal(t, "5d41402abc4b2a76b9719d911017c592", strings.TrimSpace(output))
// Test error cases
_, err = executeCommand("hash")
assert.Error(t, err)
_, err = executeCommand("hash", "invalid-algo")
assert.Error(t, err)
_, err = executeCommand("hash", "md5", "-i", "nonexistent-file")
assert.Error(t, err)
}
func TestMainFunction(t *testing.T) {
// This test is to ensure the main function is covered
// We run it in a separate process to avoid os.Exit calls
if os.Getenv("GO_TEST_MAIN") == "1" {
main()
return
func TestMain_Bad(t *testing.T) {
oldExit := exit
defer func() { exit = oldExit }()
var exitCode int
exit = func(code int) {
exitCode = code
}
cmd := exec.Command(os.Args[0], "-test.run=TestMainFunction")
cmd.Env = append(os.Environ(), "GO_TEST_MAIN=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
t.Fatalf("main function exited with error: %v", err)
rootCmd.RunE = func(cmd *cobra.Command, args []string) error {
return errors.New("test error")
}
// The rootCmd needs to be reset so that the test can be run again
defer func() { rootCmd = &cobra.Command{
Use: "trix",
Short: "A tool for encoding and decoding .trix files",
Long: `trix is a command-line tool for working with the .trix file format, which is used for storing encrypted data.`,
}
}()
main()
assert.Equal(t, 1, exitCode)
}
func TestHandleSigil_Good(t *testing.T) {
// Create a dummy command
cmd := &cobra.Command{}
buf := new(bytes.Buffer)
cmd.SetOut(buf)
// Run the handleSigil function
err := handleSigil(cmd, "base64", "hello")
assert.NoError(t, err)
// Check that the output is the base64 encoded string
assert.Equal(t, "aGVsbG8=", strings.TrimSpace(buf.String()))
}
func TestHandleSigil_Bad(t *testing.T) {
cmd := &cobra.Command{}
err := handleSigil(cmd, "bad-sigil", "hello")
assert.Error(t, err)
}
func TestRunEncodeAndDecode_Good(t *testing.T) {
// Encode
encodeCmd := &cobra.Command{}
encodeBuf := new(bytes.Buffer)
encodeCmd.SetOut(encodeBuf)
encodeCmd.SetIn(strings.NewReader("hello"))
encodeCmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
encodeCmd.Flags().StringP("output", "o", "-", "Output file")
encodeCmd.Flags().StringP("magic", "m", "TEST", "Magic number (4 bytes)")
err := runEncode(encodeCmd, []string{"base64"})
assert.NoError(t, err)
assert.NotEmpty(t, encodeBuf.String())
// Decode
decodeCmd := &cobra.Command{}
decodeBuf := new(bytes.Buffer)
decodeCmd.SetOut(decodeBuf)
decodeCmd.SetIn(encodeBuf) // Use the output of the encode as the input for the decode
decodeCmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
decodeCmd.Flags().StringP("output", "o", "-", "Output file")
decodeCmd.Flags().StringP("magic", "m", "TEST", "Magic number (4 bytes)")
err = runDecode(decodeCmd, []string{"base64"})
assert.NoError(t, err)
assert.Equal(t, "hello", strings.TrimSpace(decodeBuf.String()))
}
func TestRunEncode_Bad(t *testing.T) {
cmd := &cobra.Command{}
cmd.Flags().StringP("magic", "m", "bad", "Magic number (4 bytes)")
err := runEncode(cmd, []string{})
assert.Error(t, err)
}
func TestRunDecode_Bad(t *testing.T) {
cmd := &cobra.Command{}
cmd.Flags().StringP("magic", "m", "bad", "Magic number (4 bytes)")
err := runDecode(cmd, []string{})
assert.Error(t, err)
}
func TestRunHash_Good(t *testing.T) {
cmd := &cobra.Command{}
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetIn(strings.NewReader("hello"))
cmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
// Run the runHash function
err := runHash(cmd, []string{"sha256"})
assert.NoError(t, err)
// Check that the output is not empty
assert.NotEmpty(t, buf.String())
}
func TestRunHash_Bad(t *testing.T) {
cmd := &cobra.Command{}
err := runHash(cmd, []string{"bad-hash"})
assert.Error(t, err)
}
func TestCreateSigilRunE_Good(t *testing.T) {
cmd := &cobra.Command{}
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetIn(strings.NewReader("hello"))
cmd.Flags().StringP("input", "i", "-", "Input file or string (or stdin)")
// Run the createSigilRunE function
runE := createSigilRunE("base64")
err := runE(cmd, []string{})
assert.NoError(t, err)
}

View file

@ -0,0 +1,27 @@
package main
import (
"fmt"
"github.com/Snider/Enchantrix/pkg/crypt"
)
func main() {
fmt.Println("--- Checksum Demo ---")
cryptService := crypt.NewService()
// Luhn
luhnPayloadGood := "49927398716"
luhnPayloadBad := "49927398717"
fmt.Printf("Luhn Checksum:\n")
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadGood, cryptService.Luhn(luhnPayloadGood))
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadBad, cryptService.Luhn(luhnPayloadBad))
// Fletcher
fletcherPayload := "abcde"
fmt.Printf("\nFletcher Checksums (Payload: \"%s\"):\n", fletcherPayload)
fmt.Printf(" - Fletcher16: %d\n", cryptService.Fletcher16(fletcherPayload))
fmt.Printf(" - Fletcher32: %d\n", cryptService.Fletcher32(fletcherPayload))
fmt.Printf(" - Fletcher64: %d\n", cryptService.Fletcher64(fletcherPayload))
fmt.Println()
}

View file

@ -0,0 +1,21 @@
package main
import (
"fmt"
)
func main() {
fmt.Println("--- Test Coverage Demo ---")
fmt.Println("")
fmt.Println("This example demonstrates how to generate and interpret a test coverage report.")
fmt.Println("")
fmt.Println("1. Generate a coverage profile:")
fmt.Println(" go test ./... -coverprofile=coverage.out")
fmt.Println("")
fmt.Println("2. View the coverage report in your browser:")
fmt.Println(" go tool cover -html=coverage.out")
fmt.Println("")
fmt.Println("3. View the coverage report in your terminal:")
fmt.Println(" go tool cover -func=coverage.out")
fmt.Println("")
}

57
examples/examples_test.go Normal file
View file

@ -0,0 +1,57 @@
package examples_test
import (
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExample_Checksums(t *testing.T) {
cmd := exec.Command("go", "run", ".")
cmd.Dir = "./checksums"
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}
func TestExample_Hash(t *testing.T) {
cmd := exec.Command("go", "run", ".")
cmd.Dir = "./hash"
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}
func TestExample_PGPEncryptDecrypt(t *testing.T) {
cmd := exec.Command("go", "run", ".")
cmd.Dir = "./pgp_encrypt_decrypt"
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}
func TestExample_PGPGenerateKeys(t *testing.T) {
cmd := exec.Command("go", "run", ".")
cmd.Dir = "./pgp_generate_keys"
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}
func TestExample_PGPSignVerify(t *testing.T) {
cmd := exec.Command("go", "run", ".")
cmd.Dir = "./pgp_sign_verify"
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}
func TestExample_PGPSymmetricEncrypt(t *testing.T) {
cmd := exec.Command("go", "run", ".")
cmd.Dir = "./pgp_symmetric_encrypt"
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}
func TestExample_RSA(t *testing.T) {
cmd := exec.Command("go", "run", ".")
cmd.Dir = "./rsa"
out, err := cmd.CombinedOutput()
assert.NoError(t, err, string(out))
}

28
examples/hash/main.go Normal file
View file

@ -0,0 +1,28 @@
package main
import (
"fmt"
"github.com/Snider/Enchantrix/pkg/crypt"
)
func main() {
fmt.Println("--- Hashing Demo ---")
cryptService := crypt.NewService()
payload := "Enchantrix"
hashTypes := []crypt.HashType{
crypt.LTHN,
crypt.MD5,
crypt.SHA1,
crypt.SHA256,
crypt.SHA512,
}
fmt.Printf("Payload to hash: \"%s\"\n", payload)
for _, hashType := range hashTypes {
hash := cryptService.Hash(hashType, payload)
fmt.Printf(" - %-6s: %s\n", hashType, hash)
}
fmt.Println()
}

View file

@ -1,250 +0,0 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"time"
"github.com/Snider/Enchantrix/pkg/crypt"
"github.com/Snider/Enchantrix/pkg/crypt/std/chachapoly"
"github.com/Snider/Enchantrix/pkg/enchantrix"
"github.com/Snider/Enchantrix/pkg/trix"
)
func main() {
demoTrix()
demoHashing()
demoChecksums()
demoRSA()
demoSigils()
}
func demoTrix() {
fmt.Println("--- Trix & Sigil Chaining Demo ---")
// 1. Original plaintext (JSON data) and encryption key
type Message struct {
Author string `json:"author"`
Time int64 `json:"time"`
Body string `json:"body"`
}
originalMessage := Message{Author: "Jules", Time: time.Now().Unix(), Body: "This is a super secret message!"}
plaintext, err := json.Marshal(originalMessage)
if err != nil {
log.Fatalf("Failed to marshal JSON: %v", err)
}
key := make([]byte, 32) // In a real application, use a secure key
for i := range key {
key[i] = 1
}
fmt.Printf("Original Payload (JSON):\n%s\n\n", plaintext)
// 2. Create a Trix container with the plaintext and attach a chain of sigils
sigilChain := []string{"json-indent", "gzip", "base64", "reverse"}
trixContainer := &trix.Trix{
Header: map[string]interface{}{},
Payload: plaintext,
InSigils: sigilChain,
}
// 3. Pack the Trix container to apply the sigil transformations
fmt.Println("Packing payload with sigils:", sigilChain)
if err := trixContainer.Pack(); err != nil {
log.Fatalf("Failed to pack trix container: %v", err)
}
fmt.Printf("Packed (obfuscated) payload is now non-human-readable bytes.\n\n")
// 4. Encrypt the packed payload
ciphertext, err := chachapoly.Encrypt(trixContainer.Payload, key)
if err != nil {
log.Fatalf("Failed to encrypt: %v", err)
}
trixContainer.Payload = ciphertext // Update the payload with the ciphertext
// 5. Add encryption metadata and checksum to the header
nonce := ciphertext[:24]
trixContainer.Header = map[string]interface{}{
"content_type": "application/json",
"encryption_algorithm": "chacha20poly1305",
"nonce": base64.StdEncoding.EncodeToString(nonce),
"created_at": time.Now().UTC().Format(time.RFC3339),
}
trixContainer.ChecksumAlgo = crypt.SHA512
fmt.Printf("Checksum will be calculated with %s and added to the header.\n", trixContainer.ChecksumAlgo)
// 6. Encode the .trix container into its binary format
magicNumber := "MyT1"
encodedTrix, err := trix.Encode(trixContainer, magicNumber, nil)
if err != nil {
log.Fatalf("Failed to encode .trix container: %v", err)
}
fmt.Println("Successfully created .trix container.")
// --- DECODING ---
fmt.Println("--- DECODING ---")
// 7. Decode the .trix container
decodedTrix, err := trix.Decode(encodedTrix, magicNumber, nil)
if err != nil {
log.Fatalf("Failed to decode .trix container: %v", err)
}
fmt.Println("Successfully decoded .trix container. Checksum verified.")
fmt.Printf("Decoded Header: %+v\n", decodedTrix.Header)
// 8. Decrypt the payload
decryptedPayload, err := chachapoly.Decrypt(decodedTrix.Payload, key)
if err != nil {
log.Fatalf("Failed to decrypt: %v", err)
}
decodedTrix.Payload = decryptedPayload
fmt.Println("Payload decrypted.")
// 9. Unpack the Trix container to reverse the sigil transformations
decodedTrix.InSigils = trixContainer.InSigils // Re-attach sigils for unpacking
fmt.Println("Unpacking payload by reversing sigils:", decodedTrix.InSigils)
if err := decodedTrix.Unpack(); err != nil {
log.Fatalf("Failed to unpack trix container: %v", err)
}
fmt.Printf("Unpacked (original) payload:\n%s\n", decodedTrix.Payload)
// 10. Verify the result
// To properly verify, we need to compact the indented JSON before comparing
var compactedPayload bytes.Buffer
if err := json.Compact(&compactedPayload, decodedTrix.Payload); err != nil {
log.Fatalf("Failed to compact final payload for verification: %v", err)
}
if bytes.Equal(plaintext, compactedPayload.Bytes()) {
fmt.Println("\nSuccess! The message was decrypted and unpacked correctly.")
} else {
fmt.Println("\nFailure! The final payload does not match the original.")
}
fmt.Println()
}
func demoHashing() {
fmt.Println("--- Hashing Demo ---")
cryptService := crypt.NewService()
payload := "Enchantrix"
hashTypes := []crypt.HashType{
crypt.LTHN,
crypt.MD5,
crypt.SHA1,
crypt.SHA256,
crypt.SHA512,
}
fmt.Printf("Payload to hash: \"%s\"\n", payload)
for _, hashType := range hashTypes {
hash := cryptService.Hash(hashType, payload)
fmt.Printf(" - %-6s: %s\n", hashType, hash)
}
fmt.Println()
}
func demoChecksums() {
fmt.Println("--- Checksum Demo ---")
cryptService := crypt.NewService()
// Luhn
luhnPayloadGood := "49927398716"
luhnPayloadBad := "49927398717"
fmt.Printf("Luhn Checksum:\n")
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadGood, cryptService.Luhn(luhnPayloadGood))
fmt.Printf(" - Payload '%s' is valid: %v\n", luhnPayloadBad, cryptService.Luhn(luhnPayloadBad))
// Fletcher
fletcherPayload := "abcde"
fmt.Printf("\nFletcher Checksums (Payload: \"%s\"):\n", fletcherPayload)
fmt.Printf(" - Fletcher16: %d\n", cryptService.Fletcher16(fletcherPayload))
fmt.Printf(" - Fletcher32: %d\n", cryptService.Fletcher32(fletcherPayload))
fmt.Printf(" - Fletcher64: %d\n", cryptService.Fletcher64(fletcherPayload))
fmt.Println()
}
func demoRSA() {
fmt.Println("--- RSA Demo ---")
cryptService := crypt.NewService()
// 1. Generate RSA key pair
fmt.Println("Generating 2048-bit RSA key pair...")
publicKey, privateKey, err := cryptService.GenerateRSAKeyPair(2048)
if err != nil {
log.Fatalf("Failed to generate RSA key pair: %v", err)
}
fmt.Println("Key pair generated successfully.")
// 2. Encrypt a message
message := []byte("This is a secret message for RSA.")
fmt.Printf("\nOriginal message: %s\n", message)
ciphertext, err := cryptService.EncryptRSA(publicKey, message, nil)
if err != nil {
log.Fatalf("Failed to encrypt with RSA: %v", err)
}
fmt.Printf("Encrypted ciphertext (base64): %s\n", base64.StdEncoding.EncodeToString(ciphertext))
// 3. Decrypt the message
decrypted, err := cryptService.DecryptRSA(privateKey, ciphertext, nil)
if err != nil {
log.Fatalf("Failed to decrypt with RSA: %v", err)
}
fmt.Printf("Decrypted message: %s\n", decrypted)
// 4. Verify
if string(message) == string(decrypted) {
fmt.Println("\nSuccess! RSA decrypted message matches the original.")
} else {
fmt.Println("\nFailure! RSA decrypted message does not match the original.")
}
fmt.Println()
}
func demoSigils() {
fmt.Println("--- Standalone Sigil Demo ---")
data := []byte(`{"message": "hello world"}`)
fmt.Printf("Original data: %s\n", data)
// A chain of sigils to apply
sigils := []string{"gzip", "base64"}
fmt.Printf("Applying sigil chain: %v\n", sigils)
var transformedData = data
for _, name := range sigils {
s, err := enchantrix.NewSigil(name)
if err != nil {
log.Fatalf("Failed to create sigil %s: %v", name, err)
}
transformedData, err = s.In(transformedData)
if err != nil {
log.Fatalf("Failed to apply sigil %s 'In': %v", name, err)
}
fmt.Printf(" -> After '%s': %s\n", name, transformedData)
}
fmt.Println("\nReversing sigil chain...")
// Reverse the transformations
for i := len(sigils) - 1; i >= 0; i-- {
name := sigils[i]
s, err := enchantrix.NewSigil(name)
if err != nil {
log.Fatalf("Failed to create sigil %s: %v", name, err)
}
transformedData, err = s.Out(transformedData)
if err != nil {
log.Fatalf("Failed to apply sigil %s 'Out': %v", name, err)
}
fmt.Printf(" -> After '%s' Out: %s\n", name, transformedData)
}
if string(data) == string(transformedData) {
fmt.Println("Success! Data returned to original state.")
} else {
fmt.Println("Failure! Data did not return to original state.")
}
fmt.Println()
}

View file

@ -0,0 +1,45 @@
package main
import (
"fmt"
"log"
"github.com/Snider/Enchantrix/pkg/crypt"
)
func main() {
fmt.Println("--- PGP Encryption & Decryption Demo ---")
cryptService := crypt.NewService()
// 1. Generate PGP key pair
fmt.Println("Generating PGP key pair...")
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
if err != nil {
log.Fatalf("Failed to generate PGP key pair: %v", err)
}
fmt.Println("Key pair generated successfully.")
// 2. Encrypt a message
message := []byte("This is a secret message for PGP.")
fmt.Printf("\nOriginal message: %s\n", message)
ciphertext, err := cryptService.EncryptPGP(publicKey, message)
if err != nil {
log.Fatalf("Failed to encrypt with PGP: %v", err)
}
fmt.Printf("Encrypted ciphertext (armored):\n%s\n", ciphertext)
// 3. Decrypt the message
decrypted, err := cryptService.DecryptPGP(privateKey, ciphertext)
if err != nil {
log.Fatalf("Failed to decrypt with PGP: %v", err)
}
fmt.Printf("Decrypted message: %s\n", decrypted)
// 4. Verify
if string(message) == string(decrypted) {
fmt.Println("\nSuccess! PGP decrypted message matches the original.")
} else {
fmt.Println("\nFailure! PGP decrypted message does not match the original.")
}
fmt.Println()
}

View file

@ -0,0 +1,23 @@
package main
import (
"fmt"
"log"
"github.com/Snider/Enchantrix/pkg/crypt"
)
func main() {
fmt.Println("--- PGP Key Generation Demo ---")
cryptService := crypt.NewService()
// 1. Generate PGP key pair
fmt.Println("Generating PGP key pair...")
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
if err != nil {
log.Fatalf("Failed to generate PGP key pair: %v", err)
}
fmt.Println("Key pair generated successfully.")
fmt.Printf("\nPublic Key:\n%s\n", publicKey)
fmt.Printf("\nPrivate Key:\n%s\n", privateKey)
}

View file

@ -0,0 +1,38 @@
package main
import (
"fmt"
"log"
"github.com/Snider/Enchantrix/pkg/crypt"
)
func main() {
fmt.Println("--- PGP Signing & Verification Demo ---")
cryptService := crypt.NewService()
// 1. Generate PGP key pair
fmt.Println("Generating PGP key pair...")
publicKey, privateKey, err := cryptService.GeneratePGPKeyPair("test", "test@example.com", "test key")
if err != nil {
log.Fatalf("Failed to generate PGP key pair: %v", err)
}
fmt.Println("Key pair generated successfully.")
// 2. Sign a message
message := []byte("This is a message to be signed.")
fmt.Printf("\nOriginal message: %s\n", message)
signature, err := cryptService.SignPGP(privateKey, message)
if err != nil {
log.Fatalf("Failed to sign with PGP: %v", err)
}
fmt.Printf("Signature (armored):\n%s\n", signature)
// 3. Verify the signature
err = cryptService.VerifyPGP(publicKey, message, signature)
if err != nil {
log.Fatalf("Failed to verify signature: %v", err)
}
fmt.Println("Signature verified successfully!")
fmt.Println()
}

View file

@ -0,0 +1,24 @@
package main
import (
"fmt"
"log"
"github.com/Snider/Enchantrix/pkg/crypt"
)
func main() {
fmt.Println("--- PGP Symmetric Encryption Demo ---")
cryptService := crypt.NewService()
// 1. Encrypt a message with a passphrase
message := []byte("This is a secret message for symmetric PGP encryption.")
passphrase := []byte("my-secret-passphrase")
fmt.Printf("\nOriginal message: %s\n", message)
ciphertext, err := cryptService.SymmetricallyEncryptPGP(passphrase, message)
if err != nil {
log.Fatalf("Failed to encrypt with PGP: %v", err)
}
fmt.Printf("Encrypted ciphertext (armored):\n%s\n", ciphertext)
fmt.Println()
}

46
examples/rsa/main.go Normal file
View file

@ -0,0 +1,46 @@
package main
import (
"encoding/base64"
"fmt"
"log"
"github.com/Snider/Enchantrix/pkg/crypt"
)
func main() {
fmt.Println("--- RSA Demo ---")
cryptService := crypt.NewService()
// 1. Generate RSA key pair
fmt.Println("Generating 2048-bit RSA key pair...")
publicKey, privateKey, err := cryptService.GenerateRSAKeyPair(2048)
if err != nil {
log.Fatalf("Failed to generate RSA key pair: %v", err)
}
fmt.Println("Key pair generated successfully.")
// 2. Encrypt a message
message := []byte("This is a secret message for RSA.")
fmt.Printf("\nOriginal message: %s\n", message)
ciphertext, err := cryptService.EncryptRSA(publicKey, message, nil)
if err != nil {
log.Fatalf("Failed to encrypt with RSA: %v", err)
}
fmt.Printf("Encrypted ciphertext (base64): %s\n", base64.StdEncoding.EncodeToString(ciphertext))
// 3. Decrypt the message
decrypted, err := cryptService.DecryptRSA(privateKey, ciphertext, nil)
if err != nil {
log.Fatalf("Failed to decrypt with RSA: %v", err)
}
fmt.Printf("Decrypted message: %s\n", decrypted)
// 4. Verify
if string(message) == string(decrypted) {
fmt.Println("\nSuccess! RSA decrypted message matches the original.")
} else {
fmt.Println("\nFailure! RSA decrypted message does not match the original.")
}
fmt.Println()
}

2
go.mod
View file

@ -9,6 +9,8 @@ require (
)
require (
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect

4
go.sum
View file

@ -1,3 +1,7 @@
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/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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=

View file

@ -11,18 +11,21 @@ import (
"strings"
"github.com/Snider/Enchantrix/pkg/crypt/std/lthn"
"github.com/Snider/Enchantrix/pkg/crypt/std/pgp"
"github.com/Snider/Enchantrix/pkg/crypt/std/rsa"
)
// Service is the main struct for the crypt service.
type Service struct {
rsa *rsa.Service
pgp *pgp.Service
}
// NewService creates a new crypt Service and initialises its embedded RSA service.
// NewService creates a new crypt Service and initialises its embedded services.
func NewService() *Service {
return &Service{
rsa: rsa.NewService(),
pgp: pgp.NewService(),
}
}
@ -171,3 +174,48 @@ func (s *Service) DecryptRSA(privateKey, ciphertext, label []byte) ([]byte, erro
s.ensureRSA()
return s.rsa.Decrypt(privateKey, ciphertext, label)
}
// --- PGP ---
// ensurePGP initializes the PGP service if it is not already.
func (s *Service) ensurePGP() {
if s.pgp == nil {
s.pgp = pgp.NewService()
}
}
// GeneratePGPKeyPair creates a new PGP key pair.
func (s *Service) GeneratePGPKeyPair(name, email, comment string) (publicKey, privateKey []byte, err error) {
s.ensurePGP()
return s.pgp.GenerateKeyPair(name, email, comment)
}
// EncryptPGP encrypts data with a public key.
func (s *Service) EncryptPGP(publicKey, data []byte) ([]byte, error) {
s.ensurePGP()
return s.pgp.Encrypt(publicKey, data)
}
// DecryptPGP decrypts data with a private key.
func (s *Service) DecryptPGP(privateKey, ciphertext []byte) ([]byte, error) {
s.ensurePGP()
return s.pgp.Decrypt(privateKey, ciphertext)
}
// SignPGP creates a detached signature for a message.
func (s *Service) SignPGP(privateKey, data []byte) ([]byte, error) {
s.ensurePGP()
return s.pgp.Sign(privateKey, data)
}
// VerifyPGP verifies a detached signature for a message.
func (s *Service) VerifyPGP(publicKey, data, signature []byte) error {
s.ensurePGP()
return s.pgp.Verify(publicKey, data, signature)
}
// SymmetricallyEncryptPGP encrypts data with a passphrase.
func (s *Service) SymmetricallyEncryptPGP(passphrase, data []byte) ([]byte, error) {
s.ensurePGP()
return s.pgp.SymmetricallyEncrypt(passphrase, data)
}

View file

@ -126,6 +126,49 @@ func TestRSA_Good(t *testing.T) {
assert.Equal(t, message, plaintext)
}
// --- PGP Tests ---
func TestPGP_Good(t *testing.T) {
pubKey, privKey, err := service.GeneratePGPKeyPair("test", "test@test.com", "test comment")
assert.NoError(t, err)
assert.NotNil(t, pubKey)
assert.NotNil(t, privKey)
// Test encryption and decryption
message := []byte("secret message")
ciphertext, err := service.EncryptPGP(pubKey, message)
assert.NoError(t, err)
plaintext, err := service.DecryptPGP(privKey, ciphertext)
assert.NoError(t, err)
assert.Equal(t, message, plaintext)
// Test signing and verification
signature, err := service.SignPGP(privKey, message)
assert.NoError(t, err)
err = service.VerifyPGP(pubKey, message, signature)
assert.NoError(t, err)
// Test symmetric encryption
passphrase := []byte("my-secret-passphrase")
ciphertext, err = service.SymmetricallyEncryptPGP(passphrase, message)
assert.NoError(t, err)
assert.NotNil(t, ciphertext)
}
// --- IsHashAlgo Tests ---
func TestIsHashAlgo_Good(t *testing.T) {
assert.True(t, service.IsHashAlgo("lthn"))
assert.True(t, service.IsHashAlgo("sha512"))
assert.True(t, service.IsHashAlgo("sha256"))
assert.True(t, service.IsHashAlgo("sha1"))
assert.True(t, service.IsHashAlgo("md5"))
}
func TestIsHashAlgo_Bad(t *testing.T) {
assert.False(t, service.IsHashAlgo("not-a-hash"))
}
func TestRSA_Bad(t *testing.T) {
// Test with a key size that is too small
_, _, err := service.GenerateRSAKeyPair(1024)

165
pkg/crypt/std/pgp/pgp.go Normal file
View file

@ -0,0 +1,165 @@
package pgp
import (
"bytes"
"fmt"
"io"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
)
// Service is a service for PGP operations.
type Service struct{}
// NewService creates a new PGP Service.
func NewService() *Service {
return &Service{}
}
// GenerateKeyPair generates a new PGP key pair.
func (s *Service) GenerateKeyPair(name, email, comment string) (publicKey, privateKey []byte, err error) {
entity, err := openpgp.NewEntity(name, comment, email, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to create new entity: %w", err)
}
// Sign all the identities
for _, id := range entity.Identities {
err := id.SelfSignature.SignUserId(id.UserId.Id, entity.PrimaryKey, entity.PrivateKey, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign user id: %w", err)
}
}
// Public Key
pubKeyBuf := new(bytes.Buffer)
pubKeyWriter, err := armor.Encode(pubKeyBuf, openpgp.PublicKeyType, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to create armored public key writer: %w", err)
}
defer pubKeyWriter.Close()
if err := entity.Serialize(pubKeyWriter); err != nil {
return nil, nil, fmt.Errorf("failed to serialize public key: %w", err)
}
// a tricky little bastard, this one. without closing the writer, the buffer is empty.
pubKeyWriter.Close()
// Private Key
privKeyBuf := new(bytes.Buffer)
privKeyWriter, err := armor.Encode(privKeyBuf, openpgp.PrivateKeyType, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to create armored private key writer: %w", err)
}
defer privKeyWriter.Close()
if err := entity.SerializePrivate(privKeyWriter, nil); err != nil {
return nil, nil, fmt.Errorf("failed to serialize private key: %w", err)
}
// a tricky little bastard, this one. without closing the writer, the buffer is empty.
privKeyWriter.Close()
return pubKeyBuf.Bytes(), privKeyBuf.Bytes(), nil
}
// Encrypt encrypts data with a public key.
func (s *Service) Encrypt(publicKey, data []byte) ([]byte, error) {
pubKeyReader := bytes.NewReader(publicKey)
keyring, err := openpgp.ReadArmoredKeyRing(pubKeyReader)
if err != nil {
return nil, fmt.Errorf("failed to read public key ring: %w", err)
}
buf := new(bytes.Buffer)
w, err := openpgp.Encrypt(buf, keyring, nil, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to create encryption writer: %w", err)
}
defer w.Close()
_, err = w.Write(data)
if err != nil {
return nil, fmt.Errorf("failed to write data to encryption writer: %w", err)
}
w.Close()
return buf.Bytes(), nil
}
// Decrypt decrypts data with a private key.
func (s *Service) Decrypt(privateKey, ciphertext []byte) ([]byte, error) {
privKeyReader := bytes.NewReader(privateKey)
keyring, err := openpgp.ReadArmoredKeyRing(privKeyReader)
if err != nil {
return nil, fmt.Errorf("failed to read private key ring: %w", err)
}
buf := bytes.NewReader(ciphertext)
md, err := openpgp.ReadMessage(buf, keyring, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to read message: %w", err)
}
plaintext, err := io.ReadAll(md.UnverifiedBody)
if err != nil {
return nil, fmt.Errorf("failed to read plaintext: %w", err)
}
return plaintext, nil
}
// Sign creates a detached signature for a message.
func (s *Service) Sign(privateKey, data []byte) ([]byte, error) {
privKeyReader := bytes.NewReader(privateKey)
keyring, err := openpgp.ReadArmoredKeyRing(privKeyReader)
if err != nil {
return nil, fmt.Errorf("failed to read private key ring: %w", err)
}
signer := keyring[0]
if signer.PrivateKey == nil {
return nil, fmt.Errorf("private key not found in keyring")
}
buf := new(bytes.Buffer)
err = openpgp.ArmoredDetachSign(buf, signer, bytes.NewReader(data), nil)
if err != nil {
return nil, fmt.Errorf("failed to sign message: %w", err)
}
return buf.Bytes(), nil
}
// Verify verifies a detached signature for a message.
func (s *Service) Verify(publicKey, data, signature []byte) error {
pubKeyReader := bytes.NewReader(publicKey)
keyring, err := openpgp.ReadArmoredKeyRing(pubKeyReader)
if err != nil {
return fmt.Errorf("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("failed to verify signature: %w", err)
}
return nil
}
// SymmetricallyEncrypt encrypts data with a passphrase.
func (s *Service) SymmetricallyEncrypt(passphrase, data []byte) ([]byte, error) {
buf := new(bytes.Buffer)
w, err := openpgp.SymmetricallyEncrypt(buf, passphrase, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to create symmetric encryption writer: %w", err)
}
defer w.Close()
_, err = w.Write(data)
if err != nil {
return nil, fmt.Errorf("failed to write data to symmetric encryption writer: %w", err)
}
w.Close()
return buf.Bytes(), nil
}

View file

@ -0,0 +1,128 @@
package pgp
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestService_GenerateKeyPair_Good(t *testing.T) {
s := NewService()
pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
require.NoError(t, err, "failed to generate key pair")
assert.NotNil(t, pub, "public key is nil")
assert.NotNil(t, priv, "private key is nil")
}
func TestService_Encrypt_Good(t *testing.T) {
s := NewService()
pub, _, err := s.GenerateKeyPair("test", "test@test.com", "test")
require.NoError(t, err, "failed to generate key pair")
assert.NotNil(t, pub, "public key is nil")
data := []byte("hello world")
encrypted, err := s.Encrypt(pub, data)
require.NoError(t, err, "failed to encrypt data")
assert.NotNil(t, encrypted, "encrypted data is nil")
}
func TestService_Encrypt_Bad(t *testing.T) {
s := NewService()
_, err := s.Encrypt([]byte("bad key"), []byte("hello world"))
assert.Error(t, err)
}
func TestService_Decrypt_Good(t *testing.T) {
s := NewService()
pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
require.NoError(t, err, "failed to generate key pair")
assert.NotNil(t, pub, "public key is nil")
assert.NotNil(t, priv, "private key is nil")
data := []byte("hello world")
encrypted, err := s.Encrypt(pub, data)
require.NoError(t, err, "failed to encrypt data")
assert.NotNil(t, encrypted, "encrypted data is nil")
decrypted, err := s.Decrypt(priv, encrypted)
require.NoError(t, err, "failed to decrypt data")
assert.Equal(t, data, decrypted, "decrypted data does not match original")
}
func TestService_Decrypt_Bad(t *testing.T) {
s := NewService()
_, err := s.Decrypt([]byte("bad key"), []byte("hello world"))
assert.Error(t, err)
pub, _, err := s.GenerateKeyPair("test", "test@test.com", "test")
require.NoError(t, err)
_, priv2, err := s.GenerateKeyPair("test2", "test2@test.com", "test2")
require.NoError(t, err)
encrypted, err := s.Encrypt(pub, []byte("hello world"))
require.NoError(t, err)
_, err = s.Decrypt(priv2, encrypted)
assert.Error(t, err)
_, err = s.Decrypt(priv2, []byte("bad encrypted data"))
assert.Error(t, err)
}
func TestService_Sign_Good(t *testing.T) {
s := NewService()
_, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
require.NoError(t, err, "failed to generate key pair")
assert.NotNil(t, priv, "private key is nil")
data := []byte("hello world")
signature, err := s.Sign(priv, data)
require.NoError(t, err, "failed to sign data")
assert.NotNil(t, signature, "signature is nil")
}
func TestService_Sign_Bad(t *testing.T) {
s := NewService()
_, err := s.Sign([]byte("bad key"), []byte("hello world"))
assert.Error(t, err)
}
func TestService_Verify_Good(t *testing.T) {
s := NewService()
pub, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
require.NoError(t, err, "failed to generate key pair")
assert.NotNil(t, pub, "public key is nil")
assert.NotNil(t, priv, "private key is nil")
data := []byte("hello world")
signature, err := s.Sign(priv, data)
require.NoError(t, err, "failed to sign data")
assert.NotNil(t, signature, "signature is nil")
err = s.Verify(pub, data, signature)
require.NoError(t, err, "failed to verify signature")
}
func TestService_Verify_Bad(t *testing.T) {
s := NewService()
err := s.Verify([]byte("bad key"), []byte("hello world"), []byte("bad signature"))
assert.Error(t, err)
_, priv, err := s.GenerateKeyPair("test", "test@test.com", "test")
require.NoError(t, err)
pub2, _, err := s.GenerateKeyPair("test2", "test2@test.com", "test2")
require.NoError(t, err)
signature, err := s.Sign(priv, []byte("hello world"))
require.NoError(t, err)
err = s.Verify(pub2, []byte("hello world"), signature)
assert.Error(t, err)
}
func TestService_SymmetricallyEncrypt_Good(t *testing.T) {
s := NewService()
passphrase := []byte("hello world")
data := []byte("hello world")
encrypted, err := s.SymmetricallyEncrypt(passphrase, data)
require.NoError(t, err, "failed to encrypt data")
assert.NotNil(t, encrypted, "encrypted data is nil")
}