4.9 KiB
4.9 KiB
Extensions Guide
This document describes how to implement new encryption backends for acme-crypt.
Architecture Overview
The acme-crypt tools use a pluggable architecture based on Go interfaces. New encryption backends can be added by implementing the Crypter interface defined in internal/crypt/crypt.go.
Interface Definition
type Crypter interface {
Decrypt(encryptedData []byte) ([]byte, error)
Encrypt(plainData []byte) ([]byte, error)
}
Implementing a New Backend
1. Create the Backend
Create a new type that implements the Crypter interface:
type MyEncryptionBackend struct {
// Configuration fields
keyPath string
config *MyConfig
}
func NewMyEncryptionBackend() *MyEncryptionBackend {
return &MyEncryptionBackend{
// Initialize from environment variables or config
}
}
func (m *MyEncryptionBackend) Decrypt(encryptedData []byte) ([]byte, error) {
// Implement decryption logic
return decryptedData, nil
}
func (m *MyEncryptionBackend) Encrypt(plainData []byte) ([]byte, error) {
// Implement encryption logic
return encryptedData, nil
}
2. Add Backend Selection
Modify the main functions in cmd/CryptGet/main.go and cmd/CryptPut/main.go to support backend selection:
func createCrypter() crypt.Crypter {
backend := os.Getenv("ACME_CRYPT_BACKEND")
switch backend {
case "gpg", "":
return crypt.NewGPGCrypter()
case "age":
return crypt.NewAgeCrypter()
case "openssl":
return crypt.NewOpenSSLCrypter()
default:
fmt.Fprintf(os.Stderr, "Unknown backend: %s\n", backend)
os.Exit(1)
}
}
3. Handle File Extensions
Update the extension handling in GetOutputFilename() to recognize your backend's file extensions:
func GetOutputFilename(filename string) string {
extensions := []string{".gpg", ".asc", ".pgp", ".age", ".enc"}
for _, ext := range extensions {
if strings.HasSuffix(filename, ext) {
return strings.TrimSuffix(filename, ext)
}
}
return filename
}
Configuration
Environment Variables
Backends should use environment variables for configuration:
ACME_CRYPT_BACKEND- Backend selection (e.g., "gpg", "age", "openssl")- Backend-specific variables (e.g.,
ACME_CRYPT_RCPTfor GPG recipient)
Example Backend Configurations
GPG Backend (Current)
export ACME_CRYPT_BACKEND="gpg"
export ACME_CRYPT_RCPT="user@example.com"
Hypothetical Age Backend
export ACME_CRYPT_BACKEND="age"
export ACME_CRYPT_AGE_RECIPIENTS_FILE="$HOME/.config/age/recipients"
Hypothetical OpenSSL Backend
export ACME_CRYPT_BACKEND="openssl"
export ACME_CRYPT_CERT_FILE="$HOME/.config/crypt/cert.pem"
export ACME_CRYPT_KEY_FILE="$HOME/.config/crypt/key.pem"
Error Handling
Backends should:
- Return descriptive errors for common failure cases
- Preserve environment variables needed for GUI operations (like
DISPLAY) - Handle missing configuration gracefully
- Use stderr for error output (goes to Acme's +Errors buffer)
Testing Your Backend
- Build the tools:
mk install - Set environment variables for your backend
- Test decryption: Create an encrypted file, highlight it in Acme, run
CryptGet - Test encryption: Edit decrypted content, run
CryptPut - Verify roundtrip: Ensure the encrypted file can be decrypted again
Example: Age Backend Implementation
package crypt
import (
"bytes"
"os"
"os/exec"
)
type AgeCrypter struct {
recipientsFile string
}
func NewAgeCrypter() *AgeCrypter {
recipientsFile := os.Getenv("ACME_CRYPT_AGE_RECIPIENTS_FILE")
if recipientsFile == "" {
recipientsFile = os.Getenv("HOME") + "/.config/age/recipients"
}
return &AgeCrypter{recipientsFile: recipientsFile}
}
func (a *AgeCrypter) Decrypt(encryptedData []byte) ([]byte, error) {
cmd := exec.Command("age", "--decrypt")
cmd.Stdin = bytes.NewReader(encryptedData)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("age decrypt failed: %v\nstderr: %s", err, stderr.String())
}
return stdout.Bytes(), nil
}
func (a *AgeCrypter) Encrypt(plainData []byte) ([]byte, error) {
cmd := exec.Command("age", "--recipients-file", a.recipientsFile)
cmd.Stdin = bytes.NewReader(plainData)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("age encrypt failed: %v\nstderr: %s", err, stderr.String())
}
return stdout.Bytes(), nil
}
Contributing
When adding new backends:
- Follow the existing code style and patterns
- Add appropriate error handling and logging
- Update documentation and examples
- Test thoroughly with Acme integration
- Consider security implications of your implementation