attachments.go raw

   1  package bridge
   2  
   3  import (
   4  	"crypto/rand"
   5  	"encoding/hex"
   6  	"fmt"
   7  
   8  	"golang.org/x/crypto/chacha20poly1305"
   9  )
  10  
  11  // EncryptAttachment encrypts data with a random ChaCha20-Poly1305 key.
  12  // Returns (ciphertext, keyHex). The key is hex-encoded for use in fragment URLs.
  13  func EncryptAttachment(plaintext []byte) (ciphertext []byte, keyHex string, err error) {
  14  	// Generate random 256-bit key
  15  	key := make([]byte, chacha20poly1305.KeySize)
  16  	if _, err := rand.Read(key); err != nil {
  17  		return nil, "", fmt.Errorf("generate key: %w", err)
  18  	}
  19  
  20  	aead, err := chacha20poly1305.New(key)
  21  	if err != nil {
  22  		return nil, "", fmt.Errorf("create AEAD: %w", err)
  23  	}
  24  
  25  	// Generate random nonce
  26  	nonce := make([]byte, aead.NonceSize())
  27  	if _, err := rand.Read(nonce); err != nil {
  28  		return nil, "", fmt.Errorf("generate nonce: %w", err)
  29  	}
  30  
  31  	// Encrypt: nonce || ciphertext (nonce prepended for decryption)
  32  	encrypted := aead.Seal(nonce, nonce, plaintext, nil)
  33  
  34  	return encrypted, hex.EncodeToString(key), nil
  35  }
  36  
  37  // DecryptAttachment decrypts data that was encrypted by EncryptAttachment.
  38  // keyHex is the hex-encoded key from the fragment URL.
  39  func DecryptAttachment(ciphertext []byte, keyHex string) ([]byte, error) {
  40  	key, err := hex.DecodeString(keyHex)
  41  	if err != nil {
  42  		return nil, fmt.Errorf("decode key: %w", err)
  43  	}
  44  
  45  	if len(key) != chacha20poly1305.KeySize {
  46  		return nil, fmt.Errorf("invalid key length: %d (expected %d)", len(key), chacha20poly1305.KeySize)
  47  	}
  48  
  49  	aead, err := chacha20poly1305.New(key)
  50  	if err != nil {
  51  		return nil, fmt.Errorf("create AEAD: %w", err)
  52  	}
  53  
  54  	if len(ciphertext) < aead.NonceSize() {
  55  		return nil, fmt.Errorf("ciphertext too short")
  56  	}
  57  
  58  	// Split nonce and ciphertext
  59  	nonce := ciphertext[:aead.NonceSize()]
  60  	ct := ciphertext[aead.NonceSize():]
  61  
  62  	plaintext, err := aead.Open(nil, nonce, ct, nil)
  63  	if err != nil {
  64  		return nil, fmt.Errorf("decrypt: %w", err)
  65  	}
  66  
  67  	return plaintext, nil
  68  }
  69