jfcrypt

package module
v0.0.0-...-e62ffec Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 1, 2026 License: BSD-2-Clause Imports: 22 Imported by: 0

README

jfcrypt

jfcrypt – A Go library and command-line tool for streaming authenticated encryption of arbitrary files.

jfcrypt protects data using

  • Argon2id for key derivation (passphrase to 32-byte key), or a raw 32-byte key directly.
  • Hybrid post-quantum key encapsulation (X25519 + ML-KEM-768) for public-key encryption.
  • Tink Streaming AEAD (AES-256-GCM-HKDF) to encrypt data in 1 MiB segments.

The library exposes a clean API (Encrypt, Decrypt, NewEncryptingWriter, NewDecryptingReader) for building applications that need streaming encryption — encrypted backups, secure file transfer, etc.

The CLI operates on files directly (jfcrypt -e backup.tar produces backup.tar.jfc) or reads from stdin and writes to stdout for use in pipelines.

The first line of the output is a JSON header that records the algorithm, the format (binary or base64), and the KDF parameters used to derive the key.

Features

  • Library and CLI – use as a Go package or a standalone binary.
  • Streaming – encrypt or decrypt files of any size with a constant memory footprint (1 MiB buffers).
  • Secure defaults – 1 GiB Argon2 memory cost, 3 iterations, 4 parallel threads, 256-bit key.
  • Hybrid post-quantum – X25519 + ML-KEM-768 (FIPS 203) key encapsulation for public-key encryption without passphrases.
  • Flexible key sources – passphrase (Argon2id), raw 32-byte key, keyfile, or hybrid public/private keypair.
  • Base64 support – produce text-safe output that can be embedded in JSON, sent over email, or stored in plain-text configuration files.
  • Self-contained header – no external metadata required; decryption parameters are stored in the file.

Installation

Library
go get github.com/jsf0/jfcrypt
CLI
git clone https://github.com/jsf0/jfcrypt.git
cd jfcrypt

make
sudo make install
Dependencies
  • Go 1.24 or newer
  • Google Tink for streaming AEAD (pulled in by the Go module system).

Library API

Import the package:

import "github.com/jsf0/jfcrypt"
Key sources

jfcrypt supports three ways to provide key material, all returning a KeySource:

// Derive a key from a passphrase using Argon2id
ks := jfcrypt.KeyFromPassphrase([]byte("my passphrase"))

// Use a raw 32-byte key directly (no KDF)
var key [32]byte
// ... fill key ...
ks := jfcrypt.KeyFromRawBytes(key)

// Read a 32-byte key from a file (raw binary or base64-encoded)
ks, err := jfcrypt.KeyFromKeyfile("/path/to/secret.key")

Call ks.Zero() when done to scrub key material from memory.

Hybrid post-quantum keypair

For public-key encryption using X25519 + ML-KEM-768:

// Generate a keypair
pub, priv, err := jfcrypt.GenerateKeyPair()

// Encrypt with the public key (no passphrase needed)
ks, err := jfcrypt.KeyFromPublicKey(pub)
err = jfcrypt.Encrypt(dst, src, ks, nil)

// Decrypt with the private key
ks, err := jfcrypt.KeyFromPrivateKey(priv)
err = jfcrypt.Decrypt(dst, src, ks)

Public keys are 1216 bytes (X25519 + ML-KEM-768 encapsulation key). Private keys are 96 bytes (X25519 + ML-KEM-768 seed). Both are raw binary.

Encrypt and decrypt

The simplest API reads from an io.Reader and writes to an io.Writer:

// Encrypt
err := jfcrypt.Encrypt(dst, src, keySource, &jfcrypt.Options{
    Format:       jfcrypt.FormatBinary,
    Argon2Memory: 1024 * 1024, // 1 GiB in KB
    Argon2Time:   3,
})

// Decrypt (parameters are read from the header automatically)
err := jfcrypt.Decrypt(dst, src, keySource)

Pass nil for options to use secure defaults.

Streaming API

