Seal data and erase a person with keys
Seal a value with a per-user key held in your cloud KMS, open it with an audited purpose, rotate keys, and destroy one person's key to erase them everywhere.
Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
You will seal a note for one user, open it, then destroy that user's key so the note can never be read again. It takes about 20 minutes. Most apps never call keys directly: a db field with encrypt: true uses it for you. Use it when you seal a value yourself or need crypto-erasure.
You need
- A MainSDK app with
auth. - For production, a KMS key per region. In development the
localdriver needs no account.
1. Point keys at the KMS
keys:
driver: aws-kms
key:
us: arn:aws:kms:us-east-1:111122223333:alias/pinecart-us
eu: arn:aws:kms:eu-west-1:111122223333:alias/pinecart-eu
per-user: true
rotate: 365dThe local driver is refused in production with MAIN-KEYS-0001.
2. Seal a value
import { keys } from "mainsdk";
export async function sealNote(note: string, userId: string) {
return keys.encrypt(note, { owner: userId, region: "eu", label: "phi" });
}The label is one of personal, sensitive, child or phi.
3. Open it with a purpose
import { keys } from "mainsdk";
import type { Sealed } from "mainsdk/keys";
export async function openNote(sealed: Sealed) {
return keys.decrypt(sealed, { purpose: "show note to owner" }); // Uint8Array
}The purpose and key id go to the audit log.
4. Rotate
import { keys } from "mainsdk";
import type { Sealed } from "mainsdk/keys";
export async function rotate(sealed: Sealed) {
const report = await keys.rotate(); // or keys.rotate({ keyId: "user:u1" })
const moved = await keys.rewrap(sealed, { purpose: "move to the new key version" });
return { report, moved };
}Old data still decrypts after a rotation. rewrap wraps the data key again and leaves the ciphertext alone.
5. Erase a person
import { keys } from "mainsdk";
export async function erase(id: string) {
return keys.destroyUser(id); // { keyId, scope, destroyedAt, versions }
}6. Test it
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { keys } from "mainsdk";
it("keys AC3 makes a destroyed user's data unreadable", async () => {
createTestApp({ yaml: "schema: 1\nproject: prj_test\nkeys:\n driver: local\n per-user: true\n" });
const sealed = await keys.encrypt("note", { owner: "u1", region: "eu", label: "phi" });
await keys.destroyUser("u1");
await assert.rejects(keys.decrypt(sealed, { purpose: "test" }), { code: "MAIN-KEYS-0004" });
});Check it worked
The test passes, and reading the note after destroyUser fails with MAIN-KEYS-0004.
Troubleshooting
MAIN-KEYS-0003: a bad region, no KMS key for the region, ordecryptwithout apurpose.MAIN-KEYS-0005: the key service did not answer. Check the app may use the KMS key, then retry.MAIN-KEYS-0006: the sealed value failed its integrity check. It was changed or belongs to another key.
See the keys interface.
Was this page helpful?