Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Identity and access

Explanation

Second factor and secret storage

The operator console second factor in core/totp.py and how core/secret_box.py and core/crypto_at_rest.py protect secrets, files, and backups at rest.

Cluster ERP has three small cryptographic helpers in src/cluster_erp/core/. core/totp.py computes time-based one-time passwords for the operator console. core/secret_box.py seals small control-plane secrets that must be read back, such as the operators' authenticator seeds. core/crypto_at_rest.py encrypts stored files and database dumps. This page states what each one protects and which setting supplies its key. It never shows key values; see the settings reference for the variable names.

In short

  • The second factor is a standard time-based one-time password with six digits and 30-second steps, verified with a tolerance of one step on each side (src/cluster_erp/core/totp.py:1-4, src/cluster_erp/core/totp.py:16-17, src/cluster_erp/core/totp.py:42-55).
  • In the code at this commit, verify_totp is called only by the operator sign-in in server/operators.py (src/cluster_erp/server/operators.py:129-168).
  • Operator seeds are stored sealed with the enc: prefix; the key comes from control_encryption_key, or from secret_key when that setting is empty (src/cluster_erp/core/secret_box.py:1-9, src/cluster_erp/core/secret_box.py:33-37).
  • Files and backups use authenticated encryption with a 256-bit key given as 64 hexadecimal characters (src/cluster_erp/core/crypto_at_rest.py:1-16, src/cluster_erp/core/crypto_at_rest.py:34-40).
  • In multi-tenant mode the settings validator refuses to start without storage_encryption_key, backup_encryption_key, and mail_encryption_key (src/cluster_erp/config/settings.py:805-821).

How it works

The operator second factor

generate_secret creates a random 160-bit seed in base32 (src/cluster_erp/core/totp.py:20-22). otpauth_uri builds the enrolment link an authenticator app reads (src/cluster_erp/core/totp.py:58-63). verify_totp keeps only digits, requires six of them, and compares in constant time against the current step and its neighbours (src/cluster_erp/core/totp.py:42-55).

The operator account flow is in server/operators.py:

  • enroll_totp issues or replaces a seed, stores it sealed, stamps totp_enrolled_at, and returns the seed and link once (src/cluster_erp/server/operators.py:178-195). The command python -m cluster_erp.server.operators totp-enroll runs it (src/cluster_erp/server/operators.py:352-353).
  • authenticate checks the password first and the code second. A wrong code counts as a failed attempt. When vendor_require_totp is on, an operator without a seed cannot sign in (src/cluster_erp/server/operators.py:114-175, src/cluster_erp/config/settings.py:131).
  • A seed stored before sealing existed is sealed at its next successful use (src/cluster_erp/server/operators.py:165-167).

The full operator sign-in is described in operator console.

Sealed control-plane secrets

secret_box uses Fernet from the cryptography package. The Fernet key is the SHA-256 digest of the configured material (src/cluster_erp/core/secret_box.py:27-37). seal returns enc: plus the token, and open_secret returns the plain value of a sealed or legacy unsealed entry (src/cluster_erp/core/secret_box.py:40-61). A changed key makes open_secret raise ValueError (src/cluster_erp/core/secret_box.py:58-61). The column control_operators.totp_secret is wide enough for the sealed form (src/cluster_erp/core/tenancy.py:147-148).

The mail module seals its own provider secrets with a separate helper keyed by mail_encryption_key (src/cluster_erp/modules/mail/crypto.py:1-10). The mail module page covers it.

Files and backups at rest

crypto_at_rest defines two containers (src/cluster_erp/core/crypto_at_rest.py:1-16):

Container Use Key setting Source
Blob One stored attachment, encrypted in memory with a fresh random nonce storage_encryption_key src/cluster_erp/core/crypto_at_rest.py:50-66, src/cluster_erp/core/storage.py:138-166
Stream A database dump of any size, encrypted in chunks that cannot be dropped, repeated, or reordered backup_encryption_key src/cluster_erp/core/crypto_at_rest.py:70-140, src/cluster_erp/server/backup_tenants.py:241-320

EncryptedStorageBackend wraps any storage backend and passes the storage key as associated data, so a ciphertext copied to another key does not decrypt (src/cluster_erp/core/storage.py:138-160). Files written before a key existed are recognised by their missing header and read as plain data (src/cluster_erp/core/crypto_at_rest.py:14-15). parse_key refuses any key that is not 32 bytes of hexadecimal (src/cluster_erp/core/crypto_at_rest.py:34-40), and the settings validator applies it at start-up (src/cluster_erp/config/settings.py:822-827). The erasure ledger also seals each line with the backup key; see privacy and data-subject requests.

Why it is built this way

  • secret_box exists because an authenticator seed must be read back to verify a code, so it cannot be hashed like a password (src/cluster_erp/core/secret_box.py:1-9).
  • crypto_at_rest uses authenticated encryption so that a modified byte fails to decrypt instead of producing wrong data, and takes the key from settings, never from the repository (src/cluster_erp/core/crypto_at_rest.py:3-5).
  • parse_key refuses short keys because a short key accepted silently would be weaker than no key (src/cluster_erp/core/crypto_at_rest.py:35-36).
  • The settings validator demands a separate mail key in multi-tenant mode: one key, one purpose (src/cluster_erp/config/settings.py:815-821).

Rules and boundaries

  • Secret values never appear in change history: column names that contain totp are in the credential list of core/sensitive_fields.py (src/cluster_erp/core/sensitive_fields.py:36). See change history.
  • Changing control_encryption_key or secret_key after enrolment makes existing sealed seeds unreadable (src/cluster_erp/core/secret_box.py:58-61). Re-enrolment with totp-enroll issues a new seed (src/cluster_erp/server/operators.py:178-195).
  • The control-plane retention pass anonymises a disabled operator account whose last sign-in is older than control_actions_retention_years years, and clears its password hash and seed (src/cluster_erp/server/control_retention.py:147-159).

Interaction with other parts

Limits and open points

  • Tenant users have no second factor: a search for verify_totp and totp over src/cluster_erp finds callers only in server/operators.py, core/tenancy.py, server/control_retention.py, and the settings comment (src/cluster_erp/config/settings.py:130-131).
  • Key rotation tooling for storage_encryption_key is not determined from code beyond the module note that re-encrypt tooling can migrate legacy plaintext (src/cluster_erp/core/crypto_at_rest.py:14-15).

Terms introduced

Term Code identifier Definition Turkish term Source
time-based one-time password verify_totp A six-digit code derived from a shared seed and the current 30-second step. zamana dayalı tek kullanımlık parola src/cluster_erp/core/totp.py:42
sealed secret secret_box.seal A reversible, encrypted form of a small secret, marked with the enc: prefix. mühürlü gizli değer src/cluster_erp/core/secret_box.py:44
encryption at rest encrypt_blob Authenticated encryption of a stored file or dump before it reaches disk or a bucket. durağan şifreleme src/cluster_erp/core/crypto_at_rest.py:50

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13