Building Keeva: Client-Side Encryption and Persistent Vault State with Tatum and Walrus Memory
DEV Community

Building Keeva: Client-Side Encryption and Persistent Vault State with Tatum and Walrus Memory

Separating identity, encryption, storage, and blockchain reads

Keeva does not treat "connected wallet" as a single permission that unlocks everything. Instead, the application separates several responsibilities:

  • Sui wallet: Supplies the connected account and personal-message signatures for authorization and supported key-derivation flows.
  • Browser cryptography: Derives vault keys, encrypts records, authenticates ciphertext, and decrypts authorized data.
  • Walrus Memory SDK: Authenticates delegated storage requests and persists or retrieves encrypted envelopes.
  • Tatum integration: Checks Sui Mainnet identity, reads network telemetry, and supplies transaction/checkpoint evidence.
  • Vault projection: Reconstructs current credentials from validated immutable revisions.
  • Local browser storage: Retains encrypted gateway profiles, ciphertext caches, pending encrypted operations, and continuity checkpoints.

This separation matters because identifying a wallet does not automatically provide the correct storage configuration or authorize decryption. A user can connect the correct wallet while selecting the wrong memory account or namespace. Conversely, a valid storage delegate does not mean the application should expose credential plaintext without the vault's unlock material. Keeva checks these concerns separately.

Encrypting before data reaches Walrus Memory

Keeva uses the browser's Web Crypto API for its application-level encryption. The current implementation uses:

  • AES-256-GCM for authenticated encryption.
  • A fresh, randomly generated 96-bit IV for each encryption operation.
  • A 128-bit authentication tag.
  • PBKDF2-SHA256 with a minimum of 600,000 iterations.
  • A random 32-byte salt for key derivation.
  • Non-extractable Web Crypto keys.

AES-GCM authenticates the encrypted content and its associated context. A damaged ciphertext, incorrect key, or mismatched context causes decryption to fail rather than producing data that the application accepts.

The core operation is:

const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
  {
    name: 'AES-GCM',
    iv,
    additionalData: utf8.encode(context),
    tagLength: 128,
  },
  key,
  utf8.encode(JSON.stringify(record)),
);

The context is important. Keeva binds encrypted records to identifiers such as the memory account, namespace, vault, credential, and revision. A ciphertext should not become a valid record merely because someone places it in another storage location.

Protecting the whole credential

Encrypting only a password would still expose useful information about the user: service names, usernames, notes, categories, and rotation dates. Keeva therefore encrypts the secret field and then encrypts the complete credential record - including its metadata, revision relationship, and audit event - in an outer envelope.

This layering is not a claim that "encrypting twice" independently doubles security. Its purpose is to give the secret and complete record explicit authenticated contexts. Security still depends on correct cryptographic use, key protection, and trusted application delivery.

Walrus Memory receives the serialized encrypted envelope, not the plaintext credential body. The SDK's own SEAL and storage processing is an additional layer, separate from Keeva's browser-side encryption.

Two distinct unlock boundaries

One practical challenge was allowing a returning user to recover their configuration without confusing that operation with unlocking their passwords. Keeva separates:

  1. Unlocking the saved gateway configuration.
  2. Unlocking the encrypted vault.

The gateway configuration includes the Tatum endpoint and API key, Walrus Memory account and delegate settings, and original namespace. Restoring these settings makes the application capable of contacting the correct services. It does not itself expose the vault's credentials.

Wallet-protected gateway profiles

Each wallet can have its own encrypted configuration stored on the current device. For supported wallets, Keeva derives the configuration-encryption key from a verified deterministic Ed25519 personal-message signature, using PBKDF2. The derivation message includes:

  • The application origin.
  • Sui Mainnet identity.
  • The wallet address.
  • The profile format and KDF parameters.
  • A random per-save salt.

The signature and derived encryption key are not persisted with the encrypted profile. The signature is sensitive: it participates in deriving the key that protects saved API and delegate credentials. It should never be published or included in logs, screenshots, or demonstration recordings.

Vault unlock methods

The vault itself supports two modes:

  • Passphrase-derived encryption, accompanied by a fresh wallet authorization signature.
  • Wallet-derived encryption, using a vault-specific deterministic Ed25519 personal-message signature.

The vault header records the selected method. Reopening the vault requires that original method; Keeva does not silently switch between them. Wallets without the required deterministic signature scheme can use passphrase protection instead.

Recovering the correct vault after a wallet reconnects

The recovery problem is not simply "download everything again." The application first needs to recover the correct relationship between:

wallet → memory account → namespace → vault header → encrypted revisions

Keeva's returning-user flow is:

  1. Connect the owning Sui Mainnet wallet.
  2. Explicitly unlock that wallet's saved gateway profile.
  3. Restore its original account, namespace, and gateway credentials.
  4. Inspect existing storage.
  5. Authorize the vault unlock.
  6. Authenticate and reconstruct the encrypted inventory.

Once valid configuration and a Mainnet wallet are available, inspection runs automatically. This inspection is read-only: it does not initialize a vault, create memory records, or prompt for a wallet signature. The header is checked against the configured account, namespace, and connected wallet. Conflicting headers or mismatched identities stop the operation.

Switching between wallets

