f5prng

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Jun 30, 2026 License: MIT Imports: 10 Imported by: 0

README

f5prng

Pure-Go, password-seeded PRNG for F5 steganography, compatible with Java's SHA1PRNG. It is deterministic and not cryptographically secure — use crypto/rand when you need real randomness.

Install

go get github.com/0verkilll/f5prng

Sponsor

If this project is useful to you, please consider supporting its development:

Sponsor @0verkilll on GitHub

License

MIT

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

Constants

This section is empty.

Variables

View Source
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 GetLogger

func GetLogger() logger.Logger

GetLogger returns the package logger, or NopLogger if not set.

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

func SetLogger(l logger.Logger)

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

type Error struct {
	Code    ErrorCode
	Message string
}

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
        }
    }
}

func (Error) Error

func (e Error) Error() string

Error implements the error interface. If a translator is set via SetTranslator(), returns the localized message. Otherwise, returns the default English message.

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:

  1. Create separate instances per goroutine, or
  2. 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.

Jump to

Keyboard shortcuts

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