Skip to content

Key Model

NOOSChain uses several different keys. They are intentionally separate: identity keys prove who is acting, data-encryption keys protect payload bytes, and key envelopes let authorized recipients recover those data-encryption keys.

This document is the canonical overview of those key roles, where each value is stored, and which operations are allowed to see sensitive material.

Key Inventory

Key or materialPurposeStored whereSecret?Consensus state?
User public keyResolves a transaction signer or API actor to a user.users.public_key, transaction envelopes, request headers.NoYes
User private keySigns transaction envelopes and unwraps envelopes addressed to the user.Client, wallet, CLI file, HSM, or browser session.YesNo
Organization public keyRecipient identity for organization-scoped key envelopes when configured.Organization identity state.NoYes
Organization private keyUnwraps envelopes addressed to the organization.Client, operator custody, HSM, or future policy service.YesNo
Node public keyAuthenticates node-to-node requests and node identity.nodes.public_key.NoYes
Node private keySigns gossip, sync, Raft/internal, and node transport messages.Node process secret storage.YesNo
Record DEKEncrypts one record payload in per_record_key buckets.Generated inside the API/KMS boundary for prepared writes; never persisted raw.YesNo
Bucket DEKEncrypts records in per_bucket_key buckets.Wrapped in bucket_keys.key_envelopes; never persisted raw.YesNo raw key; wrapped metadata is consensus state
Key envelopeEncrypted DEK for one recipient.encrypted_records.key_envelopes or bucket_keys.key_envelopes.Encrypted secretYes
Encrypted payloadAES-GCM ciphertext bytes.Local availability store and legacy encrypted_records.encrypted_payload.Sensitive ciphertextNo
Payload hashSHA-256 over plaintext payload bytes.encrypted_records.payload_hash, transaction payload.No, but may be correlatableYes

Identity And Signing Keys

User transactions are signed with Ed25519 keys. A transaction envelope contains signerPublicKey, nonce, createdAt, type, and payload; the signature is computed over the canonical envelope. The transaction hash is computed from the signed envelope.

During execution, NOOSChain resolves transaction.envelope.signerPublicKey to users.public_key. Payload fields such as createdByUserId and createdByOrganizationId are not authority by themselves; they must match the resolved signer user and organization unless the actor is the system/genesis actor.

The user private key is used for two separate client-side operations:

  • signing transaction envelopes;
  • unwrapping a key envelope so the client can decrypt payload bytes locally.

Consensus never needs a user private key. Production APIs should not receive user private keys. Development helpers may accept private keys for local test flows, but those helpers are not the intended production custody model.

User Key Rotation

ROTATE_USER_KEY updates users.public_key for the same logical user. The logical user nonce stream remains with the user identity, not with the raw key, so rotation does not reopen old nonce windows.

After rotation, new transactions must be signed by the new private key. Old records remain decryptable only if their key envelopes can be opened by a key the user still controls. Re-enveloping old records for a new user key is a separate future workflow; key rotation alone does not rewrite historical record or bucket-key envelopes.

Node Keys

Node keys identify NOOSChain nodes, not application users. Node private keys sign node-to-node requests such as transaction gossip, sync/backfill transport, and internal Raft/external sidecar callbacks. The receiver verifies the request against nodes.public_key and freshness headers.

Node identity does not automatically grant user bucket permissions. A node may receive or retain encrypted payload bytes only when node sync/backfill policy and bucket permissions allow it, for example through the node organization's bucket:read_encrypted or bucket:admin access. For node-to-node replication, the node must also present an organization-signed node membership credential and prove possession of the node private key; a configured organizationId is not trusted by itself.

Payload Encryption Keys

NOOSChain supports two bucket encryption modes.

per_record_key

Each record gets a fresh random 32-byte record DEK. In the public API write flow, the API receives plaintext, checks bucket:write, encrypts with AES-256-GCM and a fresh IV, derives recipients, and returns an exact ADD_ENCRYPTED_RECORD envelope for the user to sign. The record stores:

  • payload_hash: SHA-256 over plaintext bytes;
  • encryption_metadata: algorithm, IV, auth tag, and envelope algorithm;
  • key_envelopes: encrypted copies of that record DEK for recipients;
  • public_indexes: caller-supplied public query fields;
  • creator identifiers and transaction hash.

The raw record DEK is not persisted or returned to the SDK. The application API temporarily sees plaintext and the generated record DEK in memory unless the deployment moves encryption/wrapping into KMS/HSM.

per_bucket_key

The bucket has an active bucket DEK stored only as encrypted key envelopes in bucket_keys. In the public API write flow, the API unwraps the active bucket DEK using locally configured unwrap material or a future KMS/HSM hook, encrypts the plaintext, and returns an exact ADD_ENCRYPTED_RECORD envelope for the user to sign. Records store bucket_key_id and bucket_key_version. Record-level envelopes are normally empty because decryptors first unwrap the bucket key envelope and then decrypt the record payload.

