Decentralized. Client‑encrypted. Effortless. Built on the Internet Computer Protocol (ICP), GhostKeys lets humans and machines store secrets with no logins, no servers, and no plaintext ever leaving your device.
GhostKeys is a universal secrets vault for both people and programs:
- Client‑side encryption by default: the UI encrypts everything before it ever touches the network.
- Zero friction: land on the app and start typing; optional sync to the ICP later.
- Scales from solo to enterprise: free‑tier users share a multi‑tenant canister; premium users get canister‑per‑user isolation.
- Agent‑friendly: designed so local or hosted AI agents can read/write without ever receiving raw keys.
For a feature‑oriented overview and historical context, see updated CHANGELOG.
GhostKeys is split into three logical parts:
- Entry point for the whole system. Frontend talks to Factory first.
- Spawns new dedicated Vault canisters for premium users and provides access to a pre‑created Shared Vault canister for free tier.
- Pulls Vault and Shared‑Vault WASM/DID artifacts from GitHub Releases at build time (via a shell script) instead of embedding binaries.
- Repo: https://github.com/Ghostkeys-App/factory-canister
-
Stores only ciphertext and minimal metadata for:
- Website logins (sites → entries of key/value pairs)
- Secure notes (title, ciphertext body)
- Flexible grid (spreadsheet semantics; per‑column
secret/plainhint)
-
Uses stable memory (
ic-stable-structures) and is upgrade‑friendly. -
Supports vetKD flows for verifiable encryption to derived keys.
- React/TypeScript UI with an offline‑first model (IndexedDB) and optional sync to the ICP.
- Actor bootstrap is factory‑first; the Factory reveals IDs for Vault/Shared‑Vault and the UI then creates those actors.
- UX modules include Website Logins, Secure Notes, Spreadsheet, and Profile/Seed flows.
This section explains how GhostKeys derives identities and keys and how it encrypts/decrypts data. All cryptography happens client-side; only ciphertext and public parameters ever touch the network.
Mnemonic Generate a BIP-39 mnemonic using the 2048-word English list, 128-bit entropy → 12 words.
Seed material (64 bytes) Apply a KDF to the mnemonic to obtain a deterministic 64-byte seed (IKM).
Deterministic private key (SLIP-0010)
Derive a key using SLIP-0010 with path m/44'/223'/0'/0'/0' from the 64-byte IKM.
Identity (Ed25519) Convert the derived secret to an Ed25519 keypair and produce the ICP Principal from the public key.
On login, the same mnemonic deterministically reproduces the same identity. The mnemonic is never stored or transmitted.
Derive a per-user VaultKey that is stable across sessions:
- Compute/obtain a vault public key using the same SLIP-0010 strategy as identity (or a designated vault path).
- Concatenate an application salt with the vault public key and hash with SHA-256:
VaultKey := SHA256( SALT || vault_public_key_bytes ) // 32 bytes
Obtain a second key using DFINITY’s verifiable encryption to derived keys (vetKD):
- Scope: default is
PerUser, using the caller’s Principal. - Input: build a context string
ctx = "SALT:ID:PURPOSE:ROTATION_ID"
input32 := SHA256( utf8(ctx) ) // 32 bytes
- Transport keypair: generate cryptographic randomness (e.g., via the vetKD/DFINITY randomness flow) and derive an ephemeral transport keypair; keep the private part client-side, send the
transport_public_key. - Request: send
{ scope, input: input32, transport_public_key }to the canister. - On-chain derivation: the canister derives key material under the given scope using BLS12-381 G2 and returns it encrypted to your
transport_public_key. - Client decrypt: decrypt the response with your transport private key to yield:
VetKey // 32 bytes
ROTATION_ID enables key rotation without changing identity (increment to rotate VetKey).
Mix both keys into the single key used for data encryption:
Salt:
finalSalt := SHA256( VAULT_SALT || USER_SALT )
HKDF (SHA-256):
IKM := VaultKey || VetKey // deterministic concatenation
PRK := HKDF-Extract(finalSalt, IKM)
FinalKey := HKDF-Expand(PRK, info="ghostkeys/v1/aes-gcm", L=32) // 32 bytes (AES-256)
- Algorithm: AES-256-GCM with a 12-byte (96-bit) random nonce per message.
- Input: UTF-8 plaintext.
- Output:
{ nonce(12), ciphertext, auth_tag(16) }. - Encoding: store/transport as Base64 (or Base64-URL) fields.
- Optional AAD: include stable metadata (e.g., user principal, record type) as AAD to bind ciphertext to context.
Nonce uniqueness is critical: never reuse a (FinalKey, nonce) pair.
Re-derive FinalKey from the mnemonic (or logged-in identity) → verify GCM tag → decrypt using the stored nonce and recover the plaintext.
- No plaintext ever leaves the client.
- Canisters store only ciphertext, nonces, tags, and public parameters.
- Keys are never logged or persisted in plaintext; rotation is achieved via
ROTATION_IDand/or salts. - All derivations are deterministic from the seed + context, so users can recover access from their mnemonic alone.
A high‑level sketch (see the Vault repo for the authoritative Candid):
VaultData {
vault_name: text
website_logins: vec { (site_name: text, entries: vec { (k: text, v: text) }) }
secure_notes: vec { (title: text, body_ciphertext: text) }
flexible_grid_columns: vec { (label: text, (index: nat32, is_secret: bool)) }
flexible_grid: vec { ((row: nat32, col: nat32), cell_ciphertext: text) }
}
UI encrypts per‑field using AES‑GCM (or equivalent) derived from seed/vetKD; the canister validates types and persists opaque blobs.
- Node.js (LTS), Rust toolchain, DFX ≥ 0.17
- Rust target:
rustup target add wasm32-unknown-unknown# 1) Clone and install
git clone https://github.com/Ghostkeys-App/ghostkeys-app.git
cd ghostkeys-app
npm install
# 2) Start replica
dfx start --clean --background
# 3) Deploy (factory + frontend, depending on workspace)
dfx deployAfter deploy, ensure your environment exposes the Factory canister ID. Many setups export CANISTER_ID_FACTORY_CANISTER_BACKEND, which the frontend uses to create the first actor.
We rely on remote release artifacts (WASM/DID) so local repos can stay lightweight. In dfx.json for custom canisters, you’ll typically see entries like:
{
"canisters": {
"factory-canister-backend": {
"type": "custom",
"wasm": "https://github.com/Ghostkeys-App/factory-canister/releases/download/<TAG>/factory_canister_backend.wasm",
"candid": "https://github.com/Ghostkeys-App/factory-canister/releases/download/<TAG>/factory-canister-backend.did",
"metadata": [{ "name": "candid:service" }],
"post_install": ["dfx generate factory-canister-backend"]
}
}
}The Factory repo includes a helper script,
scripts/fetch_vault_release.sh, which downloads the latest Vault/Shared‑Vault WASM/DID from the Vault repo’s releases and falls back to generating DIDs viacandid-extractorif missing. You can override the default release tag withVAULT_RELEASE_TAG=<tag>.
Frontends should always create the Factory actor first, then discover other IDs:
// sketch
const factory = await getFactoryCanisterAPI();
const { sharedVaultId, userVaultId } = await factory.get_shared_vault_or_user_vault(...);
const sharedVault = createActor(sharedVaultId);
const userVault = createActor(userVaultId);The UI will keep a single source of truth in a Vault context (IndexedDB‑backed) and expose actions like saveToIdb, syncFromIc, and reloadFromIc. A Reload button in the sidebar stays disabled until the vault exists on chain.
A detailed, commit‑grouped summary (UI/UX, state/IDB, IC integration, crypto, and policies) is maintained in CHANGELOG.md.
Phase 1: MVP (Complete)
- Local‑first vault with structured grid
- Create/edit/delete rows and columns (secret/plain)
- Deployed on local DFX
Phase 2: Zero‑Friction UX & Personal Canister Sync (Complete)
- New UI and Zero-Friction UX
- Factory canister for vault management
- Derive per‑user keys from signatures
- Stable memory in per‑user canisters
- Auto‑save to IndexedDB; manual sync button
Phase 3: Developer Templates & SDK
- Template gallery, iFrame/SDK, CLI, import/export, agent integration
Phase 4+: Teams & Enterprise
- RBAC, sharing, audit logs, rotation templates; agent‑assist ops
TBD (proprietary while pre‑launch).

