e5t

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: May 17, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

e5t

main branch GitHub go.mod Go version Go Reference Go Report Card license Release

e5t is a small Go package for AES-256-GCM encryption and decryption. It is designed for applications that need a compact, easy-to-review encryption helper with no external dependencies: only the Go standard library is used.

The package encrypts byte slices, generates a fresh random nonce for every encryption operation, prefixes that nonce to the ciphertext, and can return either raw encrypted bytes or a hex-encoded string for storage and transport.

Features

  • AES-256-GCM authenticated encryption using crypto/aes and crypto/cipher
  • Fresh random nonce generation with crypto/rand
  • Raw byte and hex-encoded ciphertext APIs
  • Deterministic 32-byte key helper based on SHA-256
  • Sentinel errors for invalid key size and short ciphertext handling
  • Zero third-party dependencies
  • Apache-2.0 licensed

Installation

go get github.com/slashdevops/e5t

Update to the latest available version:

go get -u github.com/slashdevops/e5t

Requirements

  • Go 1.26.3 or newer
  • No external Go modules

Quick Start

package main

import (
    "fmt"
    "log"

    "github.com/slashdevops/e5t"
)

func main() {
    key := e5t.GenerateHashKey("my-secret-password", "unique-salt")

    plaintext := []byte("sensitive data")
    encrypted, err := e5t.EncryptAsString(plaintext, key)
    if err != nil {
        log.Fatal(err)
    }

    decrypted, err := e5t.DecryptFromText(encrypted, key)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(decrypted))
}

API

GenerateHashKey
func GenerateHashKey(secret string, salt ...string) []byte

GenerateHashKey returns a 32-byte key by hashing secret + salt with SHA-256. The optional salt is useful when deriving separate keys for different environments, tenants, users, or data classes.

key := e5t.GenerateHashKey("application-secret", "production-config-v1")

This helper is intentionally simple and dependency-free. For user-entered passwords or high-risk secrets, prefer passing Encrypt and Decrypt a 32-byte key produced by your platform's key management system or by a dedicated password-based key derivation strategy.

Encrypt
func Encrypt(plaintext []byte, key []byte) ([]byte, error)

Encrypt encrypts plaintext with AES-256-GCM and returns raw bytes. The returned value is formatted as:

nonce || ciphertext || authentication-tag

Use this API when you want to store or transmit binary data directly.

encrypted, err := e5t.Encrypt([]byte("secret message"), key)
if err != nil {
    return err
}
EncryptAsString
func EncryptAsString(plaintext []byte, key []byte) (string, error)

EncryptAsString encrypts plaintext and returns the encrypted bytes as a hex string. Use it for text-only storage locations such as environment variables, JSON fields, fixtures, or database columns that expect text.

encrypted, err := e5t.EncryptAsString([]byte("secret message"), key)
if err != nil {
    return err
}
Decrypt
func Decrypt(ciphertext []byte, key []byte) ([]byte, error)

Decrypt reverses Encrypt. It expects raw encrypted bytes containing the nonce prefix generated by Encrypt.

decrypted, err := e5t.Decrypt(encrypted, key)
if err != nil {
    return err
}
DecryptFromText
func DecryptFromText(hexCiphertext string, key []byte) ([]byte, error)

DecryptFromText reverses EncryptAsString. It decodes the hex string and then decrypts the underlying bytes.

decrypted, err := e5t.DecryptFromText(encryptedText, key)
if err != nil {
    return err
}
VerifyEncryption
func VerifyEncryption(original []byte, encrypted string, key []byte) (bool, error)

VerifyEncryption decrypts a hex-encoded ciphertext and compares the result with original.

encrypted, err := e5t.EncryptAsString(original, key)
if err != nil {
    return err
}

match, err := e5t.VerifyEncryption(original, encrypted, key)
if err != nil {
    return err
}
if !match {
    return errors.New("decrypted data does not match original")
}

AES-GCM already authenticates ciphertext during decryption. Use VerifyEncryption when your application specifically needs to compare decrypted data with an expected plaintext value.

Error Handling

The package exposes sentinel errors for common validation failures:

var (
    ErrInvalidKeySize     = errors.New("key must be exactly 32 bytes for AES-256")
    ErrCiphertextTooShort = errors.New("ciphertext shorter than nonce prefix")
)

Use errors.Is when branching on these errors:

decrypted, err := e5t.DecryptFromText(encrypted, key)
if err != nil {
    switch {
    case errors.Is(err, e5t.ErrInvalidKeySize):
        return fmt.Errorf("invalid encryption key: %w", err)
    case errors.Is(err, e5t.ErrCiphertextTooShort):
        return fmt.Errorf("invalid ciphertext: %w", err)
    default:
        return fmt.Errorf("decrypt data: %w", err)
    }
}
_ = decrypted

Other errors may come from the Go standard library, such as hex decoding errors or AES-GCM authentication failures when ciphertext is corrupted or the wrong key is used.

Security Notes

  • Keep encryption keys out of source control and application logs.
  • Use a unique salt or context string when deriving separate keys with GenerateHashKey.
  • Store or transmit the full encrypted output. The nonce prefix is required for decryption and is safe to store with the ciphertext.
  • Reusing a key is expected; reusing a key and nonce pair is not. Encrypt and EncryptAsString generate a fresh nonce automatically.
  • Losing the key means losing the ability to decrypt the data.
  • A wrong key, modified ciphertext, or truncated ciphertext causes decryption to fail.
  • GenerateHashKey is a convenience helper, not a memory-hard password hashing function.

Quality Automation

The repository follows the same GitHub quality practices used by slashdevops/httpx:

  • main.yml validates pushes to main with format checks, go vet, race-enabled tests, coverage, and go build.
  • pr.yml runs the same quality gate for pull requests targeting main.
  • codeql.yml runs CodeQL for Go and GitHub Actions on pushes, pull requests, and a weekly schedule.
  • release.yml validates tagged releases and publishes GitHub releases with generated notes.
  • dependabot.yml checks Go module and GitHub Actions updates weekly.

Project Structure

