Documentation
¶
Overview ¶
Package f5messageembed implements the F5 steganographic algorithm for embedding hidden messages into JPEG DCT coefficients.
The F5 algorithm was developed by Andreas Westfeld (2001) and is designed to resist both visual and statistical detection attacks while maintaining high embedding capacity. This implementation produces byte-identical output to the Java reference implementation for interoperability.
Algorithm Overview ¶
F5 builds upon earlier steganographic techniques:
- Jsteg: LSB replacement (detectable via chi-square attack)
- F3: Decrements instead of overwrites (detectable via even coefficient surplus)
- F4: Sign-based encoding (resistant to statistical attacks)
- F5: F4 + matrix encoding + permutative straddling (optimal efficiency)
Key Features ¶
- Matrix Encoding: Uses (1, n, k) codes where n = 2^k - 1. Embeds k message bits in n coefficients with at most 1 change per code word.
- Permutative Straddling: Shuffles coefficient indices before embedding to distribute changes uniformly and prevent localized statistical analysis.
- Shrinkage Handling: When decrementing |1| or |-1| produces 0, the bit is re-embedded using the next available coefficient.
- Optimal K Selection: Automatically selects the best k parameter (1-8) based on message length and usable coefficient capacity.
Security Warning ¶
This package uses SHA1PRNG for pseudorandom number generation to maintain compatibility with the Java reference implementation. SHA1PRNG is NOT cryptographically secure by modern standards. The algorithm relies on SHA-1 which has known collision vulnerabilities.
DO NOT use this package for security-critical applications requiring cryptographic randomness. The PRNG is deterministic and predictable given the password/seed.
This package is intended for:
- Compatibility with existing F5-encoded images (e.g., PixelKnot)
- Research and educational purposes
- Applications where statistical undetectability is prioritized over cryptographic security
Basic Usage ¶
Embedding a message into JPEG DCT coefficients:
import "github.com/0verkilll/f5messageembed"
// Coefficients from JPEG DCT (obtained via jpeg package)
coefficients := []int16{/* ... */}
password := "secret"
message := []byte("Hello, World!")
result, err := f5messageembed.Embed(coefficients, password, message)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Embedded %d bytes using k=%d\n", result.BytesEmbedded, result.KParameter)
Capacity Calculation ¶
Before embedding, you can calculate the maximum message capacity:
capacity := f5messageembed.CalculateCapacity(coefficients)
fmt.Printf("Usable coefficients: %d\n", capacity.UsableCoefficients)
fmt.Printf("Max capacity at k=4: %d bytes\n", capacity.CapacityByK[4])
Advanced Options ¶
Using options for logging and custom k parameter:
result, err := f5messageembed.EmbedWithOptions(
coefficients,
password,
message,
f5messageembed.EmbedOptions{
Logger: myLogger,
ForceK: 4, // Force k=4 instead of auto-selection
},
)
Research Functions ¶
The package exports low-level functions for research purposes:
// Get steganographic bit value from coefficient bit := f5messageembed.GetStegoBit(coefficient) // Modify coefficient to flip its steganographic bit newCoeff := f5messageembed.ModifyCoefficient(coefficient) // Matrix encoding for a code word changePos, err := f5messageembed.MatrixEncode(codeWord, messageBits, k) // Select optimal k parameter k, err := f5messageembed.SelectOptimalK(usableCoeffCount, messageBits)
Dependencies ¶
This package depends on the following sibling packages:
- f5coefficient: DCT coefficient bit operations (GetStegoBit, ModifyCoefficient, IsShrinkageCandidate)
- f5core: Shared constants (MaxMessageSize, HeaderSize, CoefficientMin/Max, DeZigZag table)
- f5matrix: Matrix encoding for (1,n,k) code words
- f5prng: Unified PRNG interfaces and SHA1PRNG implementation for deterministic randomness
- fisheryates: Fisher-Yates shuffle for coefficient permutation
- logger: Optional structured logging support
Indirect dependencies (via f5prng):
- sha1: SHA-1 hasher for PRNG seeding
- i18n: Internationalized error messages
Reference ¶
Westfeld, A. (2001). F5 - A Steganographic Algorithm: High Capacity Despite Better Steganalysis. Lecture Notes in Computer Science, 2137, 289-302.
Index ¶
- Constants
- Variables
- func ApplyDeZigZag(shuffledIndex int) int
- func ApplyMatrixChange(coefficients []int16, indices []int, position int) int16
- func BuildHeader(k, messageSize int) int32
- func CalculateCapacityForK(usableCoeffCount, k int) int
- func CalculateCapacityForKWithShrinkage(usableCoeffCount, magnitudeOneCount, k int) int
- func CalculateEmbeddingRate(k int) float64
- func CodeWordLength(k int) int
- func ComputeCodeWordHash(codeWord []int, n int) int
- func DetectShrinkage(coefficient int16) bool
- func EffectiveCapacityCoefficients(usableCoeffCount, magnitudeOneCount int) int
- func EmbedHeader(coefficients []int16, permutation []int, header int32) (nextIndex int, err error)
- func ExtractStegoBits(coefficients []int16) []int
- func GeneratePermutation(random f5prng.RandomSource, size int) ([]int, error)
- func GetStegoBit(coefficient int16) int
- func HandleShrinkage(codeWord []int, position int, coefficient int16) (shrunk bool, newCoeff int16)
- func InitializePRNG(password string) f5prng.RandomSourcedeprecated
- func IsUsableCoefficient(index int, coefficient int16) bool
- func MatrixEncode(codeWord []int, messageBits, k int) (changePosition int, err error)
- func ModifyCoefficient(coefficient int16) int16
- func SelectOptimalK(usableCoeffCount, messageBits int) (k int, err error)
- func SelectOptimalKWithShrinkage(usableCoeffCount, magnitudeOneCount, messageBits int) (int, error)
- func SetTranslator(t TranslatorProvider)
- func Translate(key string) string
- func ValidateCoefficients(coefficients []int16) error
- func ValidateInputs(coefficients []int16, password string, message []byte, capacity int) error
- func ValidateMessage(message []byte, capacity int) error
- func ValidatePassword(password string) error
- func XORHeaderWithPRNG(header int32, random RandomSource) int32
- type CapacityResult
- type EmbedOption
- type EmbedOptions
- type EmbedResult
- func Embed(coefficients []int16, password string, message []byte) (*EmbedResult, error)
- func EmbedWithOptions(coefficients []int16, password string, message []byte, opts EmbedOptions) (*EmbedResult, error)
- func EmbedWithRandomSource(coefficients []int16, random RandomSource, message []byte, opts EmbedOptions) (*EmbedResult, error)
- type RandomSourcedeprecated
- type TranslatorProvider
- type ValidationError
Constants ¶
const ( // MaxMessageSize is the maximum message size in bytes that can be embedded. // See f5core.MaxMessageSize for documentation. MaxMessageSize = f5core.MaxMessageSize // CoefficientMin is the minimum valid JPEG quantized DCT coefficient value. // See f5core.CoefficientMin for documentation. CoefficientMin = f5core.CoefficientMin // CoefficientMax is the maximum valid JPEG quantized DCT coefficient value. // See f5core.CoefficientMax for documentation. CoefficientMax = f5core.CoefficientMax // HeaderSize is the number of bits in the F5 message header. // See f5core.HeaderSize for documentation. HeaderSize = f5core.HeaderSize )
Re-export constants from f5core for backward compatibility.
const ( ErrKeyEmptyCoefficients = "f5messageembed.error.empty_coefficients" //nolint:gosec // G101: translation key, not credential ErrKeyEmptyPassword = "f5messageembed.error.empty_password" ErrKeyMessageTooLarge = "f5messageembed.error.message_too_large" ErrKeyInsufficientCapacity = "f5messageembed.error.insufficient_capacity" ErrKeyInvalidCoefficientVal = "f5messageembed.error.invalid_coefficient_range" ErrKeyInvalidKParameter = "f5messageembed.error.invalid_k_parameter" ErrKeyInvalidForceK = "f5messageembed.error.invalid_force_k" ErrKeyPermutationFailed = "f5messageembed.error.permutation_failed" ErrKeyHeaderEmbedFailed = "f5messageembed.error.header_embed_failed" ErrKeyMessageEmbedFailed = "f5messageembed.error.message_embed_failed" ErrKeyMatrixEncodeFailed = "f5messageembed.error.matrix_encode_failed" ErrKeyInsufficientCoeffs = "f5messageembed.error.insufficient_coefficients" ErrKeyInsufficientHeader = "f5messageembed.error.insufficient_header_coefficients" ErrKeyCapacityDetails = "f5messageembed.error.capacity_details" ErrKeyNilRandomSource = "f5messageembed.error.nil_random_source" )
Error keys for i18n translation. These keys correspond to entries in locales/en-US.json. #nosec G101 -- These are translation keys, not hardcoded credentials
Variables ¶
var ( ErrEmptyCodeWord = f5matrix.ErrEmptyCodeWord ErrInvalidKParameter = f5matrix.ErrInvalidKParameter ErrCodeWordSizeMismatch = f5matrix.ErrCodeWordSizeMismatch ErrMessageBitsExceedK = f5matrix.ErrMessageBitsExceedK )
Re-export matrix encoding errors from f5matrix for backward compatibility. These are part of the public API and intentionally exported for callers.
Functions ¶
func ApplyDeZigZag ¶
ApplyDeZigZag transforms a shuffled coefficient index using the JPEG de-zigzag table.
JPEG stores DCT coefficients in zigzag order to improve compression by grouping zero coefficients together. The F5 algorithm applies this transformation after permutation to access coefficients in their natural 8x8 block positions.
Formula: zigzag = shuffled - shuffled%64 + deZigZag[shuffled%64]
This can be understood as:
- (shuffled / 64) * 64: The base offset of the 8x8 block
- deZigZag[shuffled % 64]: The de-zigzag position within the block
Parameters:
- shuffledIndex: The permuted coefficient index
Returns:
- The de-zigzagged index for accessing the coefficient array
Example:
shuffled := permutation[i] zigzag := ApplyDeZigZag(shuffled) coefficient := coefficients[zigzag]
func ApplyMatrixChange ¶
ApplyMatrixChange applies the coefficient modification at the calculated position. This is an alias to f5matrix.ApplyMatrixChange for backward compatibility. See f5matrix.ApplyMatrixChange for full documentation.
func BuildHeader ¶
BuildHeader constructs a 32-bit F5 header from k parameter and message size.
The classic F5 header format is:
- Bits 24-31 (8 bits): k parameter value (matrix encoding parameter)
- Bits 0-22 (23 bits): message file size in bytes
This header format allows messages up to 8,388,607 bytes (2^23 - 1) and k values from 0-255 (though only 1-8 are typically used).
Parameters:
- k: The matrix encoding parameter (typically 1-8)
- messageSize: The message size in bytes (max MaxMessageSize)
Returns:
- A 32-bit header value ready for XOR and embedding
Example:
header := BuildHeader(4, 1000) // header bits 24-31 = 4 (k parameter) // header bits 0-22 = 1000 (message size)
func CalculateCapacityForK ¶
CalculateCapacityForK computes the embedding capacity in bits for a specific k value.
This helper function calculates how many message bits can be embedded using a specific k parameter, accounting for the header overhead.
Parameters:
- usableCoeffCount: Number of non-zero, non-DC coefficients
- k: The matrix encoding parameter (1-8)
Returns:
- The message capacity in bits (excluding header)
Example:
// With 1000 coefficients and k=4 (n=15): capacity := CalculateCapacityForK(1000, 4) // Returns (1000/15)*4 - 32 = 66*4 - 32 = 232 bits
NOTE: This function does not account for shrinkage. For accuracy that matches F5.jar's actual k-selection on real images, prefer CalculateCapacityForKWithShrinkage which uses f5.jar's `_expected` effective pool (P − h(1)) + ⌊0.49·h(1)⌋ before computing capacity.
func CalculateCapacityForKWithShrinkage ¶
CalculateCapacityForKWithShrinkage is the shrinkage-aware variant of CalculateCapacityForK. It computes the message-byte capacity using the f5.jar effective-capacity `_expected` = (P − h(1)) + ⌊0.49·h(1)⌋, which reserves space for the expected shrinkage cascade.
Parameters mirror CalculateCapacityForK plus magnitudeOneCount, the count of usable coefficients with |value| = 1. CapacityResult.EstimatedShrinkageFactor times CapacityResult.UsableCoefficients gives the same value.
Use this for embed-time k-selection when capacity fidelity to F5.jar's published formula matters more than the simpler/faster non-shrinkage form.
func CalculateEmbeddingRate ¶
CalculateEmbeddingRate computes the embedding rate R(k) for a given k parameter.
The embedding rate represents the efficiency of the matrix encoding: R(k) = k / (2^k - 1)
As k increases, the embedding rate decreases but embedding efficiency improves (fewer changes per embedded bit). The optimal k balances capacity needs with statistical detectability.
Embedding rates for common k values:
- k=1: R(1) = 1.000 (1 bit per coefficient, lowest efficiency)
- k=2: R(2) = 0.667 (2 bits per 3 coefficients)
- k=3: R(3) = 0.429 (3 bits per 7 coefficients)
- k=4: R(4) = 0.267 (4 bits per 15 coefficients)
- k=5: R(5) = 0.161 (5 bits per 31 coefficients)
- k=6: R(6) = 0.095 (6 bits per 63 coefficients)
- k=7: R(7) = 0.055 (7 bits per 127 coefficients)
- k=8: R(8) = 0.031 (8 bits per 255 coefficients, highest efficiency)
Parameters:
- k: The matrix encoding parameter (typically 1-8)
Returns:
- The embedding rate as a float64 value
- Returns 0 for invalid k values (k <= 0)
Example:
rate := CalculateEmbeddingRate(4) // Returns 0.267 (4/15)
func CodeWordLength ¶
CodeWordLength returns the code word length n for a given k parameter. This is an alias to f5matrix.CodeWordLength for backward compatibility. See f5matrix.CodeWordLength for full documentation.
func ComputeCodeWordHash ¶
ComputeCodeWordHash computes the hash function for matrix encoding. This is an alias to f5matrix.ComputeCodeWordHash for backward compatibility. See f5matrix.ComputeCodeWordHash for full documentation.
func DetectShrinkage ¶
DetectShrinkage determines if modifying a coefficient will cause shrinkage. This is an alias to f5coefficient.IsShrinkageCandidate for backward compatibility. See f5coefficient.IsShrinkageCandidate for full documentation.
func EffectiveCapacityCoefficients ¶
EffectiveCapacityCoefficients returns the F5 effective number of coefficients available for matrix encoding (f5.jar's `_expected`), which reserves space for the expected shrinkage cascade.
It reproduces f5.jar's shipped calculation BYTE-EXACTLY (james/JpegEncoder.java:281-282):
_large = coeffCount - _zero - _one - coeffCount/64; // = P - h(1) _expected = _large + (int) (0.49 * _one); // INTEGER TRUNCATION
where P = usableCoeffCount (non-zero, non-DC) and _one = h(1) = magnitudeOneCount. Re-expressed against P:
_expected = (P − h(1)) + ⌊0.49·h(1)⌋
This is NOT the same as P − round(0.51·h(1)). For even h(1) the two differ by exactly 1 — the algebraic identity (P−h1)+⌊0.49·h1⌋ = P − ⌈0.51·h1⌉ holds, and ⌈0.51·h1⌉ exceeds round(0.51·h1) by 1 whenever h1 is even. A 1-coefficient error in `_expected` flips the selected k at a code-word bucket boundary (selectOptimalKJavaExact's `usable < byteToEmbed+4` test), producing byte-divergent, un-extractable stego. The earlier round(0.51·h1) form matched f5.jar only on the ~half of covers with odd h(1); this exact truncation matches on all of them.
Go's int() conversion truncates toward zero for the non-negative product 0.49·h(1), matching Java's (int) cast. Source: Westfeld 2001 §3, Liu 2020 Eq.(1), and f5.jar james/JpegEncoder.java.
Returns 0 if usableCoeffCount is non-positive or the reserve consumes the entire pool.
func EmbedHeader ¶
EmbedHeader embeds a 32-bit header into coefficients using simple LSB embedding.
Unlike the message payload which uses matrix encoding, the F5 header is embedded using simple LSB modification. This is because the header must be extracted first to determine the k parameter needed for matrix decoding.
The function:
- Iterates through permuted coefficient indices
- Skips DC coefficients (index % 64 == 0)
- Applies de-zigzag transformation
- Skips zero coefficients
- Embeds one header bit per usable coefficient
- Modifies coefficient if current stego bit doesn't match desired bit
Parameters:
- coefficients: The DCT coefficient array to modify (modified in-place)
- permutation: The Fisher-Yates permutation of coefficient indices
- header: The 32-bit XORed header value to embed
Returns:
- nextIndex: The permutation index to resume from for message embedding
- err: An error if insufficient coefficients for header embedding
Example:
nextIndex, err := EmbedHeader(coefficients, permutation, xoredHeader)
if err != nil {
return err
}
// Continue embedding message starting at permutation[nextIndex]
func ExtractStegoBits ¶
ExtractStegoBits extracts the steganographic bit values from a slice of coefficients. This is an alias to f5coefficient.ExtractStegoBits for backward compatibility. See f5coefficient.ExtractStegoBits for full documentation.
func GeneratePermutation ¶
func GeneratePermutation(random f5prng.RandomSource, size int) ([]int, error)
GeneratePermutation generates a Fisher-Yates permutation of indices.
The permutation is used for "permutative straddling" in F5, which shuffles coefficient indices before embedding to distribute changes uniformly across the image. This prevents localized statistical analysis attacks.
The function uses the fisheryates package's GenerateInto method for zero allocation when reusing buffers. The permutation is deterministic based on the RandomSource state, ensuring the same password produces the same permutation for both embedding and extraction.
PRNG Consumption: This function consumes PRNG state proportional to the size parameter. After calling GeneratePermutation, the PRNG state will have advanced exactly as the Java F5 implementation expects.
Parameters:
- random: A seeded f5prng.RandomSource (typically from InitializePRNG)
- size: The number of elements in the permutation (coefficient count)
Returns:
- A slice containing a permutation of [0, 1, ..., size-1]
- An error if size is negative or exceeds fisheryates.MaxPermutationSize
Example:
prng := InitializePRNG("password")
defer prng.Clear()
perm, err := GeneratePermutation(prng, len(coefficients))
if err != nil {
return err
}
// perm now contains shuffled indices for coefficient access
func GetStegoBit ¶
GetStegoBit extracts the steganographic bit value from a DCT coefficient. This is an alias to f5coefficient.GetStegoBit for backward compatibility. See f5coefficient.GetStegoBit for full documentation.
func HandleShrinkage ¶
HandleShrinkage applies a coefficient modification and detects shrinkage.
This function combines the modification operation with shrinkage detection, providing a single entry point for the shrinkage handling workflow in the F5 embedding process.
The function:
- Applies ModifyCoefficient to decrement the absolute value
- Detects if the original coefficient was 1 or -1 (shrinkage case)
- Returns both the shrinkage status and the new coefficient value
The codeWord and position parameters are included for interface consistency with the matrix encoding workflow, where shrinkage handling may need context about which code word and position triggered the shrinkage. In the current implementation, these parameters are reserved for future use.
Parameters:
- codeWord: The current code word being processed (reserved for future use)
- position: The position within the code word (reserved for future use)
- coefficient: The coefficient to modify
Returns:
- shrunk: true if the modification caused the coefficient to become 0
- newCoeff: the coefficient value after modification
Example:
shrunk, newCoeff := HandleShrinkage(codeWord, 1, int16(5)) // shrunk = false, newCoeff = 4 shrunk, newCoeff = HandleShrinkage(codeWord, 1, int16(1)) // shrunk = true, newCoeff = 0 shrunk, newCoeff = HandleShrinkage(codeWord, 1, int16(-1)) // shrunk = true, newCoeff = 0
func InitializePRNG
deprecated
func InitializePRNG(password string) f5prng.RandomSource
InitializePRNG creates and initializes a SecureRandom instance for F5 embedding.
Deprecated: Use EmbedWithRandomSource with a pre-seeded RandomSource instead. This function is kept for backward compatibility but new code should prefer the dependency injection pattern using EmbedWithRandomSource which accepts a pre-seeded RandomSource, allowing better testability and flexibility.
The PRNG is seeded with the password bytes using SHA-1 hashing, matching Java's SecureRandom SHA1PRNG algorithm exactly. This ensures byte-identical output when using the same password as the Java F5 reference implementation.
The returned RandomSource should be cleared when no longer needed to prevent sensitive password-derived state from remaining in memory:
prng := InitializePRNG(password) defer prng.Clear() // ... use prng
Parameters:
- password: The password string used to seed the PRNG
Returns:
- A seeded f5prng.RandomSource ready for permutation generation
Migration Example:
Before (deprecated):
result, err := EmbedWithOptions(coefficients, password, message, opts)
After (recommended):
prng := InitializePRNG(password) // or your own RandomSource implementation defer prng.Clear() result, err := EmbedWithRandomSource(coefficients, prng, message, opts)
func IsUsableCoefficient ¶
IsUsableCoefficient determines if a coefficient can carry steganographic data. This is an alias to f5coefficient.IsUsableCoefficient for backward compatibility. See f5coefficient.IsUsableCoefficient for full documentation.
func MatrixEncode ¶
MatrixEncode performs (1, n, k) matrix encoding to determine which coefficient to modify in order to embed k message bits. This is an alias to f5matrix.MatrixEncode for backward compatibility. See f5matrix.MatrixEncode for full documentation.
func ModifyCoefficient ¶
ModifyCoefficient decrements the absolute value of a coefficient. This is an alias to f5coefficient.ModifyCoefficient for backward compatibility. See f5coefficient.ModifyCoefficient for full documentation.
func SelectOptimalK ¶
SelectOptimalK determines the best k parameter for embedding a message.
F5 uses (1, n, k) matrix encoding where n = 2^k - 1. Higher k values provide better embedding efficiency (fewer coefficient changes) but require more coefficients per embedded bit chunk. This function selects the largest k that can accommodate the given message plus the 32-bit header.
The selection algorithm:
- Try k from 8 down to 1 (highest efficiency first)
- For each k, calculate capacity: (usableCoeffs / n) * k
- Select the first k where capacity >= messageBits + HeaderSize
- Return error if no k can accommodate the message
The capacity formula accounts for:
- Code word length n = 2^k - 1
- Each code word embeds k bits
- The 32-bit header overhead (HeaderSize = 32)
Parameters:
- usableCoeffCount: Number of non-zero, non-DC coefficients available
- messageBits: Number of bits in the message to embed
Returns:
- k: The optimal encoding parameter (1-8)
- err: ErrInsufficientCapacity if message cannot fit
Example:
// With 10000 usable coefficients and 1000 message bits: k, err := SelectOptimalK(10000, 1000) // Returns k=8 (highest efficiency that fits) // With limited coefficients, a lower k may be selected: k, err = SelectOptimalK(500, 400) // Returns k=1 (only option with enough capacity)
func SelectOptimalKWithShrinkage ¶
SelectOptimalKWithShrinkage is the shrinkage-aware k-selector. It computes the f5.jar effective capacity (P − h(1)) + ⌊0.49·h(1)⌋ and then walks the same iterative k-selection loop f5.jar uses (JpegEncoder.java:337- 349, see selectOptimalKJavaExact), so it returns the SAME k value f5.jar would pick for the same (cover, message) inputs.
Prior to 2026-05-23 this delegated to SelectOptimalK (a "largest k that fits" walk using the standard floor(P/n)*k capacity formula). That gave the WRONG k at certain bucket-boundary covers — empirically a 64×64 cover with 1081 usable coefficients and 1-byte message: f5.jar picks k=6 (its quirky formula returns 0 bits at i=7, forcing break and k = i-1); the old SelectOptimalK picked k=7 because the standard formula gives 56 non-zero bits. The mismatched k produced byte-divergent stego on roughly half of (cover, Q) combinations — see project_f5jar_parity.md and the reproducer at /tmp/mcu_parity_test/.
Use this in place of SelectOptimalK whenever the caller has access to h(1) (the magnitudeOneCount). The non-shrinkage SelectOptimalK is retained for API compatibility but does NOT match f5.jar's k choice.
func SetTranslator ¶
func SetTranslator(t TranslatorProvider)
SetTranslator sets the global translator for i18n error messages. Pass nil to disable translation and use English fallback messages.
This function is safe for concurrent use.
func Translate ¶
Translate returns a translated message for the given key. If no translator is set or the key is not found, returns the default English message. This is the public translation helper function for i18n support.
func ValidateCoefficients ¶
ValidateCoefficients validates that the coefficient slice is valid for embedding. It checks that:
- The slice is not empty
- All coefficient values are within the valid JPEG DCT range [-2048, 2047]
Returns nil if validation passes, or a ValidationError if it fails.
func ValidateInputs ¶
ValidateInputs performs all input validations for an embedding operation. This is a convenience function that combines ValidateCoefficients, ValidatePassword, and ValidateMessage.
Returns the first validation error encountered, or nil if all validations pass.
func ValidateMessage ¶
ValidateMessage validates that the message can be embedded within the given capacity. It checks that:
- The message size does not exceed MaxMessageSize (2^23 - 1 bytes)
- The message fits within the specified capacity in bytes
The capacity parameter should be the maximum bytes that can be embedded, typically obtained from CapacityResult.CapacityByK for the selected k value.
Returns nil if validation passes, or a ValidationError if it fails.
func ValidatePassword ¶
ValidatePassword validates that the password is valid for embedding. It checks that the password is not an empty string.
Returns nil if validation passes, or a ValidationError if it fails.
func XORHeaderWithPRNG ¶
func XORHeaderWithPRNG(header int32, random RandomSource) int32
XORHeaderWithPRNG XORs a header with 4 consecutive PRNG bytes.
The F5 algorithm XORs the header with PRNG bytes to add another layer of password-dependent obfuscation. This must be done in the exact same order as the Java implementation for byte-identical compatibility.
PRNG consumption order (Java-compatible):
- Byte 0: XORed with header bits 0-7
- Byte 1: XORed with header bits 8-15
- Byte 2: XORed with header bits 16-23
- Byte 3: XORed with header bits 24-31
Important: Uses signed byte arithmetic to match Java's behavior. Go's bytes are unsigned [0, 255], but Java's bytes are signed [-128, 127]. The int(int8(byte)) conversion ensures sign extension matches Java.
Parameters:
- header: The 32-bit header value from BuildHeader
- random: A seeded RandomSource (PRNG state after permutation generation)
Returns:
- The XORed header value ready for embedding
Example:
prng := InitializePRNG(password) _, _ = GeneratePermutation(prng, coeffCount) // consumes PRNG for permutation header := BuildHeader(k, len(message)) xoredHeader := XORHeaderWithPRNG(header, prng) // consumes 4 more PRNG bytes
Types ¶
type CapacityResult ¶
type CapacityResult struct {
// CapacityByK maps each k parameter (1-8) to the maximum message capacity
// in bytes when using that k value. Higher k values generally have lower
// capacity but better embedding efficiency.
CapacityByK map[int]int
// EstimatedShrinkageFactor is the estimated proportion of coefficients
// that will cause shrinkage (coefficients with absolute value 1).
// Range: 0.0 to 1.0
EstimatedShrinkageFactor float64
// TotalCoefficients is the total number of coefficients in the input.
TotalCoefficients int
// UsableCoefficients is the count of non-zero, non-DC coefficients.
// Only usable coefficients can carry steganographic data.
UsableCoefficients int
// MagnitudeOneCount is the exact integer count of usable coefficients with
// absolute value 1 (i.e. h(1), the shrinkage-eligible pool). This is the
// raw integer counter the capacity scan computes; the value is exposed so
// callers can use it without round-tripping
// EstimatedShrinkageFactor × UsableCoefficients through float64 (which
// can lose ±1 from the integer count). EstimatedShrinkageFactor is kept
// for API compatibility but is now derived from this field.
MagnitudeOneCount int
}
CapacityResult contains the capacity analysis results for a set of coefficients.
Use this to determine if a message can be embedded before attempting the embedding operation.
func CalculateCapacity ¶
func CalculateCapacity(coefficients []int16) *CapacityResult
CalculateCapacity analyzes a coefficient array and returns capacity information.
This function counts total and usable coefficients, calculates the embedding capacity for each k value (1-8), and estimates the shrinkage factor based on the distribution of coefficient magnitudes.
Usable coefficients are those that can carry steganographic data:
- Non-DC coefficients (index % 64 != 0)
- Non-zero coefficients
The capacity for each k is calculated as:
capacityBits = (usableCount / n) * k - HeaderSize capacityBytes = capacityBits / 8
where n = 2^k - 1 is the code word length.
The estimated shrinkage factor is the proportion of usable coefficients with absolute value 1, which will cause shrinkage when modified.
Parameters:
- coefficients: The JPEG DCT coefficients to analyze
Returns:
- A CapacityResult struct containing:
- TotalCoefficients: Total count of coefficients
- UsableCoefficients: Count of non-zero, non-DC coefficients
- CapacityByK: Map of k values (1-8) to capacity in bytes
- EstimatedShrinkageFactor: Proportion of |1| coefficients (0.0-1.0)
Example:
result := CalculateCapacity(coefficients)
fmt.Printf("Total: %d, Usable: %d\n", result.TotalCoefficients, result.UsableCoefficients)
fmt.Printf("Capacity at k=4: %d bytes\n", result.CapacityByK[4])
fmt.Printf("Expected shrinkage: %.1f%%\n", result.EstimatedShrinkageFactor*100)
type EmbedOption ¶
type EmbedOption func(*EmbedOptions)
EmbedOption is a functional option for configuring embedding behavior. Use these options with EmbedWithOptions or apply them directly to EmbedOptions.
func WithForceK ¶
func WithForceK(k int) EmbedOption
WithForceK returns an EmbedOption that forces a specific k parameter.
The k parameter (1-8) controls the trade-off between embedding efficiency and capacity:
- Higher k: Better efficiency (fewer changes per bit), but less capacity
- Lower k: More capacity, but more changes required
When k is 0 (default), the optimal k is automatically selected based on message size and available coefficient capacity.
Example:
opts := EmbedOptions{}
WithForceK(4)(&opts)
result, err := EmbedWithOptions(coeffs, password, message, opts)
func WithLogger ¶
func WithLogger(log logger.Logger) EmbedOption
WithLogger returns an EmbedOption that sets the logger for debugging and monitoring.
When a logger is provided, the embedding process will log:
- Debug level: k selection, shrinkage events, coefficient changes, capacity analysis
- Info level: embedding start, embedding complete
When nil is passed, no logging is performed (same as default behavior).
Example:
opts := EmbedOptions{}
WithLogger(myLogger)(&opts)
result, err := EmbedWithOptions(coeffs, password, message, opts)
type EmbedOptions ¶
type EmbedOptions struct {
// Logger is an optional logger for debugging and monitoring.
// When nil, no logging is performed.
Logger logger.Logger
// ForceK forces a specific k parameter (1-8) instead of auto-selection.
// When 0, the optimal k is automatically selected based on message size
// and available capacity.
ForceK int
}
EmbedOptions provides optional configuration for the embedding operation.
All fields are optional. Zero values indicate the default behavior:
- Logger: nil (no logging)
- ForceK: 0 (auto-select optimal k)
type EmbedResult ¶
type EmbedResult struct {
// Coefficients is the modified coefficient slice after embedding.
// This is the same slice reference passed to Embed (modified in-place).
Coefficients []int16
// KParameter is the matrix encoding parameter used (1-8).
// Higher k values provide better efficiency but require more coefficients.
KParameter int
// BytesEmbedded is the number of message bytes successfully embedded.
BytesEmbedded int
// ShrinkageCount is the number of times shrinkage occurred during embedding.
// Shrinkage happens when decrementing |1| or |-1| produces 0, requiring
// the bit to be re-embedded using the next available coefficient.
ShrinkageCount int
// UsableCoefficients is the count of non-zero, non-DC coefficients used.
UsableCoefficients int
}
EmbedResult contains the results of an F5 embedding operation.
The Coefficients field contains the modified coefficient slice. Note that embedding modifies coefficients in-place, so this is the same slice reference passed to the Embed function.
func Embed ¶
func Embed(coefficients []int16, password string, message []byte) (*EmbedResult, error)
Embed embeds a secret message into JPEG DCT coefficients using the F5 algorithm.
The F5 algorithm (Westfeld, 2001) provides high embedding efficiency through matrix encoding and distributes changes uniformly via permutative straddling. This implementation produces byte-identical output to the Java reference implementation when using the same password and coefficients.
The embedding process:
- Validate inputs (coefficients, password, message)
- Initialize PRNG with password and generate permutation
- Calculate capacity and auto-select optimal k parameter
- Build and XOR the 32-bit header
- Embed header using simple LSB embedding
- Embed message using matrix encoding with shrinkage handling
Coefficients are modified in-place for memory efficiency. The returned EmbedResult contains the same slice reference along with embedding metadata.
Parameters:
- coefficients: JPEG DCT coefficients to modify (modified in-place)
- password: Password for PRNG seeding (must not be empty)
- message: Message bytes to embed (max 8,388,607 bytes)
Returns:
- *EmbedResult: Embedding results including metadata
- error: Validation or embedding errors
Example:
result, err := Embed(coefficients, "secret password", []byte("hidden message"))
if err != nil {
log.Fatal(err)
}
fmt.Printf("Embedded %d bytes with k=%d\n", result.BytesEmbedded, result.KParameter)
func EmbedWithOptions ¶
func EmbedWithOptions(coefficients []int16, password string, message []byte, opts EmbedOptions) (*EmbedResult, error)
EmbedWithOptions embeds a secret message with additional configuration options.
This function provides the same functionality as Embed but allows:
- Logger: Inject a logger for debugging and monitoring
- ForceK: Override automatic k selection with a specific value (1-8)
When ForceK is 0 (default), the optimal k is automatically selected based on message size and available coefficient capacity, preferring higher k values for better embedding efficiency.
Internally, this function creates a PRNG using InitializePRNG(password) and delegates to EmbedWithRandomSource for the actual embedding. The PRNG is securely cleared after embedding completes.
Parameters:
- coefficients: JPEG DCT coefficients to modify (modified in-place)
- password: Password for PRNG seeding (must not be empty)
- message: Message bytes to embed (max 8,388,607 bytes)
- opts: Configuration options (Logger, ForceK)
Returns:
- *EmbedResult: Embedding results including metadata
- error: Validation or embedding errors
Example:
opts := EmbedOptions{
Logger: myLogger,
ForceK: 4, // Use k=4 instead of auto-selection
}
result, err := EmbedWithOptions(coefficients, password, message, opts)
func EmbedWithRandomSource ¶
func EmbedWithRandomSource(coefficients []int16, random RandomSource, message []byte, opts EmbedOptions) (*EmbedResult, error)
EmbedWithRandomSource embeds a secret message using a pre-seeded RandomSource.
This function provides the core embedding functionality and is designed for callers who want to manage their own PRNG lifecycle, such as when implementing dependency injection patterns or when the same PRNG needs to be used across multiple operations.
The RandomSource must be pre-seeded before calling this function. The function consumes PRNG state in the following order, matching the Java F5 reference implementation exactly:
- GeneratePermutation (consumes PRNG state proportional to coefficient count)
- XORHeaderWithPRNG (consumes 4 PRNG bytes)
- Message XOR during embedding (consumes 1 PRNG byte per message byte)
Unlike EmbedWithOptions, this function does NOT call prng.Clear() after embedding. The caller is responsible for clearing the PRNG when done if security-sensitive state needs to be wiped.
Parameters:
- coefficients: JPEG DCT coefficients to modify (modified in-place)
- random: A pre-seeded RandomSource (must not be nil)
- message: Message bytes to embed (max 8,388,607 bytes)
- opts: Configuration options (Logger, ForceK)
Returns:
- *EmbedResult: Embedding results including metadata
- error: Validation or embedding errors
Example:
prng := InitializePRNG(password)
defer prng.Clear()
result, err := EmbedWithRandomSource(coefficients, prng, message, EmbedOptions{})
PRNG Consumption Order (Java F5 compatible):
The RandomSource state is consumed in this exact order: 1. GeneratePermutation: Uses NextInt() for Fisher-Yates shuffle 2. Header XOR: Uses NextBytes(4) for header obfuscation 3. Message XOR: Uses NextBytes(1) per message byte during embedding
type RandomSource
deprecated
type RandomSource = f5prng.RandomSource
RandomSource is a type alias for f5prng.RandomSource to maintain backward compatibility. New code should use f5prng.RandomSource directly.
Deprecated: Use f5prng.RandomSource directly. This type alias will be removed in a future version.
type TranslatorProvider ¶
type TranslatorProvider interface {
// Translate looks up a translation key in the current locale.
// If the key is not found, it tries the fallback chain.
// Returns the key itself if not found in any locale.
Translate(key string) string
// TranslateWithArgs looks up a translation key and formats it with arguments.
// Uses fmt.Sprintf formatting. If the key is not found, returns the key itself.
TranslateWithArgs(key string, args ...interface{}) string
// HasKey checks if a translation key exists in the current locale or fallback chain.
HasKey(key string) bool
// SetLocale changes the current locale for translation lookups.
// The locale will be normalized before being set.
SetLocale(locale string)
// GetLocale returns the current locale being used for translations.
GetLocale() string
}
TranslatorProvider defines the interface for translation services. This interface allows the f5messageembed package to accept translation capabilities without creating a hard dependency on the full i18n package.
To integrate i18n support, create a Translator using the i18n package and pass it to SetTranslator(). The f5messageembed package will use it for all error messages.
Example Integration ¶
import (
"github.com/0verkilll/f5messageembed"
"github.com/0verkilll/i18n"
)
translator, _ := i18n.New(
i18n.WithFileSystemLoader("locales"),
i18n.WithDefaultLocale("en-US"),
)
f5messageembed.SetTranslator(translator)
When no translator is set, the package falls back to English messages.
func GetTranslator ¶
func GetTranslator() TranslatorProvider
GetTranslator returns the current global translator, or nil if not set. This function is safe for concurrent use.
type ValidationError ¶
type ValidationError struct {
// Key is the i18n key for the error message
Key string
// Message is the translated or fallback error message
Message string
}
ValidationError represents an input validation error with i18n support.
func ValidationErrorf ¶
func ValidationErrorf(key string, args ...interface{}) *ValidationError
ValidationErrorf creates a validation error with a formatted message. This is useful for validation errors that need dynamic content. The key should be an i18n key, and args are passed to fmt.Sprintf after translation if the translator supports TranslateWithArgs.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
Error implements the error interface.