Lots of progress

This commit is contained in:
2026-02-19 22:49:13 +01:00
parent 9ea5b8668f
commit d09c81da5c
15 changed files with 328 additions and 163 deletions

128
crypto.go
View File

@@ -1,17 +1,15 @@
package main
import (
"bufio"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"strings"
_ "filippo.io/age"
"filippo.io/age/armor"
"golang.org/x/crypto/chacha20poly1305"
)
//var cert *x509.Certificate
@@ -75,86 +73,80 @@ func GenerateKey() (string, error) {
return string(out), nil
}
// LoadKeyFromFile reads a key file that contains either a raw base64 string or
// "AGE_SYM_KEY=<base64>" (handy for dotenv). Whitespace is trimmed.
func LoadKeyFromFile(path string) (string, error) {
b, err := os.ReadFile(path)
func decodeKey(b64 string) ([]byte, error) {
key, err := base64.StdEncoding.DecodeString(b64) // standard padded
if err != nil {
return "", err
return nil, err
}
s := strings.TrimSpace(string(b))
if i := strings.Index(s, "AGE_SYM_KEY="); i >= 0 {
s = strings.TrimSpace(strings.TrimPrefix(s, "AGE_SYM_KEY="))
if len(key) != chacha20poly1305.KeySize {
return nil, fmt.Errorf("bad key length: got %d, want %d", len(key), chacha20poly1305.KeySize)
}
if s == "" {
return "", errors.New("empty symmetric key")
}
// Quick sanity check that its base64 and ~32 bytes after decode.
if _, err := base64.StdEncoding.DecodeString(s); err != nil {
return "", fmt.Errorf("invalid base64 key: %w", err)
}
return s, nil
return key, nil
}
// Encrypt streams plaintext from r to w using a symmetric passphrase.
// If armorOut is true, output is ASCII-armored (BEGIN AGE ENCRYPTED FILE).
func Encrypt(r io.Reader, w io.Writer, passphrase string, armorOut bool) error {
passphrase = strings.TrimSpace(passphrase)
if passphrase == "" {
return errors.New("missing passphrase")
}
var out io.WriteCloser
var err error
if armorOut {
aw := armor.NewWriter(w)
defer aw.Close()
//out, err = age.Encrypt(aw, age.NewScryptRecipient(passphrase))
} else {
//out, err = age.Encrypt(w, age.NewScryptRecipient(passphrase))
}
func EncryptFileXChaCha(keyB64, inPath, outPath string, aad []byte) error {
key, err := decodeKey(keyB64)
if err != nil {
return err
}
_, copyErr := io.Copy(out, bufio.NewReader(r))
closeErr := out.Close()
if copyErr != nil {
return copyErr
}
return closeErr
}
// Decrypt streams ciphertext from r to w using the same symmetric passphrase.
// It auto-detects armored vs binary ciphertext.
func Decrypt(r io.Reader, w io.Writer, passphrase string) error {
passphrase = strings.TrimSpace(passphrase)
if passphrase == "" {
return errors.New("missing passphrase")
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return fmt.Errorf("new aead: %w", err)
}
br := bufio.NewReader(r)
peek, _ := br.Peek(32)
//var in io.Reader = br
if strings.HasPrefix(string(peek), "-----BEGIN AGE ENCRYPTED FILE-----") {
// in = armor.NewReader(br)
plaintext, err := os.ReadFile(inPath)
if err != nil {
return fmt.Errorf("read input: %w", err)
}
//dr, err := age.Decrypt(in, age.NewScryptIdentity(passphrase))
//if err != nil {
// return err
//}
//_, err = io.Copy(w, bufio.NewWriter(wrap0600(w)))
//return err
nonce := make([]byte, chacha20poly1305.NonceSizeX) // 24 bytes
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return fmt.Errorf("nonce: %w", err)
}
ciphertext := aead.Seal(nil, nonce, plaintext, aad)
// Write: nonce || ciphertext
out := make([]byte, 0, len(nonce)+len(ciphertext))
out = append(out, nonce...)
out = append(out, ciphertext...)
if err := os.WriteFile(outPath, out, 0600); err != nil {
return fmt.Errorf("write output: %w", err)
}
return nil
}
// wrap0600 ensures that when w is an *os.File newly created by caller,
// its perms are 0600. If its not an *os.File, its returned unchanged.
func wrap0600(w io.Writer) io.Writer {
if f, ok := w.(*os.File); ok {
_ = f.Chmod(0600)
func DecryptFileXChaCha(keyB64, inPath, outPath string, aad []byte) error {
key, err := decodeKey(keyB64)
if err != nil {
return err
}
return w
aead, err := chacha20poly1305.NewX(key)
if err != nil {
return fmt.Errorf("new aead: %w", err)
}
in, err := os.ReadFile(inPath)
if err != nil {
return fmt.Errorf("read input: %w", err)
}
if len(in) < chacha20poly1305.NonceSizeX {
return errors.New("ciphertext too short")
}
nonce := in[:chacha20poly1305.NonceSizeX]
ciphertext := in[chacha20poly1305.NonceSizeX:]
plaintext, err := aead.Open(nil, nonce, ciphertext, aad)
if err != nil {
return fmt.Errorf("decrypt/auth failed: %w", err)
}
if err := os.WriteFile(outPath, plaintext, 0640); err != nil {
return fmt.Errorf("write output: %w", err)
}
return nil
}