Skip to content

Cryptography

All cryptography is in one package, core/pwcrypto. No other package uses crypto primitives. Lasbolt uses standard primitives from the Go standard library and golang.org/x/crypto. It has no new cryptography. This page names the algorithms and the parameters. The source of truth is the code and the test vectors in core/pwcrypto/testdata/.

Every change to pwcrypto needs new test vectors and a review. The vectors are in core/pwcrypto/testdata/vectors.json.

Algorithms

PurposeAlgorithmNotes
Password to keysArgon2id, then HKDF-SHA256See “Master password” below
Symmetric encryptionXChaCha20-Poly130524-byte random nonce. Associated data binds each ciphertext to its context.
Public-key sealingHPKE (RFC 9180) with the hybrid KEM ML-KEM-768 + X25519, HKDF-SHA256 and ChaCha20-Poly1305Post-quantum protection for wrapped keys
SignaturesEd25519Every signature has a purpose label
Hashes and MACsSHA-256, HMAC-SHA256
One-time passwordsTOTP, RFC 6238: HMAC-SHA1, 6 digits, 30 secondsThe compatible choice for authenticator apps
Session tokens256 random bits. The server stores SHA-256 of the token.
Safety numbersSHA-256 based, 60 decimal digitsSee below

The first byte of each ciphertext is a suite ID. 0x01 is XChaCha20-Poly1305. 0x02 is the HPKE suite. A reader refuses an unknown suite. So the format can change later without breaking old data.

Associated data and context strings come from one function, Info(purpose, ids...). It builds pword/v1/<purpose> followed by length-prefixed identifiers. A ciphertext that is moved to another place does not open.

Master password

The client runs Argon2id on the master password. The production minimum is 64 MiB of memory and 3 iterations. The default is 64 MiB, 3 iterations and 4 threads, with a random 16-byte salt. The parameters are stored for each user, so they can go up later. Code refuses weaker values, and only test builds (the insecurekdf tag) lower the minimum. The release builds and the browser code do not contain the weak parameters. A build check looks for them.

HKDF-SHA256 splits the Argon2id output into two independent keys:

KeyHKDF infoUse
Auth keypword/v1/authSent to the server to log in
Encryption keypword/v1/encNever leaves the client. Opens your private keys.

The server stores HMAC-SHA256(salt, "pword/v1/auth-verifier" || auth key) with a random salt for each user, and compares it in constant time. For an unknown user name, the server returns production KDF parameters with a derived fake salt, so the answer does not tell whether the account exists.

Key hierarchy

    flowchart TB
  MP["Master password"] -->|"Argon2id + HKDF"| ENC["Encryption key"]
  ENC -->|"XChaCha20-Poly1305"| UK["User keys<br/>HPKE key pair + Ed25519 key"]
  RC["Recovery code<br/>130 random bits"] -->|"HKDF"| RENC["Recovery enc key"]
  RENC -->|"XChaCha20-Poly1305"| UK
  UK -->|"HPKE seal"| FK["Folder key pair<br/>one for each E2E folder"]
  FK -->|"HPKE seal"| IK["Item key<br/>XChaCha20-Poly1305"]
  IK -->|"encrypts"| V["Field values"]
  TK["Team key pair"] -->|"HPKE seal"| FK
  UK -->|"HPKE seal"| TK
  
  1. User keys. One HPKE key pair (ML-KEM-768 + X25519) for encryption, and one Ed25519 key for signatures. The private keys are one bundle, sealed with the encryption key. The server stores the sealed bundle and the public keys.
  2. Folder keys. Each E2E folder has its own HPKE key pair. The public key is open. The private key is sealed to the box key of each member, or of a team. A signed bundle ties the folder public key to the folder and its creator.
  3. Item keys. Each item has a random symmetric key. The item key is sealed to the folder public key and is set once. Every field value is encrypted with the item key. A second key encrypts the metadata, so a principal can get metadata without the secret values.

Two properties follow:

  • Write without read. A person with write access can add items with only the public folder key. A CLI, a CI job or an agent can store a secret that it cannot read.
  • Cheap sharing. To add a member, one client seals the folder private key to the box key of the member. It is one operation, not one for each item. The server cannot do it.

A master password change only seals the user bundle again with a new encryption key. A key rotation makes new user keys and links them to the old keys with a signed chain.

Signatures

Every operation in an E2E folder carries the Ed25519 signature of its author. A client drops an operation without a valid signature from a member with write access. A signature covers a purpose label and the message. Labels keep a signature for one purpose from working for another. Examples are op for operations, folder-pub for folder bundles, space-member for entries of the membership log, contact for contact pins and principal-cert for scoped keys.

Other signed objects:

  • The membership log of a shared space: a hash chain of signed entries.
  • Checkpoints of E2E folders, which find hidden or changed older operations.
  • Purge and revocation records, and rotation records for user keys.

Safety numbers

A safety number lets two people check each other’s public keys. The inputs are the Ed25519 public key and the box public key of both users. The keys are put in a fixed order. Two SHA-256 digests, with a tag and a counter, give 64 bytes. The first 60 bytes make 12 groups of 5 decimal digits (each 5 bytes, as a number, modulo 100000). The result is 60 digits. Both people compute the same number and compare it out of band.

Recovery code

The recovery code has 26 characters of the Crockford base32 alphabet. That gives 130 random bits. It shows as PWRK- and groups of four. HKDF-SHA256 turns the code into an auth key and an encryption key. There is no Argon2id, because the code has enough randomness. The encryption key seals a second copy of the user bundle. The server stores only a hash of the derived auth key.

Scoped keys

A scoped key string is lasbolt_sk_<key id>_<secret>. The secret has 256 bits. HKDF splits it into an auth part (the bearer token) and an encryption part that opens the key’s private keys. The server stores only a hash of the auth part. The owner’s Ed25519 key signs a certificate for the key with its scope and expiry. Clients trust the operations of a key only inside that scope.

Encryption at rest on the server

The server has one random data key. It encrypts all server-readable values with XChaCha20-Poly1305. A key encryption key (KEK) wraps the data key, and the data key lives only in memory. The KEK comes from a provider:

ProviderKEK
passphrase (default)Argon2id of an operator passphrase, with the same minimum parameters as above
file32 random bytes from a file with mode 0600
insecure-staticA fixed, known key. Only for tests and development.

Backups can be sealed to an HPKE public key of the operator (--backup-recipient). The sealed file uses chunked XChaCha20-Poly1305 with a wrapped file key, in chunks of 1 MiB. A reader detects a changed header, a moved or removed chunk and a cut file.

Other uses

  • Peer sync uses an Ed25519 key for each device, and QUIC with TLS 1.3 and pinned keys. A MAC filter drops datagrams from strangers.
  • Bitwarden import reads password-protected exports with PBKDF2-HMAC-SHA256 or Argon2id, AES-256-CBC and HMAC-SHA256. It decrypts imports only. It never writes Lasbolt data with these.

What is not covered

  • Lasbolt has had no third-party audit.
  • The algorithms protect the data. They do not protect a device that has malware, or a master password that is weak or reused.
  • The web app gets its code from the server, so it has no E2E promise against an active attacker on the server. See Security.
Last updated on