For more control, use the streaming writer and reader directly:

// Encrypting writer — writes header immediately, caller writes plaintext
w, err := jfcrypt.NewEncryptingWriter(dst, keySource, opts)
// ... write plaintext to w ...
w.Close() // finalizes encryption and zeros key material

// Decrypting reader — parses header, returns a reader that yields plaintext
result, err := jfcrypt.NewDecryptingReader(src, keySource)
// result.Header contains the parsed file header
// ... read plaintext from result ...
result.Close() // zeros key material

CLI Usage

jfcrypt <command> [options] [file]

Command Description
-e / --encrypt Encrypt a file or stdin.
-d / --decrypt Decrypt a file or stdin.
--info Show encryption metadata from a file header.
--keygen [name] Generate a hybrid X25519 + ML-KEM-768 keypair.
-h / --help Show help message.
-v / --version Show version.
Options
Flag Effect Default
-b / --base64 Base64-encode the encrypted payload (text-safe, ~33% larger) binary
-m=SIZE / --memory=SIZE Argon2 memory cost (e.g., 64M, 256M, 1G) 1G
-i=N / --iterations=N Argon2 iteration count 3
-k=PATH / --keyfile=PATH Use a 32-byte keyfile instead of a passphrase
--pubkey=PATH Encrypt with a hybrid public key (from --keygen)
--privkey=PATH Decrypt with a hybrid private key (from --keygen)
-o=PATH / --output=PATH Explicit output file (overrides auto-naming)
--no-passphrase Skip passphrase encryption of private key (--keygen only)

When a file argument is given, output is auto-named: encrypt adds .jfc, decrypt strips it. Without a file, reads stdin and writes stdout for piping. The passphrase is supplied via the JFCRYPT_PASSPHRASE environment variable or entered interactively.

Examples
# Encrypt a file (output: backup.tar.jfc)
jfcrypt -e backup.tar

# Decrypt a file (output: backup.tar)
jfcrypt -d backup.tar.jfc

# Encrypt with explicit output path
jfcrypt -e -o=encrypted.bin backup.tar

# Pipe mode (stdin/stdout)
cat backup.tar | jfcrypt -e > backup.tar.jfc

# Encrypt with higher parameters and base64 output
jfcrypt -e -m=2G -i=4 -b backup.tar

# Generate a hybrid post-quantum keypair
jfcrypt --keygen

# Generate a named keypair (produces backup.pub and backup.key)
jfcrypt --keygen backup

# Generate keypair with unencrypted private key (for automation)
jfcrypt --keygen --no-passphrase

# Encrypt with public key (no passphrase needed — ideal for unattended backups)
jfcrypt -e --pubkey=jfcrypt.pub backup.tar

# Decrypt with private key
jfcrypt -d --privkey=jfcrypt.key backup.tar.jfc

# Inspect a file's encryption metadata
jfcrypt --info backup.tar.jfc

# Pipeline: encrypt and upload to S3
tar cf - . | jfcrypt -e --pubkey=jfcrypt.pub | aws s3 cp - s3://bucket/backup.tar.jfc

Header Format

The first line of a sealed file is a single-line JSON object:

{
  "version": "1.0.0",
  "algorithm": "AES256-GCM-HKDF-1MB",
  "format": "binary",
  "kdf": {
    "algorithm": "argon2id",
    "salt": "<base64-encoded 16-byte salt>",
    "time": 3,
    "memory": 1048576,
    "threads": 4,
    "keylen": 32
  }
}

When using hybrid public-key encryption, the KDF section contains the encapsulated key material:

{
  "version": "1.0.0",
  "algorithm": "AES256-GCM-HKDF-1MB",
  "format": "binary",
  "kdf": {
    "algorithm": "x25519-mlkem768",
    "ephemeral_key": "<base64-encoded 32-byte ephemeral X25519 public key>",
    "kem_ciphertext": "<base64-encoded 1088-byte ML-KEM-768 ciphertext>"
  }
}

When using a raw key or keyfile, the KDF section is minimal:

