Documentation
¶
Overview ¶
Package f5prng provides unified PRNG interfaces for F5 steganography packages.
This package serves as the single source of truth for PRNG interfaces across the F5 steganography ecosystem, eliminating interface fragmentation and the need for adapter types in consuming packages.
The interfaces are designed to match Java's SecureRandom behavior for compatibility with PixelKnot's F5 steganography algorithm.
Quick Start ¶
factory := f5prng.NewDefaultFactory()
prng := factory.NewPRNG()
defer prng.Clear()
if err := prng.Seed([]byte("password")); err != nil {
// handle error
}
randomBytes := prng.NextBytes(20)
randomInt := prng.NextInt()
boundedInt, _ := f5prng.NextIntN(prng, 100)
Security Warning ¶
The implementations in this package are NOT cryptographically secure. Do not use for cryptographic key generation, session tokens, or other security-critical purposes. Use crypto/rand for secure randomness.
Java Compatibility ¶
This package produces byte-identical output to Java's SecureRandom (SHA1PRNG) for compatibility with:
- PixelKnot Android app
- F5.jar reference implementation
- Other Java F5 steganography tools
Index ¶
- Variables
- func GetLogger() logger.Logger
- func GetSupportedLocales() []string
- func NextIntN(rs RandomSource, n int) (int, error)
- func SetLogger(l logger.Logger)
- func SetTranslator(translator TranslatorProvider)
- type DefaultFactory
- type Error
- type ErrorCode
- type Hasher
- type HasherWithSumInto
- type PRNGFactory
- type RandomSource
- type RandomSourceWithBytesInto
- type SHA1Hasher
- type SecureRandom
- type TranslatorProvider
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidBound is returned when NextIntN is called with n <= 0. ErrInvalidBound = Error{Code: ErrCodeInvalidBound} // ErrSeedHashFailed is returned when the hash computation fails during Seed(). ErrSeedHashFailed = Error{Code: ErrCodeSeedHashFailed} // ErrStateUpdateFailed is returned when the hash computation fails during state update. ErrStateUpdateFailed = Error{Code: ErrCodeStateUpdateFailed} // ErrNilHasher is returned when a nil hasher is provided to NewSecureRandom. ErrNilHasher = Error{Code: ErrCodeNilHasher} )
Sentinel errors for common error conditions. These can be used with errors.Is() for error checking.
Functions ¶
func GetSupportedLocales ¶
func GetSupportedLocales() []string
GetSupportedLocales returns the list of locales supported by this package.
It reads the list of embedded locale files and returns their locale codes. The returned array is sorted alphabetically for consistency.
If the embedded filesystem cannot be read (which should never happen in normal operation), this function returns a fallback array containing only "en-US" to ensure graceful degradation.
func NextIntN ¶
func NextIntN(rs RandomSource, n int) (int, error)
NextIntN returns a bounded random integer in the range [0, n). This is a helper function that converts an unbounded NextInt() to a bounded value.
The function properly handles modulo bias to ensure uniform distribution across the range [0, n).
Parameters:
- rs: The RandomSource to use for generating random integers
- n: The exclusive upper bound (must be > 0)
Returns:
- A random integer in the range [0, n), or an error if n <= 0
Example:
prng := factory.NewPRNG()
defer prng.Clear()
if err := prng.Seed([]byte("password")); err != nil {
// handle error
}
randomIndex, err := NextIntN(prng, 100) // Returns 0-99
if err != nil {
// handle error
}
func SetLogger ¶
SetLogger sets the logger for the f5prng package. Pass nil to disable logging and reset to the default NopLogger. The logger is shared across all goroutines and is thread-safe.
func SetTranslator ¶
func SetTranslator(translator TranslatorProvider)
SetTranslator sets the global translator for this package. Pass nil to disable translations and use default English messages. The translator is shared across all goroutines and is thread-safe.
Types ¶
type DefaultFactory ¶
type DefaultFactory struct{}
DefaultFactory is the default PRNGFactory implementation that creates SecureRandom instances using SHA-1 hashing for Java compatibility.
This factory creates instances that are compatible with Java's SecureRandom("SHA1PRNG") implementation, making it suitable for F5 steganography operations.
func (*DefaultFactory) NewPRNG ¶
func (f *DefaultFactory) NewPRNG() RandomSource
NewPRNG creates and returns a new SecureRandom instance. The returned instance uses SHA-1 hashing and is compatible with Java's SecureRandom("SHA1PRNG").
The returned RandomSource must be seeded before use.
Example:
factory := NewDefaultFactory()
prng := factory.NewPRNG()
defer prng.Clear()
if err := prng.Seed([]byte("password")); err != nil {
// handle error
}
randomInt := prng.NextInt()
type Error ¶
Error represents a f5prng error with a code for i18n support. The Code field allows callers to map errors to localized messages.
Example:
if err != nil {
var e f5prng.Error
if errors.As(err, &e) {
switch e.Code {
case f5prng.ErrCodeInvalidBound:
// Handle invalid bound error
case f5prng.ErrCodeSeedHashFailed:
// Handle seed hash failure
}
}
}
type ErrorCode ¶
type ErrorCode int
ErrorCode represents an error code for i18n support. Error codes enable localized error messages and programmatic error handling.
const ( // ErrCodeInvalidBound indicates n <= 0 was passed to NextIntN. ErrCodeInvalidBound ErrorCode = iota + 1 // ErrCodeSeedHashFailed indicates the hash computation failed during Seed(). // This is extremely rare and would require a seed exceeding 2^61-1 bytes. ErrCodeSeedHashFailed // ErrCodeStateUpdateFailed indicates the hash computation failed during state update. // This should never happen with proper hasher implementations. ErrCodeStateUpdateFailed // ErrCodeNilHasher indicates a nil hasher was provided to NewSecureRandom. ErrCodeNilHasher )
type Hasher ¶
type Hasher interface {
// Sum computes and returns the hash of the provided data.
// The returned slice is the final hash value (e.g., 20 bytes for SHA-1).
// Multiple calls to Sum with the same data must return identical results.
//
// Parameters:
// data - The input bytes to hash
//
// Returns:
// hash - The computed hash as a byte slice
// error - Error if hash computation fails, nil otherwise
Sum(data []byte) ([]byte, error)
// Reset clears the internal state of the hasher, allowing it to be reused
// for a new hash computation. This is more efficient than creating a new
// Hasher instance for each operation.
Reset()
// BlockSize returns the hash's underlying block size in bytes.
// For SHA-1, this is 64 bytes. For SHA-256, this is also 64 bytes.
//
// This is useful for certain cryptographic operations that need to know
// the block size, such as HMAC implementations.
//
// Returns:
// The block size in bytes
BlockSize() int
// Size returns the hash's output size in bytes.
// For SHA-1, this is 20 bytes. For SHA-256, this is 32 bytes.
//
// This allows generic code to work with different hash algorithms
// without hardcoding the output size.
//
// Returns:
// The hash output size in bytes
Size() int
}
Hasher defines the interface for cryptographic hash functions. This abstraction follows the Single Responsibility Principle by focusing solely on hashing operations, and the Dependency Inversion Principle by allowing different hash implementations (SHA-1, SHA-256, etc.) to be used interchangeably.
For F5 steganography compatibility, use a SHA-1 hasher implementation.
Implementations must be stateful and maintain internal hash state between calls. The Reset method allows reuse of the same instance for multiple hash operations.
func NewDefaultHasher ¶
func NewDefaultHasher() Hasher
NewDefaultHasher creates a new SHA-1 hasher for use with SecureRandom. This is a convenience function equivalent to NewSHA1Hasher().
func NewSHA1Hasher ¶
func NewSHA1Hasher() Hasher
NewSHA1Hasher creates a new SHA-1 hasher suitable for use with SecureRandom.
Example:
hasher := NewSHA1Hasher() sr := NewSecureRandom(hasher)
type HasherWithSumInto ¶
type HasherWithSumInto interface {
// SumInto computes the hash of data and writes it into dst.
// dst must be at least 20 bytes for SHA-1.
SumInto(dst []byte, data []byte) error
}
SecureRandom implements Java's SecureRandom with SHA1PRNG algorithm. This is a pure Go implementation that produces identical output to java.security.SecureRandom when initialized with the same seed.
The algorithm uses SHA-1 hashing to generate pseudorandom bytes and maintains an internal state that gets updated with each generation.
THREAD SAFETY: SecureRandom is NOT thread-safe. Do not share instances between goroutines. Either:
- Create separate instances per goroutine, or
- Use external synchronization (sync.Mutex)
SECURITY WARNING: This implementation is NOT cryptographically secure. SHA-1 is broken and should not be used for security purposes. Use crypto/rand for secure random generation. This implementation is suitable for:
- F5 steganography decoding (PixelKnot, F5.jar)
- Legacy Java application compatibility
- Deterministic PRNG for testing/replay
- Educational and research purposes
MEMORY PROTECTION: Always call Clear() when done to zero sensitive data:
sr := NewSecureRandom(hasher)
defer sr.Clear()
if err := sr.Seed([]byte(password)); err != nil {
// handle error
}
// ... use sr
HasherWithSumInto is an optional extension of Hasher that supports zero-allocation hashing by writing the output into a caller-provided buffer.
When a Hasher implements this interface, SecureRandom's updateState method uses SumInto instead of Sum, eliminating a 20-byte allocation per hash call. In brute-force scenarios (100K+ PRNG calls per candidate), this reduces GC pressure by eliminating ~20K allocations per candidate.
type PRNGFactory ¶
type PRNGFactory interface {
// NewPRNG creates and returns a new RandomSource instance.
// The returned instance is uninitialized and must be seeded before use.
//
// Each call creates a new, independent RandomSource. Multiple instances
// can be used concurrently (though individual instances are not thread-safe).
//
// Returns:
// A new RandomSource ready to be seeded
NewPRNG() RandomSource
}
PRNGFactory defines the interface for creating RandomSource instances. This factory pattern enables dependency injection and allows different PRNG implementations to be plugged in without changing consuming code.
Example usage:
factory := NewDefaultFactory()
prng := factory.NewPRNG()
defer prng.Clear()
if err := prng.Seed([]byte("password")); err != nil {
// handle error
}
randomBytes := prng.NextBytes(20)
func NewDefaultFactory ¶
func NewDefaultFactory() PRNGFactory
NewDefaultFactory creates a new DefaultFactory instance.
Example:
factory := NewDefaultFactory()
prng := factory.NewPRNG()
defer prng.Clear()
if err := prng.Seed([]byte("password")); err != nil {
// handle error
}
bytes := prng.NextBytes(20)
type RandomSource ¶
type RandomSource interface {
// Seed initializes or re-initializes the random source with the given seed.
// For deterministic PRNGs, the same seed must produce the same sequence
// of random values across all calls.
//
// Parameters:
// seed - The seed bytes for initializing the random state
//
// Returns:
// error - Error if seeding fails (e.g., hash computation error), nil otherwise
Seed(seed []byte) error
// NextBytes generates and returns n random bytes.
// The bytes are generated from the internal PRNG state and advance
// the state for subsequent calls.
//
// This method allocates a new slice for each call, matching Java's behavior.
// For high-performance scenarios where allocation matters, consider using
// a buffer-based approach in your implementation.
//
// Parameters:
// n - The number of random bytes to generate (must be >= 0)
//
// Returns:
// A newly allocated slice containing n pseudo-random bytes.
// Returns an empty slice if n <= 0.
NextBytes(n int) []byte
// NextInt generates and returns the next random 32-bit integer.
// For Java compatibility, this returns a signed int32 value
// in the range [-2147483648, 2147483647].
//
// This is an unbounded random int32, matching Java's SecureRandom.nextInt().
// For bounded random integers, use the NextIntN helper function.
//
// Returns:
// A pseudo-random int32 value
NextInt() int32
// Clear securely zeros all internal state to prevent memory disclosure.
// This method MUST be called when done using the RandomSource, especially
// if seeded with sensitive data (passwords, keys, etc.).
//
// After calling Clear(), the instance is no longer usable and must be
// reseeded before generating new random data.
//
// Multiple calls to Clear() are safe (idempotent).
//
// Best practice: Use defer to ensure Clear() is called:
// rs := factory.NewPRNG()
// defer rs.Clear()
// rs.Seed(sensitiveData)
// // ... use rs
Clear()
}
RandomSource defines the interface for pseudo-random number generation. This abstraction enables different PRNG implementations while maintaining the Interface Segregation Principle by providing only essential methods.
The interface is designed to match Java's SecureRandom behavior for compatibility with F5 steganography's permutation and embedding algorithms.
Method Signatures:
- NextBytes allocates and returns a new slice (matches Java behavior)
- NextInt returns an unbounded int32 (matches Java SecureRandom.nextInt())
SECURITY NOTE: Implementations of this interface are NOT cryptographically secure. Do not use for cryptographic key generation, session tokens, or other security-critical purposes. Use crypto/rand for secure randomness.
func NewSecureRandom ¶
func NewSecureRandom(hasher Hasher) RandomSource
NewSecureRandom creates a new SecureRandom instance with the provided hasher. The hasher should implement SHA-1 for Java compatibility.
The returned instance implements the RandomSource interface.
Example:
hasher := sha1.NewSHA1(sha1.NewBigEndian())
sr := NewSecureRandom(hasher)
defer sr.Clear()
if err := sr.Seed([]byte("password")); err != nil {
// handle error
}
bytes := sr.NextBytes(20)
type RandomSourceWithBytesInto ¶
type RandomSourceWithBytesInto interface {
// NextBytesInto fills dst with pseudo-random bytes. Returns nil on success,
// or the underlying error if the PRNG is not seeded or the hasher fails.
NextBytesInto(dst []byte) error
}
RandomSourceWithBytesInto is an optional extension of RandomSource that supports zero-allocation byte generation by writing into a caller-provided buffer. Consumers that extract bytes in tight loops (e.g. per-message-byte XOR masks in F5 extraction) should type-assert for this interface and use NextBytesInto when available, falling back to NextBytes otherwise.
The byte stream produced by NextBytesInto(buf) is identical to the stream produced by NextBytes(len(buf)), so callers can mix the two freely.
type SHA1Hasher ¶
type SHA1Hasher struct {
// contains filtered or unexported fields
}
SHA1Hasher wraps the sha1 package to implement the Hasher interface. This provides SHA-1 hashing compatible with Java's SHA1PRNG algorithm.
The wrapper holds the concrete *sha1.SHA1 so it can expose SumInto directly and satisfy HasherWithSumInto, avoiding a 20-byte allocation per hash call on SecureRandom's state-update hot path.
func (*SHA1Hasher) BlockSize ¶
func (h *SHA1Hasher) BlockSize() int
BlockSize returns SHA-1's block size (64 bytes).
func (*SHA1Hasher) Reset ¶
func (h *SHA1Hasher) Reset()
Reset clears the internal state of the hasher.
func (*SHA1Hasher) Size ¶
func (h *SHA1Hasher) Size() int
Size returns SHA-1's output size (20 bytes).
func (*SHA1Hasher) Sum ¶
func (h *SHA1Hasher) Sum(data []byte) ([]byte, error)
Sum computes and returns the SHA-1 hash of the provided data.
func (*SHA1Hasher) SumInto ¶
func (h *SHA1Hasher) SumInto(dst, data []byte) error
SumInto computes the SHA-1 hash of data and writes it into dst. dst must be at least 20 bytes. This is the zero-allocation path used by SecureRandom in hot loops (e.g. brute-force key recovery) where the per-call Sum allocation dominates GC pressure.
type SecureRandom ¶
type SecureRandom struct {
// contains filtered or unexported fields
}
func (*SecureRandom) Clear ¶
func (sr *SecureRandom) Clear()
Clear securely zeros all internal state to prevent memory disclosure attacks. This method MUST be called when done using SecureRandom, especially if seeded with sensitive data (passwords, keys, secrets, etc.).
Clear() performs the following operations:
- Zeros the 20-byte state array
- Zeros the 20-byte remainder buffer
- Zeros the 4-byte intBuf array
- Zeros the 20-byte outputBuf array (may hold the last hash digest)
- Resets remCount to 0
- Clears any stored error
- Resets the wrapped hasher so its internal block buffer does not retain residual state from the last Sum / SumInto call
- Hints to the garbage collector for immediate memory reclamation
After calling Clear(), the SecureRandom instance is no longer usable and must be reseeded with Seed() before generating new random data.
Security Best Practice:
sr := NewSecureRandom(hasher) defer sr.Clear() // Ensure cleanup even if panic occurs sr.Seed([]byte(sensitivePassword)) // ... use sr // Clear() called automatically on function exit
Multiple calls to Clear() are safe (idempotent - subsequent calls are no-ops).
Thread Safety: This method is NOT thread-safe. Do not call Clear() concurrently with other SecureRandom methods on the same instance.
func (*SecureRandom) LastError ¶
func (sr *SecureRandom) LastError() error
LastError returns the last error that occurred during PRNG operations.
This method MUST be checked after batched NextBytes / NextBytesInto / NextInt calls: those methods do not return errors directly (to preserve the Java-compatible signatures), so any error encountered during the batch is only surfaced here. A typical pattern is:
for i := 0; i < n; i++ {
vals[i] = rs.NextInt()
}
if err := rs.LastError(); err != nil {
// handle error — some or all of vals[] may be zero / partial
}
Returns nil if no error has occurred since the last successful Seed() call.
func (*SecureRandom) NextBytes ¶
func (sr *SecureRandom) NextBytes(n int) []byte
NextBytes returns n pseudorandom bytes. This implements Java's engineNextBytes method.
The returned slice is newly allocated for each call, matching Java's behavior. Returns an empty slice if n <= 0.
If an internal error occurs (extremely rare), returns an empty slice and stores the error in lastErr (accessible via LastError method).
func (*SecureRandom) NextBytesInto ¶
func (sr *SecureRandom) NextBytesInto(dst []byte) error
NextBytesInto fills dst with pseudo-random bytes without allocating a new slice. The byte stream is identical to NextBytes(len(dst)) — i.e. consumers that switch between NextBytes and NextBytesInto see the same Java-compatible output. Returns nil on success, or the underlying error if the PRNG is not seeded or the hasher fails. On error, dst may be partially written.
This is a zero-allocation hot-path variant used by consumers that extract individual bytes in inner loops (e.g. f5messageextract.nextSignedByte).
func (*SecureRandom) NextInt ¶
func (sr *SecureRandom) NextInt() int32
NextInt returns a pseudorandom int32. This implements Java's nextInt() method which returns a 32-bit signed integer.
IMPORTANT: This gets 4 bytes using an internal buffer to avoid allocations, while maintaining the exact byte consumption order and signed integer behavior used by Java's SecureRandom and the F5 algorithm.
The byte order matches GetNextValue() from the working implementation:
byte0 | (byte1 << 8) | (byte2 << 16) | (byte3 << 24)
Why sign-extended int32(int8(b)) is load-bearing ¶
At first glance the `int(int8(b))` casts below look like a sign-extension bug waiting to happen — the obvious "clean" rewrite is an unsigned assembly:
u := uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 return int32(u)
That rewrite produces DIFFERENT output when byte2 has its high bit set. Reason: in Go, `int(-1) << 16` is `0xFFFFFFFFFFFF0000` (64-bit, sign extended). OR-ing that into the 32-bit result bleeds 1-bits into bit positions 24..31, which should be owned exclusively by byte3. The unsigned path does not do this, so its output diverges from what Java F5 / PixelKnot produced when the reference stream was captured.
The golden test TestPRNGOutputIsStable_v1 in determinism_test.go locks the current (sign-extending) behaviour, and TestJavaCompatibility_KnownVectors pins the NextBytes stream. Both must pass; altering the byte assembly without a deliberate spec change will break decode compatibility with every PixelKnot / F5.jar artifact ever produced against this library.
If an internal error occurs (extremely rare), returns 0 and stores the error in lastErr (accessible via LastError method).
func (*SecureRandom) Seed ¶
func (sr *SecureRandom) Seed(seed []byte) error
Seed initializes the random number generator with the provided seed. This implements Java's engineSetSeed method.
For deterministic output, the same seed must always produce the same sequence of random values.
Returns an error if the hasher is nil or if the hash computation fails.
type TranslatorProvider ¶
type TranslatorProvider interface {
// Translate returns the translated string for the given key.
Translate(key string) string
// TranslateWithArgs returns the translated string with format arguments.
TranslateWithArgs(key string, args ...interface{}) string
// HasKey returns true if the translation key exists.
HasKey(key string) bool
// SetLocale changes the current locale.
SetLocale(locale string)
// GetLocale returns the current locale.
GetLocale() string
}
TranslatorProvider defines the interface for translation providers. This allows loose coupling - the package works with or without i18n. The interface matches github.com/0verkilll/i18n.Translator.
func GetTranslator ¶
func GetTranslator() TranslatorProvider
GetTranslator returns the global translator, or nil if not set.
func NewTranslator ¶
func NewTranslator(locale string) (TranslatorProvider, error)
NewTranslator creates a new translator configured for the f5prng package. If locale is empty, it will auto-detect from environment variables. The translator uses embedded locale files and the i18n package for translation.