CREATE_BUCKET_KEY creates the first active bucket key for a per_bucket_key bucket. ROTATE_BUCKET_KEY creates a new active version and marks the previous version as rotated. Existing records stay pinned to the key version used when they were created. Rotation is forward-only and does not re-encrypt historical payloads.

Key Envelopes

A key envelope is an encrypted DEK for one recipient. It lets the recipient recover a record DEK or bucket DEK without exposing the raw DEK in consensus state.

Envelope fields include:

  • recipientType: user or organization;
  • recipientId: logical recipient id;
  • recipientPublicKey: public key used for wrapping;
  • encryptedDekBase64: wrapped DEK bytes;
  • algorithm: wrapping algorithm.

The current implementation uses libsodium sealed boxes. NOOSChain identity keys are Ed25519, so the key-envelope adapter converts Ed25519 keys to Curve25519 inside the crypto helper before wrapping or unwrapping.

Recipient ids and recipient public keys are visible wherever envelopes are visible. That means envelope metadata reveals which users or organizations were included as recipients. The encrypted DEK remains protected unless the holder has the matching private key.

Recipient Selection

Recipient selection must come from bucket state, replication policy, and current access rules, not from untrusted browser input. The backend-assisted per_record_key flow derives record-DEK recipients from:

  • implicit bucket owner user and owner organization;
  • explicit bucket access rules with bucket:read_encrypted;
  • explicit bucket access rules with bucket:admin, because admin implies every bucket permission.

per_bucket_key bucket-key recipient validation also follows the bucket replication strategy for organization recipients:

  • metadata_only: organization bucket-key envelopes for node replication are rejected; user envelopes for authorized application actors may still exist.
  • replicate_encrypted_to_authorized_nodes: organization envelopes are accepted only for the creator organization or organizations with bucket:read_encrypted or bucket:admin.
  • replicate_encrypted_to_all_nodes: organization envelopes are accepted for organizations that own active registered nodes; runtime payload/key retrieval still requires an organization-signed node membership proof for the requesting node.

This is why normal public clients should not hand-author keyEnvelopes or ADD_ENCRYPTED_RECORD internals. If the client could choose the envelope list freely, it could omit authorized principals or include recipients that the bucket replication/access policy does not allow. CREATE_BUCKET_KEY and ROTATE_BUCKET_KEY validate every submitted bucket-key envelope recipient before the key version is accepted, and public ADD_ENCRYPTED_RECORD submissions must match a short-lived API-prepared envelope exactly.

Access permission and decryptability are related but not identical:

  • permission controls whether APIs return metadata or encrypted payload bytes;
  • key envelopes control whether a holder can actually unwrap the DEK;
  • local availability controls whether ciphertext bytes are present on this node.

All three must line up for successful local decryption.

Encrypted Record Write Flow

mermaid
sequenceDiagram
  participant Browser
  participant SDK
  participant API
  participant Chain

  Browser->>SDK: plaintext, bucket id, public indexes, signer
  SDK->>API: POST /buckets/:id/records/prepare-add-encrypted-record
  API->>Chain: authenticate actor and require bucket:write
  API->>Chain: verify local node is eligible under bucket replication policy
  alt per_record_key
    API->>API: generate record DEK, encrypt payload, derive/wrap recipients
  else per_bucket_key
    API->>API: unwrap active bucket DEK locally/KMS, encrypt payload
  end
  API->>API: store short-lived hash of exact unsigned envelope
  API-->>SDK: preparedTransactionId and exact ADD_ENCRYPTED_RECORD envelope
  SDK->>SDK: sign exact prepared envelope
  SDK->>API: POST /transactions with preparedTransactionId and signed tx
  API->>API: verify signature and exact prepared-envelope hash
  API->>Chain: mempool, consensus, deterministic execution

The signed ADD_ENCRYPTED_RECORD consensus payload stores the plaintext hash, encryption metadata, public indexes, creator identity, and either record-level key envelopes (per_record_key) or a bucket-key id/version reference (per_bucket_key). The public API requires a preparedTransactionId for ADD_ENCRYPTED_RECORD submission and rejects altered, expired, reused, or unprepared envelopes. The prepared-envelope hash covers the full unsigned envelope, including encryptedPayload, even though ciphertext bytes remain availability-layer data and are omitted from the canonical transaction hash.

The API sees plaintext in this default prepared write flow. A configured local node must be eligible to store payloads for the bucket before the prepare route accepts plaintext. The SDK never receives raw record DEKs, raw bucket DEKs, bucket-key envelopes for write preparation, or user private keys.

