Lots of progress
This commit is contained in:
128
crypto.go
128
crypto.go
@@ -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 it’s 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 it’s not an *os.File, it’s 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user