Consider a browser that has saved profiles for wallets A and B. After A's vault has been unlocked, switching to B must not leave A's credentials visible while B's configuration loads. Keeva invalidates the previous session and clears active gateway secrets when the wallet changes. B must restore its own configuration and unlock its own vault. Switching back to A restores A's profile rather than reusing B's most recently selected namespace.

This behavior was exercised in a controlled browser review: A's inventory returned, B exposed only its own inventory, and switching back restored A without additional mock writes.

A wallet is not a complete backup

Saved gateway profiles are specific to a browser profile and application origin. For example, localhost, 127.0.0.1, different ports, and a hosted domain have separate browser storage. On a fresh device, the user still needs the original gateway configuration and vault unlock material. Keeva does not claim that a public wallet address alone can discover and decrypt every previously created vault.

Using Walrus Memory without plaintext semantic search

Walrus Memory supports memory retrieval, but a credential vault has a different requirement from a conversational assistant. An assistant may retrieve a few semantically relevant memories. A vault must discover the records required to reconstruct its complete available history. Omitting a revision can change which password appears current. Keeva therefore does not use a capped similarity result as its inventory.

Paginated inventory discovery

The storage service first calls listMemories() and follows pagination. It checks for:

  • Inventory resynchronization requirements.
  • Valid blob identifiers.
  • Advancing cursors.
  • Records within the configured root or revision namespaces.

An index inconsistency fails synchronization rather than being presented as an empty vault.

Immutable revision namespaces

The root namespace contains the vault header. Each immutable record uses a distinct namespace beneath it:

<root>.r.<record-uuid>

Keeva retrieves an indexed record within its namespace using a constant discovery phrase, then matches the result to the exact expected blob ID. User-entered search terms are not sent to Walrus Memory. Credential metadata is decrypted and searched locally in the browser.

This deliberately trades server-side semantic understanding for keeping credential content private. It does not hide all metadata. Account identifiers, namespaces, ciphertext sizes, timing, and access patterns remain observable. Ciphertext and the constant discovery phrase may still pass through the Memory service's processing pipeline.

What Tatum contributes

Tatum's role is blockchain access and evidence - not credential encryption. Keeva supports configured Sui Mainnet GraphQL and JSON-RPC interfaces. The endpoint and interface must match what the provider actually supports.

In GraphQL mode, network telemetry includes:

query KeevaNetwork {
  chainIdentifier
  checkpoint {
    sequenceNumber
    networkTotalTransactions
    epoch {
      epochId
      referenceGasPrice
    }
  }
}

Keeva validates the returned chain identity against Sui Mainnet's 35834a8a identifier. A wallet advertising Mainnet support is not sufficient if the configured blockchain endpoint points elsewhere. Returned fields are schema-validated. GraphQL errors, partial results, incompatible data, and non-advancing pagination are not accepted as successful readings.

A read-only backend boundary

Browser requests reach Tatum through Keeva's same-origin:

POST /api/tatum

The Node backend forwards the selected endpoint, API interface, key, and permitted blockchain request. The backend restricts supported operations, bounds request and response sizes, rejects redirects, and applies an upstream deadline. It also handles limited rate-limit retries. The application does not send master keys or plaintext credentials through this route. However, the backend necessarily receives the Tatum API key for forwarding, so it remains part of the gateway trust boundary.

Transaction verification and its limits

Keeva can inspect an existing transaction by:

  • Retrieving its transaction bytes and signatures.
  • Recomputing its digest locally.
  • Verifying sender and sponsor signatures using the Sui SDK.
  • Checking the expected wallet relationship.
  • Requesting membership in the reported checkpoint.

These checks have different strengths. Digest and signature verification are local cryptographic checks. Checkpoint membership, execution status, and event contents remain provider-attested. Keeva does not independently verify a validator-certificate proof. Accordingly, the application records a checked transaction as transaction.checked; it does not turn that result into a claim that an unspecified sharing or recovery contract executed successfully.

Reconstructing state from immutable history

Password rotation creates a new revision instead of overwriting the previous stored record. Each credential revision carries:

  • A stable credential ID.
  • A unique revision ID.
  • Its parent revision ID.
  • Creation and update timestamps.
  • An authenticated audit event.

After decrypting records, Keeva validates revision ancestry. Missing ancestors and cycles are rejected. If two devices create different descendants of the same revision, the application exposes the concurrent heads as a conflict. It does not silently decide that the record with the newest timestamp must be correct. The current implementation detects such conflicts and blocks unsafe overwrites. Automated conflict reconciliation is not yet implemented.

Handling an uncertain write

A storage request can succeed even when its response is lost. Retrying with freshly generated ciphertext and a new identifier could create an unintended extra revision or incur another paid operation. Before dispatching a write, Keeva retains the encrypted request and its idempotency identifier locally. An explicit retry reuses that original operation. This is a client-side idempotent retry strategy, not an unconditional guarantee of exactly-once storage under every network or service failure.

Reconnecting or unlocking does not automatically replay paid writes. Browser Web Locks also serialize writes across same-origin tabs. This reduces local races but does not create a global lock across devices.

How memory changes Keeva over time

The hackathon asks how a product learns over time. For Keeva, the accurate answer is persistent, evolving security state - not machine learning. Keeva does not train on passwords or claim to be an autonomous security agent. Instead, stored history changes the state reconstructed in later sessions.

An illustrative workflow is:

A user stores a credential. A local audit identifies password reus

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.