The high-level TypeScript SDK helpers for this are:

  • client.transactions.addEncryptedRecordFromPlaintext(...);
  • client.transactions.submitAddEncryptedRecordFromPlaintext(...).

Payload Decryption Flow

mermaid
sequenceDiagram
  participant Client
  participant API
  participant Chain

  Client->>API: GET /buckets/:id/records/:recordId
  API->>Chain: require bucket:read_metadata
  API-->>Client: record metadata, encryption metadata, record envelopes, bucket key reference
  Client->>API: GET /records/:recordId/payload
  API->>Chain: require bucket:read_encrypted
  API-->>Client: encrypted payload bytes and, for per_bucket_key, bucket-key envelopes
  alt per_record_key
    Client->>Client: find record DEK envelope matching public key
    Client->>Client: unwrap record DEK with private key
  else per_bucket_key
    Client->>Client: find bucket-key envelope matching public key
    Client->>Client: unwrap bucket DEK with private key
  end
  Client->>Client: decrypt AES-GCM payload and verify auth tag

For application reads, /records/:recordId/payload is the encrypted-read gate: it returns ciphertext only when the actor has bucket:read_encrypted or bucket:admin and the source node has the payload locally. For node replication, the same endpoint may return ciphertext and per-bucket-key envelope metadata only when verified node membership and the bucket replication strategy allow that node organization to store the payload. Unauthorized node payload responses must not include ciphertext, encryption metadata, key envelopes, or bucket-key references beyond the minimum miss record.

Common failures:

  • no bucket:read_metadata: metadata query is denied;
  • no bucket:read_encrypted: payload query is denied or returns no ciphertext;
  • payload unavailable locally: metadata exists but ciphertext is absent on this node;
  • no matching envelope: the caller is permitted or can see metadata, but no record DEK or bucket DEK was wrapped for the private key they hold;
  • wrong private key: envelope unwrap fails;
  • tampered ciphertext or metadata: AES-GCM authentication fails.

Storage And Consensus Boundary

Plaintext payload bytes and raw DEKs are not consensus state. Consensus and replay depend on deterministic metadata:

  • record id and bucket id;
  • payload_hash;
  • encryption metadata;
  • normalized key envelopes;
  • bucket-key id/version when applicable;
  • public indexes;
  • creator identifiers;
  • transaction hash.

encrypted_payload bytes are availability-layer data. Different nodes may legitimately have different local ciphertext availability while sharing the same canonical blocks and state roots. Payload backfill and pruning update local availability; they do not rewrite consensus state.

For node replication of per_bucket_key buckets, bucket-key envelope sharing uses the same replication boundary as ciphertext placement. An authorized replication node can receive the bucket-key envelope metadata needed for the record version it is allowed to store; an unauthorized node may still sync block and record metadata but must not receive ciphertext or bucket-key material through replication APIs.

Prepared Writes And KMS Options

Current browser/API writes use prepared ADD_ENCRYPTED_RECORD envelopes. The API receives plaintext, performs encryption and recipient/key-reference derivation, stores a short-lived exact-envelope hash, and returns the unsigned envelope for the user to sign. The backend does not receive the user's private key, but it temporarily sees plaintext and, for per_record_key, the generated record DEK.

A KMS/HSM-backed production design can preserve server-derived recipient lists without exposing wrapping key material to application memory:

  • API derives authorized recipients or active bucket-key version;
  • API asks KMS/HSM to generate/wrap a record DEK or unwrap/use a bucket DEK;
  • application code receives only ciphertext, envelopes/references, and non-secret metadata needed for the transaction envelope.

This changes custody mechanics but not the consensus model: transactions still store encrypted envelopes and never store raw DEKs.

Security Guarantees And Caveats

NOOSChain guarantees:

  • transaction authority comes from signer public keys resolved to identities;
  • private keys are not required for consensus or replay;
  • plaintext payloads are not stored as consensus state;
  • raw DEKs are not persisted in ordinary chain tables;
  • bucket:admin implies other bucket permissions;
  • bucket:read_encrypted gates encrypted payload API access;
  • encrypted payload availability is separate from canonical record state.

Current caveats:

  • development helper APIs may accept plaintext or private keys and are not production custody patterns;
  • prepared writes currently let the application backend see plaintext and generated per-record DEKs unless encryption is moved behind KMS/HSM;
  • envelope metadata reveals recipient ids and recipient public keys;
  • user key rotation does not automatically re-envelope historical DEKs;
  • access removal from a per_bucket_key bucket requires key rotation to exclude the removed organization from future bucket-key versions; old versions may remain decryptable by old envelope recipients;
  • removing bucket:read_encrypted blocks future API reads but does not physically delete ciphertext already stored locally until pruning/GC runs.

Audience-first NOOSChain documentation.