Relatude Help

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 written

PasswordValue and PasswordStorage are in Relatude.DB.Common. What to know:

  • The store does the work on write. SetPassword and Clear only 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 a ValueConstraintException that 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.
  • NeedsRehash is 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 successful Verify at 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.