{
  "version": "1.0.0",
  "algorithm": "AES256-GCM-HKDF-1MB",
  "format": "binary",
  "kdf": {
    "algorithm": "none"
  }
}
Field Description
version The jfcrypt version that created the file.
algorithm Always AES256-GCM-HKDF-1MB.
format binary (raw bytes) or base64 (text-safe).
kdf.algorithm argon2id (passphrase), none (raw key / keyfile), or x25519-mlkem768 (hybrid).
kdf.salt Base64-encoded 16-byte random salt (passphrase only).
kdf.time Argon2 iteration count.
kdf.memory Argon2 memory cost in kilobytes.
kdf.threads Argon2 parallelism (default 4).
kdf.keylen Derived key length in bytes (32).
kdf.ephemeral_key Base64-encoded ephemeral X25519 public key (hybrid only).
kdf.kem_ciphertext Base64-encoded ML-KEM-768 ciphertext (hybrid only).

Documentation

Index

Constants

View Source
const (
	Version = "1.0.0"

	DefaultArgon2Memory  uint32 = 1024 * 1024 // 1 GiB in KB
	DefaultArgon2Time    uint32 = 3
	DefaultArgon2Threads uint8  = 4
	DefaultKeyLen        uint32 = 32
)
View Source
const (

	// HybridPublicKeyLen is the length of a hybrid X25519 + ML-KEM-768 public key.
	HybridPublicKeyLen = x25519PubKeyLen + mlkemEncapKeyLen // 1216

	// HybridPrivateKeyLen is the length of a hybrid X25519 + ML-KEM-768 private key.
	HybridPrivateKeyLen = x25519PrivKeyLen + mlkemSeedLen // 96

)

Variables

View Source
var (
	ErrInvalidHeader     = errors.New("jfcrypt: invalid header")
	ErrUnsupportedAlgo   = errors.New("jfcrypt: unsupported algorithm")
	ErrUnsupportedKDF    = errors.New("jfcrypt: unsupported KDF")
	ErrUnsupportedFmt    = errors.New("jfcrypt: unsupported format")
	ErrInvalidKey        = errors.New("jfcrypt: invalid key")
	ErrKeyLength         = errors.New("jfcrypt: key must be exactly 32 bytes")
	ErrEmptyPassphrase   = errors.New("jfcrypt: passphrase cannot be empty")
	ErrDecryptFailed     = errors.New("jfcrypt: decryption failed")
	ErrInvalidSalt       = errors.New("jfcrypt: invalid salt encoding")
	ErrInvalidPublicKey  = errors.New("jfcrypt: invalid public key")
	ErrInvalidPrivateKey = errors.New("jfcrypt: invalid private key")
	ErrKEMFailed         = errors.New("jfcrypt: key encapsulation failed")
)

Functions

func Decrypt

func Decrypt(dst io.Writer, src io.Reader, key KeySource) error

Decrypt reads a jfcrypt stream from src and writes plaintext to dst. This is a convenience wrapper around NewDecryptingReader.

func Encrypt

func Encrypt(dst io.Writer, src io.Reader, key KeySource, opts *Options) error

Encrypt reads plaintext from src, encrypts it, and writes the jfcrypt header + ciphertext to dst. This is a convenience wrapper around NewEncryptingWriter.

func GenerateKeyPair

func GenerateKeyPair() (pub, priv []byte, err error)

GenerateKeyPair generates a hybrid X25519 + ML-KEM-768 keypair. Public key: X25519 pub (32) || ML-KEM encapsulation key (1184) = 1216 bytes. Private key: X25519 priv (32) || ML-KEM seed (64) = 96 bytes.

func NewEncryptingWriter

func NewEncryptingWriter(dst io.Writer, key KeySource, opts *Options) (io.WriteCloser, error)

NewEncryptingWriter returns a writer that encrypts data written to it. The jfcrypt header is written to dst immediately. The caller writes plaintext to the returned writer, then calls Close() to finalize. Close zeros key material.

func WriteHeader

