Passwords
A PasswordValue property holds a password. By default the store keeps it as an Argon2id hash,
which can be verified but never read back. A property can also allow a password encrypted with the
database's key (AES-256-GCM), which can be read back, or kept as plain text. The code chooses how to
keep a password when it sets one:
public class Member {
public Guid Id { get; set; }
public string Email { get; set; } = "";
[PasswordProperty(MinLength = 10, RequireDigit = true)]
public PasswordValue Password { get; set; } = PasswordValue.Empty;
[PasswordProperty(AllowEncrypted = true, AllowHashed = false)]
public PasswordValue SmtpPassword { get; set; } = PasswordValue.Empty;
}
member.Password.SetPassword("correct horse battery 7"); // hashed, the default
member.SmtpPassword.SetPassword(secret, PasswordStorage.Encrypted);
db.Update(member); // hashed and encrypted here
var m = db.Get<Member>(id);
m.Password.IsSet; // true
m.Password.Storage; // PasswordStorage.Hashed
m.Password.Verify(typed); // true or false
m.Password.IsPasswordRetrievable; // false: a hash cannot be read back
m.SmtpPassword.GetPassword(); // the password, decrypted with the database's key
m.Password.Clear(); // removed when the node is next writtenPasswordValue and PasswordStorage are in Relatude.DB.Common. What to know:
- The store does the work on write.
SetPasswordandClearonly record the change. When the node is written, the store checks the property's rules and makes the hash or the encryption. A refused password fails the write with aValueConstraintExceptionthat never quotes the password. - The password never leaves the store. Only the stored form reaches the log. JSON gives
{"isSet": true, "storage": "Hashed"}; send{"set": "...", "storage": "Hashed"}or{"clear": true}to change it. A value that holds no change, like a stale object saved again, leaves the stored password as it is. - A query can only ask
IsSet:Where(m => m.Password.IsSet). Comparing, sorting, grouping or selecting a password throws. A password is never in an index, the text index, the display name, GraphQL or a facet. NeedsRehashis true when a password was kept with other hash settings, pepper or key than the store's current ones. Set the password again after a successfulVerifyat sign-in.
The keys are database settings, under LocalSettings.Passwords in relatude.db.json (the
Passwords group on the admin UI's Content settings page):
"Passwords": {
"EncryptionKey": "<32 random bytes in base64>",
"OldEncryptionKeys": [ "<a previous key, still used to decrypt>" ],
"Pepper": null,
"HashMemoryKiB": 19456, "HashIterations": 2, "HashParallelism": 1
}Keep the keys in configuration or user secrets, which the server never writes to its settings files.
Without EncryptionKey, no password can be kept encrypted. Losing it loses every encrypted password,
so keep a copy, and when you change it, move the old key into OldEncryptionKeys. The Pepper is an
optional secret mixed into every hash. Set it once, before the first password: a hash made with
another pepper cannot be verified. The hash settings default to OWASP's minimum for Argon2id, and
each hash keeps the settings it was made with.
The property's options, in the attribute and the datamodel editor: AllowHashed (on),
AllowEncrypted and AllowPlainText (off), MinLength (8), MaxLength (128), RequireUppercase,
RequireLowercase, RequireDigit, RequireSymbol, and RegularExpression with
RegularExpressionMessage. A password property cannot be indexed, unique, part of the display name
or per culture, or sit on a type kept inside another node's embedded property. A remote store cannot
set a password: only the store that keeps the node has the keys.