Skip to main content

Password

The password package provides pluggable password encoding with a composite encoder that supports multiple algorithms simultaneously.

Encoder Interface

Every algorithm-specific encoder and the composite encoder implement the same Encoder interface:

type Encoder interface {
Encode(password string) (string, error)
Matches(password, encodedPassword string) bool
UpgradeEncoding(encodedPassword string) bool
}

Composite Encoder

The composite encoder stores the algorithm identifier as a prefix in the encoded password, enabling seamless algorithm migration:

import "github.com/coldsmirk/vef-framework-go/password"

encoder := password.NewCompositeEncoder(
password.EncoderBcrypt, // default for new passwords
map[password.EncoderID]password.Encoder{
password.EncoderBcrypt: password.NewBcryptEncoder(),
password.EncoderArgon2: password.NewArgon2Encoder(),
password.EncoderScrypt: password.NewScryptEncoder(),
password.EncoderSha256: password.NewSha256Encoder(),
},
)

Encoding

encoded, err := encoder.Encode("my-password")
// → "{bcrypt}$2a$10$..."

Matching

The encoder automatically detects the algorithm from the {prefix}:

// Matches against any supported algorithm
ok := encoder.Matches("my-password", "{bcrypt}$2a$10$...") // true
ok = encoder.Matches("my-password", "{argon2}$argon2id$...") // true
ok = encoder.Matches("my-password", "{sha256}abc123...") // true

If an encoded password has no {prefix}, CompositeEncoder falls back to the default encoder ID for matching and upgrade checks. Unknown prefixes return false from Matches.

Upgrade Detection

Check if a password needs re-encoding (e.g., when migrating from SHA256 to bcrypt):

needsUpgrade := encoder.UpgradeEncoding("{sha256}abc123...")
// → true (because default is bcrypt, not sha256)

UpgradeEncoding returns true immediately for any valid non-default prefix. For the default prefix or a no-prefix value, it delegates to the default encoder's own UpgradeEncoding logic, such as bcrypt cost comparison.

Available Encoders

Encoder IDConstructorSecurity Level
password.EncoderBcryptpassword.NewBcryptEncoder()⭐⭐⭐⭐ Recommended
password.EncoderArgon2password.NewArgon2Encoder()⭐⭐⭐⭐⭐ Strongest
password.EncoderScryptpassword.NewScryptEncoder()⭐⭐⭐⭐ Strong
password.EncoderPbkdf2password.NewPbkdf2Encoder()⭐⭐⭐ Standard (FIPS-friendly)
password.EncoderSha256password.NewSha256Encoder()⭐⭐ Legacy only
password.EncoderMd5password.NewMd5Encoder()⭐ Legacy / interop only
password.EncoderPlaintextpassword.NewPlaintextEncoder()⭐ Testing only

The public encoder ID constants are EncoderBcrypt, EncoderArgon2, EncoderScrypt, EncoderPbkdf2, EncoderMd5, EncoderSha256, and EncoderPlaintext.

The {prefix} segment is derived from the EncoderID constant value: EncoderBcrypt{bcrypt}, EncoderArgon2{argon2}, EncoderScrypt{scrypt}, EncoderPbkdf2{pbkdf2}, EncoderMd5{md5}, EncoderSha256{sha256}, and EncoderPlaintext{plaintext}.

Encoder Options

EncoderOption functions
NewBcryptEncoderWithBcryptCost(cost)
NewArgon2EncoderWithArgon2Memory(memory), WithArgon2Iterations(iterations), WithArgon2Parallelism(parallelism)
NewScryptEncoderWithScryptN(n), WithScryptR(r), WithScryptP(p)
NewPbkdf2EncoderWithPbkdf2Iterations(iterations), WithPbkdf2HashFunction(hashFunction)
NewMd5EncoderWithMd5Salt(salt), WithMd5SaltPosition(position)
NewSha256EncoderWithSha256Salt(salt), WithSha256SaltPosition(position)

The corresponding option types are exported as BcryptOption, Argon2Option, ScryptOption, Pbkdf2Option, Md5Option, and Sha256Option.

The exported option functions are WithBcryptCost, WithArgon2Memory, WithArgon2Iterations, WithArgon2Parallelism, WithScryptN, WithScryptR, WithScryptP, WithPbkdf2Iterations, WithPbkdf2HashFunction, WithMd5Salt, WithMd5SaltPosition, WithSha256Salt, and WithSha256SaltPosition.

Default parameters:

EncoderDefaults
bcryptbcrypt.DefaultCost (10); valid cost range is 4..31
Argon2idmemory 64 * 1024 KiB, iterations 3, parallelism 4
scryptN = 32768, r = 8, p = 1
PBKDF2310000 iterations with sha256; sha512 is also supported
MD5 / SHA-256no salt by default; with salt, default salt position is suffix and prefix is available

Encrypted Password Transport

password.Encoder stays a plain KDF used for both storage and comparison — there is no cipher-wrapping encoder. If the client encrypts the password before sending it (a common "front-end encrypts, backend verifies" pattern), decryption happens one layer up, at the authenticator: register a security.PasswordDecryptor and PasswordAuthenticator decrypts the transmitted credential to plaintext before verifying it against the stored hash. See Login Hardening for the full setup.

Password Format

Encoded passwords follow the format: {algorithm}encoded_value

{bcrypt}$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
{argon2}$argon2id$v=19$m=65536,t=3,p=4$c29tZXNhbHQ$...
{sha256}5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8

Standalone MD5 and SHA-256 encoders without salt return the raw hex digest. When configured with salt, they use the inner hash format {algorithm}$salt$hash; when wrapped by NewCompositeEncoder, the composite adds the outer {algorithm} prefix before that encoded value.

Error Sentinels

ErrorWhen it appears
ErrInvalidCostbcrypt cost is outside 4..31
ErrInvalidMemoryArgon2 memory is too small
ErrInvalidIterationsiteration count is lower than 1
ErrInvalidParallelismArgon2 parallelism is lower than 1
ErrInvalidEncoderIDCompositeEncoder cannot find an encoder for the prefix
ErrInvalidHashFormatencoded password format is malformed
ErrDefaultEncoderNotFounddefault encoder ID is not registered in CompositeEncoder