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 material | Purpose | Stored where | Secret? | Consensus state? |
|---|---|---|---|---|
| User public key | Resolves a transaction signer or API actor to a user. | users.public_key, transaction envelopes, request headers. | No | Yes |
| User private key | Signs transaction envelopes and unwraps envelopes addressed to the user. | Client, wallet, CLI file, HSM, or browser session. | Yes | No |
| Organization public key | Recipient identity for organization-scoped key envelopes when configured. | Organization identity state. | No | Yes |
| Organization private key | Unwraps envelopes addressed to the organization. | Client, operator custody, HSM, or future policy service. | Yes | No |
| Node public key | Authenticates node-to-node requests and node identity. | nodes.public_key. | No | Yes |
| Node private key | Signs gossip, sync, Raft/internal, and node transport messages. | Node process secret storage. | Yes | No |
| Record DEK | Encrypts one record payload in per_record_key buckets. | Generated inside the API/KMS boundary for prepared writes; never persisted raw. | Yes | No |
| Bucket DEK | Encrypts records in per_bucket_key buckets. | Wrapped in bucket_keys.key_envelopes; never persisted raw. | Yes | No raw key; wrapped metadata is consensus state |
| Key envelope | Encrypted DEK for one recipient. | encrypted_records.key_envelopes or bucket_keys.key_envelopes. | Encrypted secret | Yes |
| Encrypted payload | AES-GCM ciphertext bytes. | Local availability store and legacy encrypted_records.encrypted_payload. | Sensitive ciphertext | No |
| Payload hash | SHA-256 over plaintext payload bytes. | encrypted_records.payload_hash, transaction payload. | No, but may be correlatable | Yes |
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:userororganization;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 withbucket:read_encryptedorbucket: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
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 executionThe 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
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 tagFor 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:adminimplies other bucket permissions;bucket:read_encryptedgates 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_keybucket 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_encryptedblocks future API reads but does not physically delete ciphertext already stored locally until pruning/GC runs.