<img src="https://github.com/coignard/qatsi/blob/main/assets/logo.svg?raw=true" alt="Qatsi Logo" height="72">
[](https://github.com/coignard/qatsi/actions)
[](https://github.com/coignard/qatsi/security/code-scanning)
[](https://crates.io/crates/qatsi)
[](https://docs.rs/qatsi)
[](LICENSE)
[](https://ko-fi.com/coignard)
Stateless secret generation via hierarchical memory-hard key derivation using Argon2id. Generates cryptographically secure mnemonic or alphanumeric secrets without storing anything to disk.
> [!CAUTION]
> Qatsi is not a password manager. It is a hierarchical deterministic key derivation tool designed for generating reproducible secrets from high-entropy master secrets for high-stakes credentials: password manager master passwords, full-disk encryption passphrases, PGP and SSH key passphrases, and access to critical services on air-gapped systems where credential loss is unacceptable.
>
> For day-to-day website passwords with varying policies, rotation requirements, and existing credentials, use a traditional password manager like KeePassXC or Bitwarden. Use Qatsi where you need reproducible secrets across systems without persistent storage. See [SECURITY.md](SECURITY.md) for threat model and design limitations, or the [technical report](https://doi.org/10.48550/arXiv.2510.18614) for detailed cryptographic analysis.
## Install
```bash
cargo install qatsi
```
Or build from source manually:
```bash
git clone https://github.com/coignard/qatsi
cd qatsi
cargo build --release
sudo cp target/release/qatsi /usr/local/bin/
```
## Usage
```bash
# 8-word mnemonic (103.4 bits entropy)
qatsi --mode mnemonic --security standard
# 24-word mnemonic (310.2 bits entropy)
qatsi --mode mnemonic --security paranoid
# 20-character password (129.8 bits entropy)
qatsi --mode password --security standard
# 48-character password (311.6 bits entropy)
qatsi --mode password --security paranoid
```
You can override the default security presets with custom parameters for fine-grained control:
```bash
# Generate a 12-word mnemonic with custom KDF memory (256 MiB)
qatsi --mode mnemonic --words 12 --kdf-memory 256
# Generate a 32-character password with custom KDF iterations
qatsi --mode password --length 32 --kdf-iterations 24
```
Example usage:
```
$ qatsi --mode password --security paranoid
In [0]: ****************
In [1]: 0802BDCD52656EE9 # PGP Key ID
In [2]: Somewhere # Place created
In [3]:
Out[0]:
3:L;M3ks1ByuQ0d6b-Z*|MDtRKjQ6t:L>YjhXg+@@%emz{|m
Settings:
├─ KDF [✓] Argon2id (m=128 MiB, t=32, p=6)
├─ Master [✓] 16 bytes (16 chars)
├─ Layers [✓] 2 layers
│ ├─ [✓] In [1]: 16 bytes (16 chars)
│ └─ [✓] In [2]: 9 bytes (9 chars)
├─ Keystream ChaCha20 (256-bit)
├─ Sampling Unbiased rejection
└─ Output 48 chars
Stats:
├─ Entropy [✓] 311.6 bits (Paranoid)
├─ Length [✓] 48 chars
├─ Charset 90 chars
└─ Time 4.6s
[✓] Security: Paranoid
```
## How it works
Qatsi combines a master secret with context layers through iterative Argon2id hashing. The final derived key seeds a ChaCha20 stream cipher for unbiased generation of mnemonics or passwords. For a detailed cryptographic analysis, see the [technical report](https://doi.org/10.48550/arXiv.2510.18614).
Let $K_0 = M$ (master secret). For each layer $L_i$ with $i \in [1, n]$:
$$K_i = \text{Argon2id}(K_{i-1}, \text{Salt}(L_i), m, t, p, \ell)$$
where:
$$\text{Salt}(L) = \begin{cases}
L & \text{if } |L| \geq 16 \text{ bytes} \\
\text{BLAKE2b-512}(L) & \text{if } |L| < 16 \text{ bytes}
\end{cases}$$
Parameters:
- $K_{i-1}$ — previous derived key (or master secret for $i=1$)
- $m$ — memory cost (KiB): 65536 (Standard) or 131072 (Paranoid)
- $t$ — iterations: 16 (Standard) or 32 (Paranoid)
- $p$ — parallelism: 6
- $\ell$ — output length: 32 bytes (256 bits)
### Unbiased rejection sampling
Rejection sampling eliminates modulo bias by rejecting values outside a uniform range.
Mnemonics (EFF Large Wordlist, 7776 words):
```
Threshold T = ⌊2^16 / 7776⌋ × 7776 = 8 × 7776 = 62208
Algorithm:
1. Sample 16-bit value r from ChaCha20 keystream
2. If r < 62208:
Select word: W[r mod 7776]
3. Else: reject and repeat
Expected samples per word: 65536 / 62208 ≈ 1.053
Rejection rate: 3328 / 65536 ≈ 5.08%
```
Passwords (90-character alphabet: A-Z, a-z, 0-9, 28 symbols):
```
Threshold T = 256 - (256 mod 90) = 180
Algorithm:
1. Sample 8-bit value b from ChaCha20 keystream
2. If b < 180:
Select character: A[b mod 90]
3. Else: reject and repeat
Expected samples per character: 256 / 180 ≈ 1.422
Rejection rate: 76 / 256 ≈ 29.69%
```
This provably achieves uniform distribution (proven in Section 3.4 of the technical report).
### Output entropy
Mnemonics (7776-word EFF Large Wordlist):
$$H_{\text{mnemonic}} = w \times \log_2(7776) = w \times 12.925 \text{ bits}$$
- Standard (8 words): 103.4 bits
- Paranoid (24 words): 310.2 bits
Passwords (90-character alphabet):
$$H_{\text{password}} = \ell \times \log_2(90) = \ell \times 6.492 \text{ bits}$$
- Standard (20 characters): 129.8 bits
- Paranoid (48 characters): 311.6 bits
## Performance
Measured on Apple M1 Pro (2021), 16 GB RAM, Rust 1.90 release build, median of 5 runs:
| Standard (64 MiB, t=16, p=6) | | |
| Single layer | 544 | 64 |
| 3 layers | 1613 | 64 |
| Paranoid (128 MiB, t=32, p=6) | | |
| Single layer | 2273 | 128 |
| 3 layers | 6697 | 128 |
| Output generation | <1 | <1 |
Output generation (1000 iterations): mnemonic 2 µs, password 3 µs.
Time complexity: $O(n)$ in layer count. Space complexity: $O(1)$ in output size, $O(m)$ in KDF memory.
## Test
Run the complete test suite:
```bash
cargo test
```
## Documentation
- [Technical report](https://doi.org/10.48550/arXiv.2510.18614)
- [SECURITY.md](SECURITY.md)
## License
GPL-3.0-or-later