- Generate 3168‑byte secret key (
secretKey.qkey) & 1568-byte public key (publicKey.qkey) for ML-KEM-1024 post-quantum key encapsulation algorithm. - Encrypt client-side arbitrary files using hybrid cryptography. In this approach, the ML-KEM-1024 securely negotiates a symmetric key between the sides, which is then used by AES-256-GCM to directly encrypt the file data.
- Decrypt
.qenccontainers created by this tool. - Split
.qenccryptocontainers with.qkeyprivate keys into multiple.qcontshards, export canonical signable*.qvmanifest.json, and embed both the canonical manifest and an initial manifest bundle into every shard. You choose total shards n and RS data k; threshold t is computed ast = k + (n-k)/2. With fewer than t shards, no information about the original secret can be retrieved. - Attach detached authenticity material (
.qsig, Stellar.sig,.pqpk,.ots) to a canonical manifest or existing manifest bundle, producing a self-contained*.extended.qvmanifest.jsonbundle and optionally rewriting a full shard cohort in place. - Restore from a single input set of files (
.qcont+ optional canonical*.qvmanifest.jsonor bundled*.extended.qvmanifest.json+ optional.qsig/.sig/.pqpk/.ots) and reconstruct.qenc+ private.qkeyfrom a sufficient number of shards (>= t), but only if the archive authenticity policy is satisfied. - Verify detached manifest signatures from external signer apps (Quantum Signer
.qsig, Stellar WebSigner.sig) with explicit archive policy evaluation, proof-identity deduplication, and separate bundle-pinned vs user-pinned signer identity reporting. - Verifies file integrity using SHA3-512 hash sum and provides process logs to track operations.
- All cryptographic operations are performed directly in the client's browser, ensuring the confidentiality of user data.
| Stage | Main inputs | Main outputs | What this stage adds |
|---|---|---|---|
| Generate | Browser entropy | secretKey.qkey, publicKey.qkey |
ML-KEM-1024 key pair |
| Encrypt | File(s), publicKey.qkey |
.qenc |
Post-quantum confidentiality and AEAD integrity |
| Split | .qenc, secretKey.qkey |
.qcont, *.qvmanifest.json |
Threshold recovery, embedded manifest, initial bundle |
| Attach | Manifest or bundle, .qsig/.sig, optional .pqpk, optional .ots |
*.extended.qvmanifest.json, optional rewritten .qcont |
Portable authenticity material without changing canonical signed bytes |
| Restore | .qcont, optional manifest/bundle, optional signatures/pins/timestamps |
Recovered .qenc, recovered secretKey.qkey |
Policy-gated archive reconstruction |
| Decrypt | .qenc, secretKey.qkey |
Original file(s) | Payload recovery and UI-level file-hash confirmation |
| Artifact | Produced by | Purpose | Notes |
|---|---|---|---|
.qenc |
Encrypt | Encrypted container | Carries public metadata, key commitment, ciphertext |
.qcont |
Split | Threshold shard | Carries one Shamir share, RS fragment stream, embedded manifest, embedded bundle |
*.qvmanifest.json |
Split | Canonical signable manifest | Immutable detached-signature payload |
*.extended.qvmanifest.json |
Attach | Self-contained manifest bundle | Mutable bundle containing policy, attached keys, signatures, timestamps |
.qsig |
Quantum Signer | Detached PQ signature | Current supported format is Quantum Signer v2 |
.sig |
Stellar WebSigner | Detached Ed25519 signature proof | Current supported format is stellar-signature/v2 JSON |
.pqpk |
Quantum Signer | Detached PQ public key | Used for bundle pinning or user pinning |
.ots |
External timestamp tool | OpenTimestamps evidence | Linked to detached signature bytes, not to the bundle itself |
The risk of cryptographic obsolescence is a matter of concern. It is anticipated that classical asymmetric schemes (RSA, ECC) will become vulnerable once sufficiently large fault-tolerant quantum computers are developed. This process gives rise to two distinct risks associated with long-lived data:
- It can be argued that the present is an opportune moment to harvest encrypted data, as this may be decrypted in the future when PQ capabilities are extant.
- In the context of standards transition, it is anticipated that standards bodies will ultimately necessitate or advocate for the utilisation of post-quantum algorithms in novel systems and for archival data. Systems that do not employ PQC will encounter compatibility, compliance and migration expenses.
High-value data must be made available with a high degree of reliability and protection against single-point failures. Distributed threshold storage (Shamir shares across independent storage providers) has been shown to address availability and improve censorship resistance, but it has also been demonstrated to introduce metadata and integrity challenges.
It is imperative that users generate, back up, shard and restore cryptographic containers with minimal chance of loss or misconfiguration.
Threat model (concise):
- Adversary goals: confidentiality breach of stored files now or later; integrity forgeries; denial of recovery by withholding shares.
- Adversary capabilities: network observers, storage provider compromise, passive archive collection (future decryption), host compromise (user device), malicious browser extension, supply-chain compromise of third-party libs.
- Assumptions: attacker cannot simultaneously control threshold number of independent share custodians, user device may be compromised (lossy trust), users will use multiple independent storage locations for shares.
Quantum Vault combines local post-quantum encryption, threshold recovery, and detached authenticity into one workflow. It is intended for situations where ordinary file encryption is not enough because you also want durable recovery, signer-verifiable provenance, and the ability to attach authenticity evidence later without changing the canonical signed archive description.
| If you need to... | Quantum Vault does this by... |
|---|---|
| Keep archive contents confidential even if storage is copied | Encrypting locally in the browser with ML-KEM-1024, KMAC256, and AES-256-GCM |
| Avoid a single backup location becoming a single point of failure | Splitting one archive into threshold .qcont shards |
| Prove who approved an archive | Signing the canonical *.qvmanifest.json with detached .qsig or Stellar .sig proofs |
| Add signer keys or timestamp evidence later without invalidating signatures | Storing mutable authenticity artifacts in the manifest bundle, not in the canonical signed bytes |
| Block restore unless authenticity requirements are met | Evaluating archive policy during restore (integrity-only, any-signature, strong-pq-signature) |
- Post-quantum confidentiality: Ensure that container data confidentiality resists known quantum attacks by using a lattice-based KEM (ML-KEM-1024) to agree symmetric keys and KMAC256 + AES-256-GCM hybrid encryption for payloads.
- Durable distributed storage: Enable secure splitting and reconstruction that provide configurable threshold/availability guarantees without leaking information below threshold.
- Zero-trust server model: All sensitive cryptographic operations and secrets must remain inside the user’s browser; no private material is uploaded or retained by any server.
- Integrity and provenance: Provide robust content integrity checks (SHA3-512) and authenticated metadata so users can detect tampering and identify container format and parameter versions.
- Usability & recoverability: Provide clear UX flows and automation for key generation, secure backups, shard distribution.
- Align algorithms and parameters with authoritative recommendations & auditability.
flowchart TB
subgraph CREATE["Create archive"]
G["Generate key pair<br/>ML-KEM-1024 KeyGen"]:::crypto
PK["publicKey.qkey"]:::artifact
SK["secretKey.qkey"]:::artifact
U["User file(s)"]:::input
E["Encrypt<br/>ML-KEM-1024 Encaps → KMAC256 → AES-256-GCM"]:::crypto
Q[".qenc"]:::artifact
S["Split<br/>Shamir(secretKey) + Reed-Solomon(ciphertext)"]:::split
QC[".qcont shards<br/>(embedded canonical manifest + initial bundle)"]:::artifact
CM["*.qvmanifest.json<br/>(canonical signable manifest)"]:::artifact
G --> PK
G --> SK
U --> E
PK --> E
E --> Q
Q --> S
SK --> S
S --> QC
S --> CM
end
subgraph AUTH["Add authenticity"]
SG["Sign externally"]:::auth
SIG[".qsig / .sig"]:::artifact
OPT["optional .pqpk / .ots"]:::optional
AT["Attach"]:::auth
EM["*.extended.qvmanifest.json"]:::artifact
CM --> SG
SG --> SIG
CM --> AT
SIG --> AT
OPT --> AT
AT --> EM
AT -. optional embedded bundle rewrite .-> QC
end
subgraph RECOVER["Recover archive"]
R["Restore<br/>(policy-gated reconstruction)"]:::restore
OUT["Recovered .qenc + secretKey.qkey"]:::artifact
D["Decrypt<br/>ML-KEM-1024 Decaps → KMAC256 → AES-256-GCM"]:::restore
F["Original file(s)"]:::input
QC --> R
CM -. optional .-> R
EM -. optional .-> R
SIG -. optional .-> R
OPT -. optional .-> R
R --> OUT
OUT --> D
D --> F
end
classDef input fill:#f5f5f5,stroke:#666,stroke-width:1px,color:#111;
classDef crypto fill:#e8f1ff,stroke:#3d5a80,stroke-width:1.5px,color:#111;
classDef split fill:#fff1d6,stroke:#a06b00,stroke-width:1.5px,color:#111;
classDef auth fill:#f3e8ff,stroke:#7c3aed,stroke-width:1.5px,color:#111;
classDef restore fill:#e8f7e8,stroke:#2e7d32,stroke-width:1.5px,color:#111;
classDef artifact fill:#ffffff,stroke:#444,stroke-width:1px,color:#111;
classDef optional fill:#fafafa,stroke:#999,stroke-width:1px,color:#555;
| Stage | User action | Required inputs | Primary outputs | Important behavior |
|---|---|---|---|---|
| 1. Generate | Create a key pair in-browser | None | secretKey.qkey, publicKey.qkey |
Secrets stay client-side only |
| 2. Encrypt | Encrypt one file or a local file bundle | File(s), publicKey.qkey |
.qenc |
Uses ML-KEM-1024 + KMAC256 + AES-256-GCM |
| 3. Split | Convert one archive into threshold shards | .qenc, matching secretKey.qkey |
.qcont, *.qvmanifest.json |
Verifies the private key matches the archive before sharding |
| 4. Sign | Sign the canonical manifest in an external signer app | *.qvmanifest.json |
.qsig or .sig |
The signed bytes are always the canonical manifest, not the mutable bundle |
| 5. Attach | Merge signatures, pins, and timestamps into the bundle | Manifest or bundle, detached artifacts | *.extended.qvmanifest.json, optional rewritten shards |
Full shard cohort rewrites embedded bundles; partial input updates only the manifest-side bundle |
| 6. Restore | Reconstruct from shards with optional authenticity inputs | .qcont, optional manifest/bundle/signatures/pins/timestamps |
Recovered .qenc, recovered secretKey.qkey |
Recovery is blocked unless the selected archive policy is satisfied |
| 7. Decrypt | Decrypt the recovered archive | .qenc, recovered secretKey.qkey |
Original file(s) | The UI confirms privateMeta.fileHash before export |
| UI mode | Default archive policy | Intended path | Practical effect |
|---|---|---|---|
| Lite | integrity-only |
Simpler protect/restore flow | Restore can proceed without detached signatures, but provenance is not signer-authenticated |
| Pro | strong-pq-signature |
Full authenticity workflow | Restore blocks until at least one valid strong PQ detached signature satisfies policy |
LICENSE # License text (GPLv3)
README.md # Project overview, architecture, format docs
index.html # Main HTML file
style.css # Main CSS styles file
package.json # Dependencies and npm scripts
package-lock.json # Locked dependency graph
scripts/
├── dev.mjs # Local static dev server
├── build.mjs # Deterministic build + integrity checks
└── selftest.mjs # Headless self-test runner entrypoint
public/
└── third-party/
└── erasure.js # Reed-Solomon runtime library
src/
├── main.js # Application entry point
├── utils.js # Shared browser utilities
├── app/ # App-layer adapters (browser/runtime boundary)
│ ├── crypto-service.js # UI-facing facade for core crypto operations
│ ├── session-wipe.js # beforeunload/pagehide secret wipe registry
│ ├── browser-entropy-collector.js # Browser entropy collection (DOM events)
│ ├── restore-inputs.js # Restore input classification from File objects
│ └── shard-preview.js # Lightweight .qcont preview for UI status
└── core/
├── crypto/ # Core crypto + format logic (UI-agnostic)
│ ├── index.js # Main encryption/decryption orchestration
│ ├── aead.js # AES-GCM nonce/IV policy helpers
│ ├── kdf.js # KMAC derivation and key commitment helpers
│ ├── kmac.js # Local SP 800-185 KMAC adapter over noble
│ ├── mlkem.js # ML-KEM-1024 implementation
│ ├── entropy.js # CSPRNG + entropy mixing primitives
│ ├── erasure-runtime.js # RS runtime resolver (globalThis/injected)
│ ├── constants.js # Format/profile constants
│ ├── policy.js # Crypto policy validation
│ ├── bytes.js # Byte/hex/constant-time helpers
│ ├── qenc/
│ │ └── format.js # .qenc header build/parse
│ ├── qcont/
│ │ ├── build.js # Shard construction + initial manifest bundle
│ │ ├── attach.js # Manifest bundle attach/merge workflow
│ │ └── restore.js # Shard restore/reconstruction + authenticity gating
│ ├── manifest/
│ │ ├── archive-manifest.js # Canonical archive manifest schema/validation
│ │ ├── manifest-bundle.js # Self-contained manifest bundle schema/validation
│ │ └── jcs.js # Project-defined canonicalization helpers (QV-C14N-v1)
│ ├── auth/
│ │ ├── qsig.js # Quantum Signer v2 detached signature parsing/verify
│ │ ├── stellar-sig.js # Stellar WebSigner v2 detached signature verify
│ │ ├── opentimestamps.js # OpenTimestamps parsing/linking
│ │ ├── signature-identity.js # Detached proof identity normalization/dedupe
│ │ ├── signature-suites.js # Normalized signature suite registry
│ │ └── verify-signatures.js # Unified verification policy orchestration
│ ├── splitting/
│ │ └── sss.js # Shamir Secret Sharing
│ └── selftest.js # Headless/browser self-test suite
└── features/ # UI workflows and rendering
├── lite-mode.js # Simplified interface
├── bundle-payload.js # Multi-file bundle payload helpers
├── qcont/
│ ├── build-ui.js # Pro split UI handlers
│ ├── attach-ui.js # Pro attach UI handlers
│ └── restore-ui.js # Pro restore UI handlers
└── ui/
├── ui.js # Pro UI orchestration
├── shards-status.js # Shard threshold/readiness status UI
├── logging.js # Unified structured logs for Lite/Pro
└── toast.js # Toast notification UI helpers
dist/ # Generated build artifacts
| Area | Responsibility | Key outputs or decisions |
|---|---|---|
src/core/crypto/index.js |
Encrypt/decrypt orchestration | .qenc construction and parsing |
src/core/crypto/qenc/ |
Archive header format | Container header and metadata framing |
src/core/crypto/qcont/build.js |
Split/shard builder | .qcont, canonical manifest, initial bundle |
src/core/crypto/qcont/attach.js |
Bundle attachment workflow | Extended bundle, optional shard rewrites |
src/core/crypto/qcont/restore.js |
Cohort selection and reconstruction | Policy-gated restore, mixed-bundle handling |
src/core/crypto/manifest/ |
Manifest and bundle schemas | Canonical archive description and mutable authenticity bundle |
src/core/crypto/auth/ |
Detached signature and OTS verification | PQ signatures, Stellar proofs, proof-identity dedupe, timestamp linkage |
src/app/restore-inputs.js |
Restore-time file classification | Detects .qcont, manifest, bundle, signatures, .pqpk, .ots |
src/core/features/qcont/ |
Pro mode workflows | Build, Attach, Restore UI integration |
src/core/features/lite-mode.js |
Lite mode workflow | Simpler end-to-end protect/restore experience |
- Post-quantum Module-Lattice-based Key Encapsulation Mechanism: ML-KEM-1024. Used to encapsulate/decapsulate a shared secret.
- Key derivation function: KMAC256. Used to derive the AES encryption key and AES IV's from the shared secret.
- Authenticated Encryption with Associated Data: AES-256-GCM. Used to encrypt the file payload and authenticate the header additional authenticated data (AAD).
- Secret sharing algorithm: Shamir's secret sharing. Used to shard and reconstruct private keys files.
- File sharing algorithm: Reed-Solomon codes. Used to shard and reconstruct containers files.
.qencfile format (one file is encrypted container - single-stream or per-chunk AEAD)
| Data | Length | Description |
|---|---|---|
| MAGIC | 4 bytes | ASCII QVv1 |
| keyLen | 4 bytes (Uint32 BE) | length of encapsulatedKey |
| encapsulatedKey | keyLen bytes | ML‑KEM ciphertext |
| containerNonce | 12 bytes | container nonce / IV root |
| kdfSalt | 16 bytes | random salt for KMAC |
| metaLen | 2 bytes (Uint16 BE) | length of metaJSON |
| metaJSON | metaLen bytes UTF‑8 | JSON metadata |
| keyCommitment | 32 bytes | required SHA3‑256(Kenc) key commitment |
| ciphertext | remaining bytes | AES‑GCM ciphertext (single or concatenation) |
.qenc metaJSON (indicative):
{
"KEM":"ML-KEM-1024",
"KDF":"KMAC256",
"AEAD":"AES-256-GCM",
"fmt":"QVv1-5-0",
"cryptoProfileId":"QV-MLKEM1024-KMAC256-AES256GCM-SHA3_512-v2",
"kdfTreeId":"QV-KDF-TREE-v2",
"aead_mode":"single-container-aead | per-chunk-aead",
"iv_strategy":"single-iv | kmac-prefix64-ctr32-v3",
"noncePolicyId":"QV-GCM-RAND96-v1 | QV-GCM-KMACPFX64-CTR32-v3",
"nonceMode":"random96 | kmac-prefix64-ctr32",
"counterBits":0 | 32,
"maxChunkCount":1 | 4294967295,
"aadPolicyId":"QV-AAD-HEADER-CHUNK-v1",
"hasKeyCommitment": true,
"payloadFormat":"wrapped-v1",
"payloadLength": 12345,
"chunkSize": 8388608,
"chunkCount": 1,
"domainStrings": {
"kdf":"quantum-vault:kdf:v2",
"iv":"quantum-vault:chunk-iv:v2",
"kenc":"quantum-vault:kenc:v2",
"kiv":"quantum-vault:kiv:v2"
}
}Private metadata (encrypted inside payload, wrapped-v1):
{
"originalFilename":"<string|null>",
"timestamp":"<ISO8601 time>",
"fileHash":"<SHA3-512 hex of original file>",
"originalLength": 123
}Payload format (wrapped-v1):
[uint32be privateMetaLen][privateMetaJSON][fileBytes]
Note: when aead_mode is per-chunk-aead, the ciphertext is a concatenation of per‑chunk AES‑GCM outputs (each chunk includes its 16‑byte tag).
AAD:
-
Single-container AEAD: entire header from MAGIC through
metaJSON. -
Per-chunk AEAD:
AAD_i = header || uint32_be(chunkIndex) || uint32_be(plainLen_i). -
.qcontcomposite shard file format (one file contains part of Shamir's splited key + part of RS fragment)
| Data | Length | Description |
|---|---|---|
| MAGIC_SHARD | 4 bytes | ASCII QVC1 |
| metaLen | 2 bytes (Uint16 BE) | |
| metaJSON | metaLen bytes (UTF-8) | RS params, counts, hashes, etc. |
| manifestLen | 4 bytes (Uint32 BE) | length of embedded canonical archive manifest |
| manifestBytes | manifestLen bytes | full canonical signable *.qvmanifest.json bytes |
| manifestDigest | 64 bytes | SHA3-512(manifestBytes) |
| bundleLen | 4 bytes (Uint32 BE) | length of embedded manifest bundle |
| bundleBytes | bundleLen bytes | full canonical QV-Manifest-Bundle JSON bytes |
| bundleDigest | 64 bytes | SHA3-512(bundleBytes) |
| encapBlobLen | 4 bytes (Uint32 BE) | |
| encapBlob | encapBlobLen bytes | ML-KEM ciphertext |
| containerNonce | 12 bytes | from .qenc header |
| kdfSalt | 16 bytes | from .qenc header |
| qencMetaLen | 2 bytes (Uint16 BE) | |
| qencMetaBytes | qencMetaLen bytes (UTF-8) | original .qenc metadata (dup for convenience) |
| keyCommitLen | 1 byte | key commitment length (must be 32 in current format) |
| keyCommitBytes | keyCommitLen bytes | required SHA3-256(Kenc) from .qenc header |
| shardIndex | 2 bytes (Uint16 BE) | 0-based index (0..n-1) |
| shareLen | 2 bytes (Uint16 BE) | |
| shareBytes | shareLen bytes | one Shamir share |
| fragments stream | … | concatenation of per-chunk fragments for this shard; |
| each fragment is stored as `[len32 |
.qcont metaJSON (indicative):
{
"containerId":"<SHA3-512 hex of .qenc header>",
"alg":{"KEM":"ML-KEM-1024","KDF":"KMAC256","AEAD":"AES-256-GCM","RS":"ErasureCodes","fmt":"QVqcont-6"},
"aead_mode":"single-container | per-chunk",
"iv_strategy":"single-iv | kmac-prefix64-ctr32-v3",
"cryptoProfileId":"QV-MLKEM1024-KMAC256-AES256GCM-SHA3_512-v2",
"kdfTreeId":"QV-KDF-TREE-v2",
"noncePolicyId":"QV-GCM-RAND96-v1 | QV-GCM-KMACPFX64-CTR32-v3",
"nonceMode":"random96 | kmac-prefix64-ctr32",
"counterBits":0 | 32,
"maxChunkCount":1 | 4294967295,
"aadPolicyId":"QV-AAD-HEADER-CHUNK-v1",
"n":5,"k":3,"m":2,"t":4,
"rsEncodeBase":255,
"chunkSize":8388608,
"chunkCount":1,
"containerHash":"<SHA3-512 hex of .qenc file>",
"encapBlobHash":"<SHA3-512 hex>",
"privateKeyHash":"<SHA3-512 hex>",
"payloadLength":12345,
"originalLength":123,
"ciphertextLength":456,
"domainStrings":{
"kdf":"quantum-vault:kdf:v2",
"iv":"quantum-vault:chunk-iv:v2",
"kenc":"quantum-vault:kenc:v2",
"kiv":"quantum-vault:kiv:v2"
},
"fragmentFormat":"len32-prefixed",
"perFragmentSize":789,
"hasKeyCommitment":true,
"keyCommitmentHex":"<hex>",
"hasEmbeddedManifest":true,
"manifestDigest":"<SHA3-512(manifestBytes)>",
"hasEmbeddedBundle":true,
"bundleDigest":"<SHA3-512(bundleBytes)>",
"authPolicyLevel":"integrity-only | any-signature | strong-pq-signature",
"shareCommitments":["<hex>", "..."],
"fragmentBodyHashes":["<hex>", "..."],
"timestamp":"<ISO8601 time>"
}Note: for wrapped-v1, payloadLength refers to the encrypted payload size (private metadata + file bytes). The original file length is stored inside the private metadata.
Canonical manifest is generated at split stage with schema/version quantum-vault-archive-manifest/v2 and serialized as project-defined canonical JSON QV-C14N-v1 (not full RFC 8785). The same canonical bytes are:
- exported as the signable
*.qvmanifest.json, - embedded into every
.qcontshard, - embedded inside every manifest bundle,
- used as detached-signature input for
.qsig/.sig.
Key contract points:
- Primary authenticity anchor is
qenc.qencHashwith explicit algorithmqenc.hashAlg = "SHA3-512"(hash over full.qencbytes). qenc.containerIdis a secondary identifier (containerIdRole = "secondary-header-id",containerIdAlg = "SHA3-512(qenc-header-bytes)").- Nonce policy is mode-bound and explicit:
noncePolicyId,nonceMode,counterBits,maxChunkCount. - Per-chunk nonce contract is fail-closed:
chunkIndexis uint32,0 <= chunkIndex < chunkCount <= maxChunkCount <= 4294967295. shardBindingis explicitly defined and non-recursive:bodyDefinitionId = "QV-QCONT-SHARDBODY-v1"- shard body hash input includes fragment stream payload only (
len32-prefixedRS fragment stream), - excludes header, embedded manifest/digest, embedded bundle/digest, and external signatures,
- optional Shamir share commitments commit to raw share bytes.
- archive authenticity policy is committed inside the canonical manifest as
authPolicyCommitment; the concreteauthPolicyobject lives in the manifest bundle.
Current supported format boundary is fail-closed:
- archive manifests must use
quantum-vault-archive-manifest/v2, .qcontshards must useQVqcont-6,- detached PQ signatures must use Quantum Signer major version 2 with context
quantum-signer/v2, - Stellar detached signatures must use
stellar-signature/v2.
Manifest bundle is a self-contained mutable JSON object with:
- embedded canonical
manifest, manifestDigest = SHA3-512(canonical manifest bytes),- explicit
authPolicy(integrity-only | any-signature | strong-pq-signatureplusminValidSignatures), - attached
publicKeys[],signatures[], andtimestamps[].
Changing bundle attachments does not mutate the canonical manifest bytes and does not change detached-signature payload semantics.
Typical naming:
- split exports canonical signable manifest as
*.qvmanifest.json, - attach exports self-contained bundle as
*.extended.qvmanifest.json, - exporting a signable manifest from an existing bundle uses
*.signable.qvmanifest.json.
Supported detached signature formats:
- Quantum Signer
.qsig(current supported detached format: major version 2, contextquantum-signer/v2) - Stellar WebSigner
.sigJSON documents with schemastellar-signature/v2, proof typesep53-message-signatureorxdr-envelope-proof
Detached-signature verification occurs during Attach (when importing external artifacts into a bundle) and during Restore (when evaluating archive authenticity policy).
| Artifact | Produced by | What it authenticates | Post-quantum | Can satisfy archive policy | Typical pin source |
|---|---|---|---|---|---|
.qsig |
Quantum Signer | Canonical manifest bytes | Yes | Yes | Bundled .pqpk or user-supplied .pqpk |
.sig |
Stellar WebSigner | Canonical manifest bytes | No, Ed25519 only | Yes for any-signature; never enough for strong-pq-signature by itself |
Bundled Stellar signer address or expected signer input |
.ots |
OpenTimestamps tooling | Detached signature bytes (linked by stamped SHA-256) | N/A | No | None; evidence only |
Supported signer pin sources:
bundlePinned: signer identity comes from material embedded in the manifest bundle (for example attached.pqpkor bundled Stellar signer address),userPinned: signer identity comes from restore-time user input (.pqpkor expected Stellar signer),signerPinned = bundlePinned || userPinned.
Archive policy satisfaction does not by itself require a signer pin. Pinning binds a verified detached signature to an expected signer identity when the bundle or the user supplies that identity.
| Policy level | Minimum requirement | Unsigned archive restorable | Ed25519-only signatures sufficient |
|---|---|---|---|
integrity-only |
No detached signature required | Yes | Yes, but not required |
any-signature |
minValidSignatures valid detached signatures |
No | Yes |
strong-pq-signature |
minValidSignatures valid detached signatures and at least one valid strong PQ detached signature |
No | No |
| Field | Meaning |
|---|---|
signatureVerified |
At least one detached signature verified successfully |
strongPqSignatureVerified |
At least one verified detached signature came from a strong PQ suite |
bundlePinned |
At least one verified signature matched signer material embedded in the bundle |
userPinned |
At least one verified signature matched signer material supplied by the user at restore time |
policySatisfied |
The selected archive authenticity policy accepted the available valid signatures |
Policy counting rules:
minValidSignaturescounts unique detached proof identities, not repeated verification results of the same proof,- semantically equivalent Stellar v2 proofs are deduplicated even if the JSON is serialized differently,
strong-pq-signaturerequires at least one valid strong PQ detached signature,- invalid extra signatures are reported but ignored for policy counting.
OpenTimestamps rules:
- timestamps are attached to detached signature bytes, not to the manifest bundle itself,
- OTS may be embedded inside the bundle or supplied externally as
.ots, - restore links OTS by stamped
SHA-256(detachedSignatureBytes), - if multiple OTS proofs target the same detached signature, restore reports one preferred evidence item and prefers apparently complete proofs,
- unrelated or ambiguous
.otsfiles fail closed, - current OTS handling parses and links stamped digests and reports whether a proof appears complete; it does not independently validate a full external timestamp attestation chain,
- OTS evidence never satisfies archive signature policy by itself.
- Receiver generates ML‑KEM‑1024 key pair (public
publicKey.qkey1568 B, privatesecretKey.qkey3168 B). - Sender:
{encapsulatedKey, sharedSecret} = ml_kem1024.encapsulate(publicKey). - Generate
kdfSalt(16 B) andcontainerNonce(12 B). - Derive keys with KMAC256:
Kraw = KMAC256(sharedSecret, (kdfSalt || metaBytes), { dkLen: 32, customization: domainStrings.kdf })Kenc = KMAC256(Kraw, [1], { dkLen: 32, customization: domainStrings.kenc })Kiv = KMAC256(Kraw, [2], { dkLen: 32, customization: domainStrings.kiv })- Import
Kencas AES‑GCM key. For per-chunk mode:prefix64 = KMAC256(Kiv, containerNonce, { dkLen: 8, customization: domainStrings.iv })IV_i = prefix64 || uint32_be(chunkIndex)(iv_strategy = kmac-prefix64-ctr32-v3).
- Key commitment:
keyCommitment = SHA3-256(Kenc)(required in current supported containers and shards).
- Single-container AEAD (small files):
- Build payload as
wrapped-v1(private metadata + file bytes). - Generate containerNonce (12B) random; call ciphertext = AES-GCM.encrypt(payload, iv=containerNonce, key=aesKey, AAD=headerBytes).
- Produce
.qencwith header (including key commitment) + ciphertext.
- Per-chunk AEAD (big files):
- Build payload as
wrapped-v1. - Break payload into chunks of
chunkSize. For each chunk index i computeIV_i = prefix64 || uint32_be(i)withprefix64 = KMAC256(Kiv, containerNonce, { dkLen: 8, customization: domainStrings.iv }). - Encrypt each chunk with AES‑GCM using
AAD_i = header || uint32_be(i) || uint32_be(plainLen_i). - Concatenate all
cipherChunk_ito form the ciphertext stream in.qenc.
- Read container bytes and ensure length >= minimal header size.
- Parse header fields (MAGIC, keyLen, encapsulatedKey, iv, salt, metaLen, metaJson). Validate
metaLenandkeyLenbounds. - Validate strict policy metadata (
cryptoProfileId, domain strings, nonce policy/mode/bounds, AEAD mode). sharedSecret = ml_kem1024.decapsulate(encapsulatedKey, secretKey); normalize toUint8Array.- Derive AES key with
KMAC256(sharedSecret, salt || metaBytes, { dkLen: 32, customization: domainStrings.kdf }), then deriveKenc/Kivwith their dedicated domain strings. Import key. Zeroize derived bytes andsharedSecret. - Verify the required
keyCommitmentbefore decryption. - Decrypt with AES-GCM providing
additionalData = header. If auth fails, raise an error (tampered container or wrong key). - If
payloadFormatiswrapped-v1, unpack private metadata and recover original file bytes. - If end-to-end payload integrity confirmation is needed, compute
SHA3-512(fileBytes)and compare it toprivateMeta.fileHashafter decryption. The current restore UI performs this check before exporting restored files.
- Split (.qenc → .qcont): parse the
.qencheader, Shamir‑split the ML‑KEM private key intonshares with thresholdt = k + (n-k)/2, Reed‑Solomon split the ciphertext intonfragments tolerating up to(n-k)/2erasures, and embed both the canonical archive manifest and the initial manifest bundle into every.qcontshard. - Attach (manifest/bundle + detached artifacts): verify external
.qsig/.sig, optionally bind exact signer identity with.pqpkor expected Stellar signer, attach.ots, and emit a self-contained manifest bundle. If a full shard cohort is loaded, the embedded bundle inside every shard can be rewritten in place without mutating canonical manifest bytes; otherwise Attach updates only the manifest-side bundle. - Combine (.qcont → .qenc + .qkey): parse all provided files, classify optional external manifest/bundle/signature/timestamp inputs, resolve archive context deterministically from the uploaded bundle, the uploaded canonical manifest, or embedded bundle preference logic (no “largest cohort wins”), verify commitments/hashes, evaluate signature policy, reconstruct private key via Shamir, reconstruct ciphertext via RS, rebuild
.qenc, and verifyqencHashfrom the canonical manifest before decrypt path is allowed. If multiple bundle cohorts share the same canonical manifest, a canonical manifest alone may be insufficient to disambiguate restore; provide the bundle file or signer pins in that case.
- Lattice-based key encapsulation mechanism, defined in FIPS-203. This algorithm, like other post-quantum algorithms, is designed to be resistant to attacks by quantum computers that could potentially break modern cryptosystems based on factorisation of large numbers or discrete logarithm, such as RSA and ECC. The ML-KEM-1024 provides Category 5 security level (roughly equivalent to AES-256) according to NIST guidelines.
- KMAC is a NIST-approved (SP 800-185) keyed algorithm based on KECCAK for MAC/PRF/KDF tasks. KMAC is considered a direct replacement for HMAC for the SHA-3 family.
- Using audited libraries for hashing and secret-sharing:
noble-hasheswas independently audited by Cure53, andshamir-secret-sharingwas audited by Cure53 and Zellic. Thenoble-post-quantumlibrary has not been independently audited at this time. - Using SHA3-512 for hash sums is in line with post-quantum security recommendations, as quantum computers can reduce hash cracking time from 2^n to 2^n/2 operations. Australian ASD prohibits SHA256 and similar hashes after 2030.
- There is no protection in JavaScript implementations of cryptographic algorithms against side-channel attacks. This is due to the way JIT compilers and rubbish collectors work in JavaScript environments, which makes achieving true runtime constancy extremely difficult. If an attacker can access application memory, they can potentially extract sensitive information.
- ML-KEM (Key Encapsulation Mechanism) does not check who sent the ciphertext. If you decrypt it with the wrong public key, it will simply return a different shared secret, not an error.
- Shamir's algorithm (SSS) provides information-theoretic security, which means that if there are less than a threshold number of shares, no information about the original secret can be obtained, regardless of computational power. Users need to independently ensure the reliability of storing each share.
- Inspired by diceslice and tidecoin.
| Topic | Source |
|---|---|
| ML-KEM-1024 | FIPS 203 |
| ML-DSA | FIPS 204 |
| SLH-DSA | FIPS 205 |
SHA-3 (SHA3-256, SHA3-512) |
FIPS 202 |
| KMAC256 | SP 800-185 |
| AES-256-GCM | SP 800-38D |
| Ed25519 | RFC 8032 |
| Stellar address encoding | SEP-0023 |
| OpenTimestamps project | opentimestamps.org |
npm install
npm run devnpm run buildnpm run selftestDeployment is handled by GitHub Actions (.github/workflows/pages.yml) on every push to main.
For a local Pages-equivalent build, use:
BASE_PATH=/quantum-vault/ npm run buildThis project is distributed under the terms of the GNU Affero General Public License v3.0. See the LICENSE file for the full text.
Browser encryption/decryption tool libraries:
- SHA3-512 for hashing and KMAC256 for KDF noble-hashes;
- ML-KEM-1024 for post-quantum key encapsulation used in combination with AES-256-GCM for symmetric file encryption noble-post-quantum;
- Shamir's secret sharing algorithm for splitting shamir-secret-sharing;
- Reed-Solomon erasure codes for splitting based on ErasureCodes.
The application incorporates the following dependencies that are released under the permissive MIT License and Apache License 2.0.
| Library | Version | Copyright holder | Upstream repository |
|---|---|---|---|
| shamir-secret-sharing | 0.0.4 | Privy | https://github.com/privy-io/shamir-secret-sharing |
| noble-post-quantum | 0.5.4 | Paul Miller | https://github.com/paulmillr/noble-post-quantum |
| noble-hashes | 2.0.1 | Paul Miller | https://github.com/paulmillr/noble-hashes |