K9Crypt Go is a production-focused encryption module with a V5 AEAD payload format, authenticated metadata, cipher selection (AES-256-GCM / ChaCha20-Poly1305), PBKDF2 master key derivation with HMAC-SHA512 message key expansion, bounded batch processing, and safe defaults for low-latency application encryption.
- V5 AEAD format - single-pass authenticated encryption with a 19-byte header and 12-byte IV
- Cipher selection - AES-256-GCM (default) or ChaCha20-Poly1305 via
CipherIdoption - Master key + message keys - PBKDF2 master key derivation with a single HMAC-SHA512 expansion per message
- Backward compatible - transparently decrypts legacy V1-V4 payloads
- Authenticated metadata - magic, version, flags, cipher ID, time step, and issue time bound to ciphertext via AAD
- Freshness validation - optional max-age and clock-skew enforcement on decrypt
- Batch operations - sequential and parallel batch encrypt/decrypt with progress callbacks;
BatchSizeis clamped to2 - Compression support - optional zlib compression with configurable level
header(19) | salt(32) | iv(12) | ciphertext(N) | tag(16) | integritySalt(16) | dataHash(64)
header: magic(4) | version(1) | flags(1) | cipherId(1) | stepSeconds(4) | issuedAt(8)
The per-message key is derived from the master key, salt, and time metadata with a single HMAC-SHA512 pass over that input. This is not RFC 5869 HKDF (there is no extract/expand split); the exact byte layout is wire format and must not change. The master key is derived once via PBKDF2-HMAC-SHA512 from the secret key and a fixed salt, then cached for subsequent operations.
| ID | Cipher |
|---|---|
| 0x00 | AES-256-GCM (default) |
| 0x01 | ChaCha20-Poly1305 |
The decrypt path automatically detects the payload version and dispatches accordingly:
- V5 - AEAD decrypt with an HMAC-SHA512-expanded message key
- V1-V4 - legacy 5-cipher-chain decrypt with PBKDF2-derived key
Legacy payloads can be rejected by setting AllowLegacyPayloads: false in DecryptOptions. They are accepted by default for compatibility, but their key derivation (PBKDF2/Argon2) runs before anything is authenticated, so decrypting untrusted input is CPU- and memory-heavy; at most 4 such derivations run at once and the rest wait. Disable legacy payloads when the input is not trusted.
go get github.com/K9Crypt/k9crypt-gopackage main
import (
"fmt"
"log"
k9crypt "github.com/K9Crypt/k9crypt-go/src"
)
func main() {
encryptor, err := k9crypt.New("VeryLongSecretKey!@#1234567890")
if err != nil {
log.Fatal(err)
}
encrypted, err := encryptor.Encrypt("Hello, World!")
if err != nil {
log.Fatal(err)
}
decrypted, err := encryptor.Decrypt(encrypted)
if err != nil {
log.Fatal(err)
}
fmt.Println(decrypted)
}encryptor, err := k9crypt.NewGenerated()
if err != nil {
log.Fatal(err)
}
generatedKey := encryptor.GetGenerated()data := []byte{0x00, 0xff, 0x10, 0x80}
encrypted, err := encryptor.EncryptBytes(data, nil)
if err != nil {
log.Fatal(err)
}
decrypted, err := encryptor.DecryptBytes(encrypted, nil)
if err != nil {
log.Fatal(err)
}issuedAt := int64(1700000000)
encrypted, err := encryptor.EncryptWithOptions("short-lived", &k9crypt.EncryptOptions{
IssuedAtUnix: &issuedAt,
TimeStepSeconds: 300,
})
if err != nil {
log.Fatal(err)
}
maxAge := int64(300)
now := int64(1700000100)
decrypted, err := encryptor.DecryptWithOptions(encrypted, &k9crypt.DecryptOptions{
MaxAgeSeconds: &maxAge,
AllowedClockSkewSeconds: 30,
NowUnixSeconds: &now,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(decrypted)level := 6
encrypted, err := encryptor.EncryptWithOptions("compressible payload", &k9crypt.EncryptOptions{
CompressionLevel: &level,
})Levels are 0-9; 0 (the default) stores the payload raw, any higher level compresses it with zlib before encryption. The deflate stream is embedded in the payload and the level is not recorded, so any level can decrypt any other.
chacha := byte(k9crypt.CipherChaCha20Poly1305)
encrypted, err := encryptor.EncryptWithOptions("ChaCha20 encrypted", &k9crypt.EncryptOptions{
CipherId: &chacha,
})
if err != nil {
log.Fatal(err)
}
// decrypt automatically detects the cipher from the payload header
decrypted, err := encryptor.Decrypt(encrypted)For file and batch operations, pass CipherId through EncryptFileOptions or EncryptManyOptions:
chacha := byte(k9crypt.CipherChaCha20Poly1305)
encrypted, err := encryptor.EncryptFile(data, &k9crypt.EncryptFileOptions{
CipherId: &chacha,
})data := make([]byte, 1024*1024)
encrypted, err := encryptor.EncryptFile(data, &k9crypt.EncryptFileOptions{
CompressionLevel: 0,
CipherId: nil, // nil = AES-256-GCM (default)
OnProgress: func(p k9crypt.ProgressInfo) {
fmt.Printf("%.0f%%\n", p.Percentage)
},
})
if err != nil {
log.Fatal(err)
}
decrypted, err := encryptor.DecryptFile(encrypted, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(len(decrypted))items := []string{"user1", "user2", "user3"}
encrypted, err := encryptor.EncryptMany(items, &k9crypt.EncryptManyOptions{
CipherId: nil, // nil = AES-256-GCM (default)
Parallel: true,
BatchSize: 2,
})
if err != nil {
log.Fatal(err)
}
decrypted, err := encryptor.DecryptMany(encrypted, &k9crypt.DecryptManyOptions{
Parallel: true,
BatchSize: 2,
SkipInvalid: false,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(decrypted)allowLegacy := false
plaintext, err := encryptor.DecryptWithOptions(ciphertext, &k9crypt.DecryptOptions{
AllowLegacyPayloads: &allowLegacy,
})| Function | Description |
|---|---|
New(secretKey string) (*K9Crypt, error) |
Creates an encryptor from a non-empty string secret. |
NewGenerated() (*K9Crypt, error) |
Creates an encryptor with a 50-byte random secret. |
NewWithKey(secretKey []byte) (*K9Crypt, error) |
Creates an encryptor from a cloned byte-slice secret; nil generates a key, empty slices are rejected. |
NewWithOptions(secretKey string, compressionLevel int) (*K9Crypt, error) |
Creates a string-key encryptor with a default compression level. |
NewWithKeyAndOptions(secretKey []byte, compressionLevel int) (*K9Crypt, error) |
Creates a byte-key encryptor with a default compression level. |
NewGeneratedWithOptions(compressionLevel int) (*K9Crypt, error) |
Creates a generated-key encryptor with a default compression level. |
| Method | Description |
|---|---|
Encrypt(plaintext string) |
Encrypts a string with default options. |
EncryptWithOptions(plaintext string, options *EncryptOptions) |
Encrypts a string with time/compression/cipher options. |
EncryptBytes(plaintext []byte, options *EncryptOptions) |
Encrypts binary data and marks the payload as binary. |
Decrypt(ciphertext string) |
Decrypts a string payload. |
DecryptWithOptions(ciphertext string, options *DecryptOptions) |
Decrypts with freshness and compatibility policy checks. |
DecryptBytes(ciphertext string, options *DecryptOptions) |
Decrypts and returns raw bytes. |
EncryptFile(data []byte, options *EncryptFileOptions) |
Encrypts buffered file data up to 16 MB. |
DecryptFile(ciphertext string, options *DecryptFileOptions) |
Decrypts buffered file data up to 16 MB. |
EncryptMany(dataArray []string, options *EncryptManyOptions) |
Batch encrypts strings. |
DecryptMany(ciphertextArray []string, options *DecryptManyOptions) |
Batch decrypts strings. |
SetCompressionLevel(level int) |
Sets default compression level 0-9. |
GetCompressionLevel() |
Returns current default compression level. |
GetGenerated() |
Returns a copy of the generated secret, or nil for caller-provided secrets. |
| Constant | Value | Description |
|---|---|---|
CipherAes256Gcm |
0x00 | AES-256-GCM cipher ID |
CipherChaCha20Poly1305 |
0x01 | ChaCha20-Poly1305 cipher ID |
PayloadVersionV5 |
5 | Current payload version |
PayloadHeaderSizeV5 |
19 | V5 header size in bytes |
IvSizeV5 |
12 | V5 IV/nonce size in bytes |
EncryptOptions
CompressionLevel *int- optional per-call compression level;0means raw mode.TimeStepSeconds uint32- optional time bucket size, default300, maximum86400.IssuedAtUnix *int64- optional authenticated issue time.CipherId *byte- optional cipher selection;nildefaults to AES-256-GCM (0x00), useCipherChaCha20Poly1305(0x01) for ChaCha20-Poly1305.
DecryptOptions
MaxAgeSeconds *int64- optional freshness limit.AllowedClockSkewSeconds int64- optional clock skew allowance.NowUnixSeconds *int64- optional deterministic validation time.AllowLegacyPayloads *bool- defaulttrue; set tofalseto reject pre-v5 payloads.
EncryptFileOptions / EncryptManyOptions also expose CipherId, compression, time metadata, progress, and parallel batch controls where applicable.
DecryptFileOptions / DecryptManyOptions also expose freshness, compatibility, progress, SkipInvalid, and parallel batch controls where applicable.
- Master key is derived once via PBKDF2-HMAC-SHA512 (600,000 iterations) from the secret key and a fixed salt, then cached
- Per-message: random 32-byte salt and 12-byte IV are generated
- Per-message key is derived with a single HMAC-SHA512 pass over the master key, salt, and time bucket metadata (not RFC 5869 HKDF)
- Plaintext is encrypted with the selected AEAD cipher (AES-256-GCM or ChaCha20-Poly1305) using the header as AAD
- Body is authenticated via HMAC-SHA512 with the per-message key
- Final payload is base64-encoded
- Payload version is detected from the header
- V5 payloads: freshness validation, master key derivation, message key expansion, MAC verification, AEAD decrypt
- Legacy V1-V4 payloads: policy check, hash/MAC verification, key derivation, 5-cipher-chain decrypt
All intermediate key material (per-message keys, derived keys) is zeroed after use via zeroBytes() to minimize sensitive data lifetime in memory.
This project is licensed under the MIT License.
