Skip to content

Passwords

The password is the primary credential of a user account, and the one most clients use to sign in. Where it is stored depends on the directory that holds the account.

For accounts of the internal directory, the server keeps the password itself. It stores the password as a hash in the account’s Password credential, and it enforces a password policy whenever a password is set. A hash is the output of a one-way function: the server can check a password against it, but cannot recover the password from it.

For accounts of an LDAP or SQL directory, the directory holds the password. The server verifies it against the directory in one of two ways: by binding to the directory as the user, or by comparing it with the hash the directory returns.

In every case except an LDAP bind, the server compares the password with a stored hash itself. It must therefore understand the format of that hash. This matters most when passwords are imported from another system, because those hashes were produced by other software.

Password hashing and the password policy are configured on the Authentication singleton (found in the WebUI under Settings › Authentication › General).

A password can be set in clear text, either by the user in the Account Manager or by an administrator. The server then hashes it with the algorithm selected in passwordHashAlgorithm: argon2id, bcrypt, scrypt or pbkdf2. Changing the algorithm affects only passwords set afterwards, and existing hashes keep working, so the algorithm can be changed at any time without locking anyone out.

Changes to the Authentication singleton, including the password policy below, take effect after a ReloadSettings action.

A value can also be given that is already a hash. The server stores a well-formed hash as given when it is in one of the $ formats below, or in one of the braced formats other than {PLAIN} and {CLEAR}. Any other value, including a BSDi _ hash, is treated as a clear-text password and hashed.

This behaviour is what makes migrations possible without knowing the users’ passwords. An administrator sets the secret of the Password credential to the hash from the old system, and the user keeps signing in with the same password (Managing accounts).

The server recognises the format of a stored hash from its prefix. The table lists the accepted prefixes and shows which schemes Stalwart can also generate for new passwords:

PrefixSchemeGenerated by Stalwart
$argon2id$, $argon2i$, $argon2d$Argon2argon2id
$2a$, $2b$, $2x$, $2y$bcryptbcrypt ($2b$)
$scrypt$scryptscrypt
$pbkdf2...PBKDF2pbkdf2
$6$, $5$SHA-512 crypt, SHA-256 cryptNo
$sha1$SHA-1 cryptNo
$1$MD5 cryptNo
_BSDi extended DES cryptNo
{SHA}, {SHA256}, {SHA512}Base64-encoded SHA digestNo
{SSHA}, {SSHA256}, {SSHA512}Base64-encoded salted SHA digestNo
{MD5}Base64-encoded MD5 digestNo
{CRYPT}Traditional Unix crypt, or any $ format aboveNo
{ARGON2}, {ARGON2I}, {ARGON2ID}, {PBKDF2}The corresponding PHC string after the prefixNo
{PLAIN}, {CLEAR}Clear textNo

Scheme names in braces are matched without regard to case, so {ssha} and {SSHA} are equivalent.

Two kinds of value are refused at sign-in, and both are logged as auth.error events:

  • A value that starts with $ but matches none of the formats fails with Unrecognized password hash format.
  • A value in braces with an unknown scheme fails with Unsupported password hash algorithm.

A value without any prefix is compared as clear text. That case arises only with external directories that store clear-text passwords, because the server hashes every clear-text password that it stores itself.

The password policy decides which new passwords the server accepts. It applies to passwords set for accounts of the internal directory, both when a user chooses a new password and when an administrator sets one. It does not apply to imported hashes, because the server never sees the password behind them. It also does not apply to passwords verified by an external directory, which enforces its own policy.

The policy makes four decisions.

The first is the permitted length. minPasswordLength and maxPasswordLength bound the length of a new password. On a new installation, a password must be between 8 and 128 characters long. The upper bound exists to protect the hashing function from very long inputs.

The second is how hard a password must be to guess. minPasswordStrength rejects passwords below a strength level computed by the zxcvbn estimator. zxcvbn scores a password by how quickly an attacker would guess it, and takes dictionary words, keyboard patterns and dates into account. The levels are:

  • zero, too guessable. It accepts any password within the length bounds.
  • one and two, increasingly hard to guess.
  • three, safely unguessable. A new installation requires three.
  • four, very unguessable. It also rejects many passwords that users choose deliberately.

Each level rejects more passwords than the one below it. Because the score measures guessability and not character classes, a long passphrase of common words passes, while a short password padded with symbols does not.

The third is how long a password stays valid. passwordDefaultExpiry gives every new password an expiry date that far in the future. While the field is unset, passwords do not expire. Once a password has expired, the account is restricted until the password is changed:

  • The user can still sign in to the WebUI, but only to change the password.
  • The account keeps receiving mail.
  • Every other permission is withheld.
  • Sign-in to third-party OAuth applications is refused.

App passwords and API keys have their own expiry dates, so an expired account password does not affect them.

The fourth is how many secondary credentials an account may hold. maxAppPasswords and maxApiKeys set how many app passwords and API keys an account may have. A new installation allows 5 app passwords and 5 API keys per account. An account’s own maxAppPasswords and maxApiKeys quotas take precedence over these values (Quotas).

Some common policy rules are not available. The internal directory offers no password history, no reuse prevention and no character-class rules. Deployments that need them delegate authentication to an identity provider, through an OpenID Connect or LDAP directory, and enforce the policy there.