func WriteHeader(w io.Writer, h Header) error

WriteHeader serializes the header as a single JSON line followed by newline.

func ZeroBytes

func ZeroBytes(b []byte)

ZeroBytes overwrites a byte slice with zeros.

Types

type DecryptResult

type DecryptResult struct {
	io.Reader
	Header Header
	// contains filtered or unexported fields
}

DecryptResult holds the plaintext reader and parsed header. Call Close() when done to zero key material.

func NewDecryptingReader

func NewDecryptingReader(src io.Reader, key KeySource) (*DecryptResult, error)

NewDecryptingReader reads the jfcrypt header from src, derives the key, and returns a DecryptResult whose Reader yields plaintext. The header is consumed immediately and available via DecryptResult.Header.

func (*DecryptResult) Close

func (d *DecryptResult) Close() error

Close zeros key material. Always call this when done reading.

type Format

type Format string

Format specifies the output encoding.

const (
	FormatBinary Format = "binary"
	FormatBase64 Format = "base64"
)
type Header struct {
	Version   string    `json:"version"`
	Algorithm string    `json:"algorithm"`
	Format    Format    `json:"format"`
	KDF       KDFParams `json:"kdf"`
}

Header represents the JSON metadata line at the start of a jfcrypt file.

func ParseHeader

func ParseHeader(r *bufio.Reader) (Header, error)

ParseHeader reads and validates a jfcrypt header from the stream. It consumes exactly the first line (through the newline). Headers larger than 64 KB are rejected.

func (Header) Validate

func (h Header) Validate() error

Validate checks the header for internal consistency.

type KDFParams

type KDFParams struct {
	Algorithm     string `json:"algorithm"`
	Salt          string `json:"salt,omitempty"`
	Time          uint32 `json:"time,omitempty"`
	Memory        uint32 `json:"memory,omitempty"`
	Threads       uint8  `json:"threads,omitempty"`
	KeyLen        uint32 `json:"keylen,omitempty"`
	EphemeralKey  string `json:"ephemeral_key,omitempty"`
	KEMCiphertext string `json:"kem_ciphertext,omitempty"`
}

KDFParams contains key derivation parameters stored in the header.

type KeySource

type KeySource interface {

	// Zero scrubs key material held by this KeySource.
	Zero()
	// contains filtered or unexported methods
}

KeySource provides the encryption key material. Implementations are constructed via KeyFromPassphrase, KeyFromRawBytes, or KeyFromKeyfile. Call Zero() when done to scrub sensitive material from memory.

func KeyFromKeyfile

func KeyFromKeyfile(path string) (KeySource, error)

KeyFromKeyfile returns a KeySource that reads a 32-byte key from a file. The file may contain 32 raw bytes or base64-encoded 32 bytes.

func KeyFromPassphrase

func KeyFromPassphrase(passphrase []byte) KeySource

KeyFromPassphrase returns a KeySource that derives a key using Argon2id. The passphrase is copied internally.

func KeyFromPrivateKey

func KeyFromPrivateKey(priv []byte) (KeySource, error)

KeyFromPrivateKey returns a KeySource for decryption using a hybrid X25519 + ML-KEM-768 private key (96 bytes).

func KeyFromPublicKey

func KeyFromPublicKey(pub []byte) (KeySource, error)

KeyFromPublicKey returns a KeySource for encryption using a hybrid X25519 + ML-KEM-768 public key (1216 bytes).

func KeyFromRawBytes

func KeyFromRawBytes(key [32]byte) KeySource

KeyFromRawBytes returns a KeySource that uses the given 32-byte key directly. The value type forces compile-time length verification.

type Options

type Options struct {
	Format        Format
	Argon2Memory  uint32 // in KB
	Argon2Time    uint32 // iterations
	Argon2Threads uint8
}

Options controls encryption behavior. nil means use all defaults.

func DefaultOptions

func DefaultOptions() *Options

DefaultOptions returns Options with secure defaults.

Directories

Path Synopsis
cmd
jfcrypt command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL