Sealed twice: GhostVault's hybrid ML-KEM-1024 and ML-DSA-87 design
How GHOSTSHELL's vault keeps secrets under symmetric keys, where it uses post-quantum public-key cryptography and pairs each algorithm with a classical one, and what the design does not protect.
A technical paper from the GHOSTSHELL project, describing GHOSTSHELL 0.2.0. It has not been peer reviewed.
Abstract
GhostVault is GHOSTSHELL's encrypted store for SSH passwords, key passphrases, files and, if the user chooses, the whole configuration. Its everyday path is symmetric: Argon2id stretches the passphrase, HKDF-SHA-512 separates keys and AES-256-GCM seals everything under an authenticated header, so a quantum computer gains at most Grover's square-root speed-up against it. Public-key cryptography appears only where a vault must open without its passphrase or for someone else, in recovery kits, escrow slots and shared bundles, and there every operation is sealed twice: ML-KEM-1024 with ECDH P-384 to encrypt, and ML-DSA-87 with ECDSA P-384 to sign shared bundles, so an attacker has to break both halves. This paper describes the construction as the code implements it, GHOSTSHELL's own key combiner included, reports measured sizes and timings, and sets out the limits its documentation records, among them what a passphrase change does not rotate and what stays in memory after the vault locks.
1 Introduction and threat
A terminal for network engineers holds credentials for many machines. In GHOSTSHELL, GhostVault replaces the vault of Tabby, the terminal it is built on, as the store for SSH passwords, key passphrases, files and, optionally, the whole config.yaml. Three threats shape its design:
- Theft of the file.
config.yaml, a backup or a synced copy is taken and attacked offline, with no time limit. - Harvest now, decrypt later. Anything public-key-encrypted that leaves the machine, a copy of the vault file with its recovery or escrow slots, a bundle sent to a colleague, can be kept until a quantum computer breaks its public-key half [1].
- Modification. A changed header, slot or record must be refused, not trusted.
The design answers the first with memory-hard key derivation and 256-bit symmetric encryption, the second with hybrid post-quantum public-key cryptography, and the third with authenticated encryption that binds every header field. The companion paper Harvest now, decrypt later places the vault in GHOSTSHELL's wider threat model.
2 Background
2.1 What a quantum computer changes for a vault
Shor's algorithm [2], whose approach extends to elliptic curves, breaks ECDH and ECDSA on P-384, so classical public-key encryption of a vault's master key would not survive a recording or a stored copy. Symmetric cryptography fares differently: Grover's algorithm speeds up key search only by a square root [3], so a 256-bit AES key keeps roughly 128 bits of strength, and key search on a 256-bit block cipher defines NIST's highest post-quantum security category [1].
2.2 ML-KEM and ML-DSA
ML-KEM, the key-encapsulation mechanism of FIPS 203 [4], derives from CRYSTALS-Kyber [5]; ML-DSA, the signature scheme of FIPS 204 [6], derives from CRYSTALS-Dilithium [7]. ML-KEM-1024 and ML-DSA-87 are their largest parameter sets, in NIST's highest security category; NIST recommends ML-KEM-768 by default and allows larger sets where higher security is required [1]. A vault's contents can stay sensitive for many years, so GhostVault uses the larger sets, the ones NSA's CNSA 2.0 suite calls for; the CNSA 2.0 profile for SSH, for example, requires ML-KEM-1024 and ML-DSA-87 [8]. This is a choice of parameters, not a claim of CNSA 2.0 compliance.
2.3 Hybrids
NIST allows a composite key establishment whose shared secret is the concatenation of two secrets fed to a key derivation, as long as one comes from an approved scheme, and dual signatures that are accepted only when every component verifies [1]. Bindel et al. analyse which ways of combining two KEMs stay secure while either one does [9]. The classical half is insurance against a young algorithm or its code failing: SIKE fell to a classical attack [10], Rainbow, a finalist signature scheme, lost a key on a laptop [11], and Kyber implementations leaked keys through timing [12].
2.4 Argon2id and HKDF
Argon2id is a memory-hard password-hashing function: each guess must fill and revisit a large block of memory, which makes guessing expensive on any hardware. RFC 9106 gives two recommended settings, the second of which is 64 MiB, 3 passes and 4 lanes for environments with less memory [13]. HKDF derives independent keys from one secret with a salt and a context label [14].
3 Design
3.1 The symmetric core
A vault has a random 256-bit master key (Figure 1). Each key slot wraps it with AES-256-GCM. The payload, which holds the vault's contents, the identity's secret keys and the keyring, is sealed with AES-256-GCM under HKDF-SHA-512(master key, salt ghostvault: + vault id). Its authenticated data is the canonical JSON of the whole header: suite, id, creation time, generation, public identity and every slot. Changing any header byte, a slot, the generation or the identity, makes the payload fail authentication. Each AES-256-GCM box has a random 96-bit nonce and a 128-bit tag.
master key 256 bits, random
├─ passphrase slot AES-256-GCM(master key), key = HKDF-SHA-512(Argon2id(passphrase), salt = slot context)
├─ recipient slots AES-256-GCM(master key), key = hybrid KEM (ML-KEM-1024 + ECDH P-384)
│ to a recovery kit or an escrow vault
└─ payload AES-256-GCM, key = HKDF-SHA-512(master key, "ghostvault:" + vault id)
{ vault contents, identity secret keys, keyring }
authenticated data: suite, id, created, generation, identity, every slot
vaultFormat.ts builds it.3.2 The passphrase slot
Argon2id, version 0x13, runs over the passphrase in Unicode NFKC form with a 32-byte random salt and a 32-byte output. The standard profile is RFC 9106's second setting, 64 MiB, 3 passes and 4 lanes [13]; the high profile uses 256 MiB with the same passes and lanes. The output goes through HKDF-SHA-512 with the slot context as salt [14], and the result wraps the master key, the slot context again serving as authenticated data. The context binds vault id, slot id, slot type and the Argon2id parameters, so a slot cannot be moved to another vault or have its cost lowered. Because the file is untrusted, its parameters are range-checked before any work is done: 19 MiB to 1 GiB of memory, 1 to 10 passes, 1 to 16 lanes, a 16- to 64-byte salt, at most 32 slots, and known slot types only.
3.3 The hybrid KEM combiner
Recipient slots and bundles use a KEM built from ML-KEM-1024 and ephemeral-static ECDH on P-384 (Figure 2). The sender encapsulates to the recipient's ML-KEM-1024 key, which gives a 32-byte secret and a 1,568-byte ciphertext, and runs ECDH between a fresh P-384 key and the recipient's static one, which gives a 48-byte secret. The shared key is:
key = HKDF-SHA-512(ikm = ss_mlkem ‖ ss_ecdh, salt = SHA-512(transcript), info = "GHOSTVAULT v2 hybrid KEM key: " + purpose)
The transcript is the length-prefixed concatenation of a fixed label, the ML-KEM ciphertext, the ephemeral P-384 key, the recipient's ML-KEM key, the recipient's P-384 key and the purpose, such as vault <id> slot <slot id> or bundle <id>. The input follows NIST's composite pattern, the two secrets concatenated before a key derivation [1]; the salt ties the key to both ciphertexts, both recipient keys and one purpose. ML-KEM's implicit rejection is kept: a wrong key or a changed ciphertext yields an unrelated key, and the AES-256-GCM that follows fails. This combiner is GHOSTSHELL's own, not a published construction, and no third party has analysed it.
pq.ts: both secrets feed one HKDF-SHA-512, salted with a hash of everything the encapsulation used.3.4 Composite signatures
Bundles carry a composite signature: ML-DSA-87 and ECDSA P-384 with SHA-384, both over m = SHA-512(length-prefixed: label, purpose, message). A signature is accepted only if both halves verify, NIST's rule for dual signatures [1]. ML-DSA-87 signs hedged, with fresh randomness each time, and an empty context; the ECDSA half is encoded as r ‖ s in 96 bytes.
3.5 The vault's identity
Each vault has one hybrid KEM key pair and one hybrid signing key pair. The public halves form its GhostVault ID, ghostvault-id:2: followed by the base64url of their canonical JSON; its fingerprint is GV2- and eight groups of four hex digits from SHA-512 over a label and that JSON. The secret halves live only inside the encrypted payload: the 64-byte ML-KEM seed and the 32-byte ML-DSA seed, from which the lattice keys are derived again on each use, and the two P-384 private keys. When decapsulating, the recipient's public key is recomputed from its secret rather than taken from a stored copy.
3.6 Where post-quantum cryptography is used, and where it is not needed
Table 1 lists every place the vault encrypts or signs. Post-quantum cryptography appears exactly where a public key does; everywhere else a 256-bit symmetric key suffices.
| Part | Protection | Kind |
|---|---|---|
| Passphrase slot | Argon2id, HKDF-SHA-512, AES-256-GCM | 256-bit symmetric |
| Payload: secrets, configuration, identity, keyring | AES-256-GCM with the header as authenticated data | 256-bit symmetric |
| History and session logs, off by default | AES-256-GCM per record, a key per file from the keyring | 256-bit symmetric |
| Recovery kit | A slot for a fresh hybrid KEM key pair, whose secret half is saved only in the .gvkit file | PQ hybrid ML-KEM-1024 + P-384 |
| Escrow | A slot for another vault's KEM key, with that vault's fingerprint bound into the slot | PQ hybrid ML-KEM-1024 + P-384 |
| Bundle, for backup or sharing | Hybrid KEM to the recipient, AES-256-GCM payload, the sender's composite signature | PQ hybrid both pairs |
| No vault set up | SSH passwords and key passphrases in Windows Credential Manager | Windows |
4 Implementation
4.1 Code and libraries
The cryptography is in src/ghostshell-core/src/ghostvault/: primitives.ts (AES-256-GCM, HKDF-SHA-512, SHA-512, canonical JSON, length prefixes), kdf.ts, pq.ts, vaultFormat.ts, bundle.ts and records.ts; services/vault.service.ts holds the open vault. ML-KEM-1024 and ML-DSA-87 come from @noble/post-quantum 0.7.1 and Argon2id from @noble/hashes 2.4.0. Everything classical, AES-256-GCM, HKDF, SHA-2, ECDH and ECDSA on P-384 and the random numbers, comes from the platform's crypto: BoringSSL in Electron, OpenSSL in Node. The suite string GV2:Argon2id:AES-256-GCM:HKDF-SHA-512:ML-KEM-1024+P-384:ML-DSA-87+P-384 is checked on every container and bundle, and anything else is refused.
4.2 Audit status
@noble/post-quantum states that it has not been independently audited: its authors audited version 0.6.1 themselves in April 2026, and it does not claim constant-time execution. @noble/hashes had an independent audit at version 1.0.0 in January 2022, which left out argon2. Constant time matters for ML-KEM, as KyberSlash showed [12]. In the hybrid the P-384 half runs in BoringSSL, so a weakness in one implementation does not by itself expose a key.
4.3 Operations
Key material held in buffers, the master key and every key derived from it, is zero-filled when it is no longer needed. Table 2 lists what each operation does, in the corrected wording of the 0.2.0 documentation.
| Operation | What happens |
|---|---|
| Opening the vault | Argon2id over the passphrase slot, or the hybrid KEM with a recovery kit. The master key is kept in a buffer; the identity's secret keys and both keyring keys as base64 strings. |
| Locking | The master-key buffer is zero-filled and the session dropped. The identity and keyring strings stay in memory until the garbage collector reuses it, and the history file key stays for the session. Locking follows Lock now, the timeout (one minute by default), screen lock, sleep, or turning the vault off. |
| Re-confirmation | Before anything that changes who can open the vault, the passphrase or a recovery kit is asked for again; a confirmation covers the next 60 seconds. |
| Changing the passphrase, rotating the master key | A new master key and passphrase slot; recovery and escrow slots are re-encrypted to their stored public keys. The identity and the keyring are kept. |
| Removing a slot | Offered with a rotation: without one, whoever holds the removed key and an earlier copy of the file could still learn the master key. |
| Importing a bundle | The signature is checked against the sender key the bundle carries before anything is decrypted; the secrets are then merged, and only then is the sender's fingerprint shown for comparison. |
5 Evaluation
5.1 Tests
The app's unit suite, re-run independently for 0.2.0 (245 tests: 244 passed, 1 skipped outside the vault), has 21 vault tests, all passing. They check that AES-256-GCM refuses any change to the ciphertext, nonce, key or authenticated data; that out-of-range Argon2id parameters are refused and that one derivation runs the real standard profile; that the hybrid KEM agrees on its key, and that another purpose, secret key, ML-KEM ciphertext or ephemeral P-384 key gives an unrelated one; that a composite signature fails with a changed message, purpose or key, or with either half taken from another signer; that changing the generation, creation time, identity, KDF passes or payload stops the vault opening; passphrase change and master-key rotation, with identity and keyring kept; escrow ownership and revocation; record files refusing reordering, foreign keys and edits; and bundles refusing a changed note or a swapped sender.
5.2 Sizes and timings
For this paper, the app's own modules (pq.ts, kdf.ts, vaultFormat.ts, bundle.ts) were loaded from the source tree through the app's test hooks into Node 24.22.2 on Windows 11, on a 14th-generation Intel Core i9 laptop processor, on 12 November 2026, while other development processes were running. Each time is the median of the stated number of calls after warm-up calls, each timed with performance.now(); the passphrase derivation was measured in two separate runs. Tables 3 and 4 give the results.
| Item | Bytes |
|---|---|
| ML-KEM-1024 encapsulation key, ciphertext | 1,568 each |
| ML-KEM-1024 seed, the stored secret | 64 |
| P-384 public key (SPKI), private key (PKCS #8) | 120, 185 |
| ML-DSA-87 public key, signature, seed | 2,592, 4,627, 32 |
| ECDSA P-384 signature | 96 |
| GhostVault ID, in characters | 7,927 |
| New container; with one recovery slot | 7,812; 12,674 |
| Bundle with one short secret | 15,156 |
| Operation | Calls | Median | Range |
|---|---|---|---|
| Hybrid KEM key pair | 100 | 3.0 | 2.8–7.5 |
| Encapsulate | 100 | 5.1 | 4.8–8.9 |
| Decapsulate | 100 | 5.8 | 5.2–15.2 |
| Signing key pair | 50 | 6.8 | 6.3–16.1 |
| Sign 1 KiB | 50 | 27.3 | 16.1–134.6 |
| Verify 1 KiB | 50 | 7.2 | 6.8–8.7 |
| Create a bundle | 30 | 30.2 | 21.9–79.7 |
| Open a bundle | 30 | 13.0 | 12.7–14.1 |
| Open the vault with a recovery kit | 30 | 7.9 | 7.4–9.4 |
| Argon2id standard, first run | 5 | 2,808 | 2,477–2,899 |
| Argon2id standard, second run | 7 | 3,190 | 2,434–3,570 |
| Argon2id high, first run | 3 | 11,892 | 11,704–15,139 |
| Argon2id high, second run | 5 | 12,636 | 11,721–13,825 |
Every public-key operation takes milliseconds, and opening a vault costs one Argon2id derivation, about three seconds at the standard profile. Each guess at a stolen file costs an attacker the same derivation: three passes over 64 MiB of memory. Signing times vary widely because ML-DSA repeats its signing loop a variable number of times [6]. GHOSTSHELL's documentation gives about 7 seconds for the high profile; in these runs it took about 12. The app runs the same code inside Electron, which these figures do not measure.
6 Limitations and what is not done
- The library and the combiner.
@noble/post-quantumhas not been independently audited and does not claim constant-time execution, argon2 was outside@noble/hashes' audit, and the KEM combiner has not been analysed by a third party. - What stays in memory. Passphrases pass through JavaScript strings, which cannot be wiped. The identity's secret keys and the keyring keys are strings while the vault is open, and they stay in memory after it locks, until the garbage collector reuses it. Only the master key and the keys in buffers are zero-filled.
- A passphrase change does not rotate the identity. The identity and keyring are never rotated, by design, so older files and bundles stay readable. An old copy of the vault file with its old passphrase, or with a removed recovery kit, still gives them, and with them history and log records written later, bundles later sent to this GhostVault ID, and escrow slots other vaults hold for it.
- Escrow and bundles prove less than they might seem to. No signature shows who made an escrow file: anyone who knows a vault's GhostVault ID can make a vault file with a slot for it. A bundle's valid signature proves that the bundle is intact, not who sent it, since anyone who knows the recipient's ID can sign one with their own identity; the fingerprint shown after import is what to compare.
- Outside the model. Rolling the file back to an older valid copy (there is no trusted counter); metadata such as file names, sizes, slot labels and KDF parameters; malware running as the user; plugins, which run with the app's privileges; and weak passphrases, which Argon2id makes slower to guess, not safe. History files have no end marker, so their newest commands could be cut off unnoticed.
- Not re-run for 0.2.0. The vault flows in the running app, last driven end to end with Playwright for 0.1.0 on 5 November 2026.
7 Conclusion
GhostVault uses post-quantum cryptography exactly where a public key is unavoidable, and seals each such operation twice, with ML-KEM-1024 and P-384 to encrypt and with ML-DSA-87 and P-384 to sign, so a quantum computer or a flaw in young code must defeat both halves. Everything else runs on a symmetric path whose strength a quantum computer can at best halve, and the costs measured here are milliseconds for the hybrid operations and seconds for the passphrase. The design's weak points are recorded rather than hidden: an unaudited post-quantum library, a combiner nobody else has analysed, keys left in memory as strings, and an identity that a passphrase change does not replace.
References
- [1]D. Moody, R. Perlner, A. Regenscheid, A. Robinson and D. Cooper. Transition to Post-Quantum Cryptography Standards. NIST Internal Report 8547, final, October 2026. doi:10.6028/NIST.IR.8547.
- [2]P. W. Shor. Polynomial-Time Algorithms for Prime Factorization and Discrete Logarithms on a Quantum Computer. SIAM Journal on Computing 26(5):1484–1509, 1997. doi:10.1137/S0097539795293172.
- [3]L. K. Grover. A fast quantum mechanical algorithm for database search. Proceedings of the 28th Annual ACM Symposium on Theory of Computing (STOC '96), pp. 212–219, 1996. doi:10.1145/237814.237866.
- [4]National Institute of Standards and Technology. Module-Lattice-Based Key-Encapsulation Mechanism Standard. FIPS 203, August 2024. doi:10.6028/NIST.FIPS.203.
- [5]J. Bos, L. Ducas, E. Kiltz, T. Lepoint et al. CRYSTALS – Kyber: A CCA-Secure Module-Lattice-Based KEM. 2018 IEEE European Symposium on Security and Privacy (EuroS&P), pp. 353–367. doi:10.1109/EuroSP.2018.00032.
- [6]National Institute of Standards and Technology. Module-Lattice-Based Digital Signature Standard. FIPS 204, August 2024. doi:10.6028/NIST.FIPS.204.
- [7]L. Ducas, E. Kiltz, T. Lepoint, V. Lyubashevsky et al. CRYSTALS-Dilithium: A Lattice-Based Digital Signature Scheme. IACR Transactions on Cryptographic Hardware and Embedded Systems 2018(1):238–268. doi:10.46586/tches.v2018.i1.238-268.
- [8]A. Becker, C. Wynn and M. Jenkins. Commercial National Security Algorithm (CNSA) Suite 2.0 Profile for SSH. Internet-Draft draft-becker-cnsa2-ssh-profile-07, October 2026, individual submission, work in progress.
- [9]N. Bindel, J. Brendel, M. Fischlin, B. Goncalves and D. Stebila. Hybrid Key Encapsulation Mechanisms and Authenticated Key Exchange. Post-Quantum Cryptography (PQCrypto 2019), LNCS 11505, pp. 206–226, 2019. doi:10.1007/978-3-030-25510-7_12.
- [10]W. Castryck and T. Decru. An Efficient Key Recovery Attack on SIDH. Advances in Cryptology (EUROCRYPT 2023), LNCS 14008, pp. 423–447, 2023. doi:10.1007/978-3-031-30589-4_15.
- [11]W. Beullens. Breaking Rainbow Takes a Weekend on a Laptop. Advances in Cryptology (CRYPTO 2022), LNCS 13508, pp. 464–479, 2022. doi:10.1007/978-3-031-15979-4_16.
- [12]D. J. Bernstein, K. Bhargavan, S. Bhasin, A. Chattopadhyay et al. KyberSlash: Exploiting secret-dependent division timings in Kyber implementations. IACR Transactions on Cryptographic Hardware and Embedded Systems 2025(2):209–234. doi:10.46586/tches.v2025.i2.209-234.
- [13]A. Biryukov, D. Dinu, D. Khovratovich and S. Josefsson. Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications. RFC 9106, September 2021.
- [14]H. Krawczyk and P. Eronen. HMAC-based Extract-and-Expand Key Derivation Function (HKDF). RFC 5869, May 2010.