acme-misc/crypt/EXTENSIONS.md

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_RCPT for 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

  1. Build the tools: mk install
  2. Set environment variables for your backend
  3. Test decryption: Create an encrypted file, highlight it in Acme, run CryptGet
  4. Test encryption: Edit decrypted content, run CryptPut
  5. 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:

  1. Follow the existing code style and patterns
  2. Add appropriate error handling and logging
  3. Update documentation and examples
  4. Test thoroughly with Acme integration
  5. Consider security implications of your implementation