.
|-- .github/         GitHub Actions, CodeQL, Dependabot, and release metadata
|-- .golangci.yaml   Optional local golangci-lint configuration
|-- doc.go           Package documentation rendered by pkg.go.dev
|-- e5t.go           Public encryption API
|-- e5t_test.go      Unit tests and benchmarks
|-- example_test.go  Executable Go examples for documentation
|-- go.mod           Module definition with no external requirements
|-- LICENSE          Apache License 2.0
|-- README.md        Project overview and usage guide
`-- SECURITY.md      Vulnerability reporting policy

Testing

Run the test suite:

go test ./...

Run benchmarks:

go test -bench=. ./...

Check test coverage:

go test -cover ./...

Run the same local quality checks used by CI:

go fmt ./...
go vet ./...
go test -race -coverprofile=/tmp/e5t-coverage.txt -covermode=atomic ./...
go build ./...

License

e5t is licensed under the Apache License 2.0.

Contributing

Issues and pull requests are welcome at github.com/slashdevops/e5t. Please keep changes small, idiomatic, tested, documented, and dependency-free unless there is a clear reason to expand the project scope.

Documentation

Overview

Package e5t provides small AES-256-GCM encryption helpers built only on the Go standard library.

The package encrypts and decrypts byte slices with AES in Galois/Counter Mode (GCM). GCM is an authenticated encryption mode: successful decryption proves that the ciphertext, authentication tag, and nonce match the supplied key.

Encryption Format

Encrypt returns raw bytes in this format:

nonce || ciphertext || authentication-tag

EncryptAsString returns the same bytes encoded as hexadecimal text. Decrypt expects the raw byte format, and DecryptFromText expects the hex-encoded format.

Keys

AES-256 requires a 32-byte key. GenerateHashKey is a convenience helper that returns 32 bytes by hashing a key string plus an optional salt with SHA-256:

key := e5t.GenerateHashKey("application-secret", "config-v1")

For higher-risk secrets or user-entered passwords, pass a 32-byte key produced by a dedicated key management or password-based key derivation strategy.

Basic Usage

key := e5t.GenerateHashKey("my-secret-password", "unique-salt")

encrypted, err := e5t.EncryptAsString([]byte("sensitive data"), key)
if err != nil {
	log.Fatal(err)
}

decrypted, err := e5t.DecryptFromText(encrypted, key)
if err != nil {
	log.Fatal(err)
}

fmt.Println(string(decrypted))

Error Handling

The package returns ErrInvalidKeySize when a key is not exactly 32 bytes and ErrCiphertextTooShort when encrypted input is shorter than the nonce prefix. Use errors.Is to branch on these sentinel errors.

Dependencies

e5t has zero third-party dependencies. It uses crypto/aes, crypto/cipher, crypto/rand, crypto/sha256, encoding/hex, and other standard library packages.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidKeySize is returned when a key is not exactly 32 bytes for AES-256.
	ErrInvalidKeySize = errors.New("key must be exactly 32 bytes for AES-256")

	// ErrCiphertextTooShort is returned when ciphertext is shorter than the nonce prefix.
	ErrCiphertextTooShort = errors.New("ciphertext shorter than nonce prefix")
)

Sentinel errors for common error conditions.

Functions

func Decrypt

func Decrypt(ciphertext []byte, key []byte) ([]byte, error)

Decrypt decrypts raw bytes produced by Encrypt.

ciphertext must include the nonce prefix generated during encryption. key must be the same 32-byte key used for encryption.

Example (WrongKey)

ExampleDecrypt_wrongKey demonstrates error handling with wrong key.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	// Encrypt with one key
	key1 := e5t.GenerateHashKey("password1", "salt")
	encrypted, err := e5t.EncryptAsString([]byte("secret"), key1)
	if err != nil {
		log.Fatal(err)
	}

	// Try to decrypt with wrong key
	key2 := e5t.GenerateHashKey("password2", "salt")
	_, err = e5t.DecryptFromText(encrypted, key2)
	if err != nil {
		fmt.Println("Decryption failed with wrong key")
	}

}
Output:
Decryption failed with wrong key

func DecryptFromText

func DecryptFromText(hexCiphertext string, key []byte) ([]byte, error)

DecryptFromText decodes hexCiphertext and decrypts it with AES-256-GCM.

hexCiphertext must be a value returned by EncryptAsString. key must be the same 32-byte key used for encryption.

Example

ExampleDecryptFromText demonstrates basic decryption from hex-encoded string.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	key := e5t.GenerateHashKey("my-secret-password", "random-salt")

	// Encrypt
	plaintext := []byte("Secret Message")
	encrypted, err := e5t.EncryptAsString(plaintext, key)
	if err != nil {
		log.Fatal(err)
	}

	// Decrypt
	decrypted, err := e5t.DecryptFromText(encrypted, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(decrypted))

}
Output:
Secret Message

func Encrypt

func Encrypt(plaintext []byte, key []byte) ([]byte, error)

Encrypt encrypts plaintext with AES-256-GCM and returns raw encrypted bytes.

The returned bytes are formatted as nonce followed by encrypted data and the GCM authentication tag. key must be exactly 32 bytes.

Example

ExampleEncrypt demonstrates basic encryption returning raw bytes.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	// Generate an encryption key
	key := e5t.GenerateHashKey("my-secret-password", "random-salt")

	// Encrypt some data
	plaintext := []byte("Hello, World!")
	encrypted, err := e5t.Encrypt(plaintext, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Encrypted data length: %d bytes\n", len(encrypted))
	fmt.Println("Encrypted data is raw bytes")

}
Output:
Encrypted data length: 41 bytes
Encrypted data is raw bytes

func EncryptAsString

func EncryptAsString(plaintext []byte, key []byte) (string, error)

EncryptAsString encrypts plaintext with AES-256-GCM and returns hex-encoded ciphertext.

The decoded ciphertext is formatted as nonce followed by encrypted data and the GCM authentication tag. key must be exactly 32 bytes.

Example

ExampleEncryptAsString demonstrates basic encryption.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	// Generate an encryption key
	key := e5t.GenerateHashKey("my-secret-password", "random-salt")

	// Encrypt some data
	plaintext := []byte("Hello, World!")
	encrypted, err := e5t.EncryptAsString(plaintext, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Encrypted data length: %d characters\n", len(encrypted))
	fmt.Println("Encrypted data is hex-encoded")

}
Output:
Encrypted data length: 82 characters
Encrypted data is hex-encoded
Example (ApiKey)

ExampleEncryptAsString_apiKey demonstrates encrypting an API key.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	// Encrypt a sensitive API key
	apiKey := []byte("sk_live_1234567890abcdef")
	encryptionKey := e5t.GenerateHashKey("master-key", "api-keys-v1")

	encrypted, err := e5t.EncryptAsString(apiKey, encryptionKey)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("API key encrypted")

	// Later, decrypt it
	decrypted, err := e5t.DecryptFromText(encrypted, encryptionKey)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Decrypted API key: %s\n", string(decrypted))

}
Output:
API key encrypted
Decrypted API key: sk_live_1234567890abcdef
Example (Config)

ExampleEncryptAsString_config demonstrates encrypting configuration data.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	// Simulate encrypting sensitive configuration
	config := []byte(`{"db_password":"secret123","api_key":"abc-xyz"}`)

	// Use application-specific key and salt
	key := e5t.GenerateHashKey("app-master-key", "config-v1")

	encrypted, err := e5t.EncryptAsString(config, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Configuration encrypted")

	// Decrypt when needed
	decrypted, err := e5t.DecryptFromText(encrypted, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Decrypted config length: %d bytes\n", len(decrypted))

}
Output:
Configuration encrypted
Decrypted config length: 47 bytes
Example (MultipleEncryptions)

ExampleEncryptAsString_multipleEncryptions demonstrates that the same plaintext encrypts differently each time.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	key := e5t.GenerateHashKey("password", "salt")
	message := []byte("same message")

	// Encrypt the same message twice
	encrypted1, err := e5t.EncryptAsString(message, key)
	if err != nil {
		log.Fatal(err)
	}

	encrypted2, err := e5t.EncryptAsString(message, key)
	if err != nil {
		log.Fatal(err)
	}

	// The encrypted values will be different
	fmt.Printf("First encryption length: %d\n", len(encrypted1))
	fmt.Printf("Second encryption length: %d\n", len(encrypted2))
	fmt.Println("Each encryption produces unique ciphertext")

	// But both decrypt to the same message
	decrypted1, err := e5t.DecryptFromText(encrypted1, key)
	if err != nil {
		log.Fatal(err)
	}

	decrypted2, err := e5t.DecryptFromText(encrypted2, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Both decrypt correctly: %t\n", string(decrypted1) == string(decrypted2))

}
Output:
First encryption length: 80
Second encryption length: 80
Each encryption produces unique ciphertext
Both decrypt correctly: true
Example (RoundTrip)

ExampleEncryptAsString_roundTrip demonstrates a complete encryption and decryption cycle.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	// Create a key
	key := e5t.GenerateHashKey("secure-password", "app-salt-v1")

	// Original message
	message := []byte("This is a confidential message.")

	// Encrypt
	encrypted, err := e5t.EncryptAsString(message, key)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Message encrypted successfully")

	// Decrypt
	decrypted, err := e5t.DecryptFromText(encrypted, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Decrypted: %s\n", string(decrypted))

}
Output:
Message encrypted successfully
Decrypted: This is a confidential message.
Example (UserSpecific)

ExampleEncryptAsString_userSpecific demonstrates per-user encryption.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	masterKey := "application-master-key"

	// Encrypt data for user 1
	user1ID := "user-001"
	user1Key := e5t.GenerateHashKey(masterKey, user1ID)
	user1Data := []byte("User 1 private data")
	encrypted1, err := e5t.EncryptAsString(user1Data, user1Key)
	if err != nil {
		log.Fatal(err)
	}

	// Encrypt data for user 2
	user2ID := "user-002"
	user2Key := e5t.GenerateHashKey(masterKey, user2ID)
	user2Data := []byte("User 2 private data")
	encrypted2, err := e5t.EncryptAsString(user2Data, user2Key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Each user has their own encrypted data")

	// Each user can only decrypt their own data
	decrypted1, err := e5t.DecryptFromText(encrypted1, user1Key)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("User 1 data: %s\n", string(decrypted1))

	decrypted2, err := e5t.DecryptFromText(encrypted2, user2Key)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("User 2 data: %s\n", string(decrypted2))

}
Output:
Each user has their own encrypted data
User 1 data: User 1 private data
User 2 data: User 2 private data

func GenerateHashKey

func GenerateHashKey(secret string, salt ...string) []byte

GenerateHashKey creates a deterministic 32-byte key from secret and an optional salt.

It hashes secret plus the first salt value with SHA-256. This helper is useful for deriving stable AES-256 keys from application secrets or context strings. For user-entered passwords or high-risk secrets, prefer a dedicated key management or password-based key derivation strategy.

Example

ExampleGenerateHashKey demonstrates basic key generation.

package main

import (
	"fmt"

	"github.com/slashdevops/e5t"
)

func main() {
	// Generate a key without salt
	key1 := e5t.GenerateHashKey("my-password")
	fmt.Printf("Key length: %d bytes\n", len(key1))

	// Generate a key with salt (recommended)
	key2 := e5t.GenerateHashKey("my-password", "unique-salt")
	fmt.Printf("Key with salt length: %d bytes\n", len(key2))

}
Output:
Key length: 32 bytes
Key with salt length: 32 bytes
Example (WithUserID)

ExampleGenerateHashKey_withUserID demonstrates using user ID as salt.

package main

import (
	"fmt"

	"github.com/slashdevops/e5t"
)

func main() {
	userID := "user-12345"
	masterPassword := "app-master-secret"

	// Generate a unique key for this user
	userKey := e5t.GenerateHashKey(masterPassword, userID)
	fmt.Printf("Generated user-specific key: %d bytes\n", len(userKey))

}
Output:
Generated user-specific key: 32 bytes

func VerifyEncryption

func VerifyEncryption(original []byte, encrypted string, key []byte) (bool, error)

VerifyEncryption decrypts encrypted and compares the result with original.

encrypted must be a hex-encoded ciphertext returned by EncryptAsString. The function returns false with a nil error when decryption succeeds but the decrypted data does not match original.

Example

ExampleVerifyEncryption demonstrates verifying encrypted data matches original data.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	key := e5t.GenerateHashKey("my-password", "my-salt")

	original := []byte("Important data to verify")
	encrypted, err := e5t.EncryptAsString(original, key)
	if err != nil {
		log.Fatal(err)
	}

	// Verify the encrypted data matches the original
	match, err := e5t.VerifyEncryption(original, encrypted, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Data matches: %t\n", match)

}
Output:
Data matches: true
Example (IntegrityCheck)

ExampleVerifyEncryption_integrityCheck demonstrates using VerifyEncryption for integrity checking.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	key := e5t.GenerateHashKey("secure-key", "app-v1")

	// Simulate storing data
	userData := []byte("User profile data")
	encryptedData, err := e5t.EncryptAsString(userData, key)
	if err != nil {
		log.Fatal(err)
	}

	// Store encryptedData in database/file...
	// Later, retrieve and verify integrity

	verified, err := e5t.VerifyEncryption(userData, encryptedData, key)
	if err != nil {
		fmt.Println("Verification failed:", err)
		return
	}

	if verified {
		fmt.Println("Data integrity verified successfully")
	} else {
		fmt.Println("Data integrity check failed - data was modified")
	}

}
Output:
Data integrity verified successfully
Example (Mismatch)

ExampleVerifyEncryption_mismatch demonstrates verification with mismatched data.

package main

import (
	"fmt"
	"log"

	"github.com/slashdevops/e5t"
)

func main() {
	key := e5t.GenerateHashKey("password", "salt")

	original := []byte("Original message")
	different := []byte("Different message")

	// Encrypt different data
	encrypted, err := e5t.EncryptAsString(different, key)
	if err != nil {
		log.Fatal(err)
	}

	// Verify against original (should not match)
	match, err := e5t.VerifyEncryption(original, encrypted, key)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Data matches: %t\n", match)

}
Output:
Data matches: false

Types

This section is empty.

Jump to

Keyboard shortcuts

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