Encryption workflow
A streamlined guide to the encryption workflow, including key generation, encryption process, and JWE construction.
Developers best subscribe to the tap.requirePresentationRequested webhook and disable Notabene managed encryption in the Dashboard Settings. The diagram below illustrates the flow of encrypted PII through the Notabene network: The originator entity requests the beneficiary entity's encryption key from the DID Document, encrypts the PII and sends it to Notabene’s PII server. The receiving entity later pulls and decrypts the data using its private key stored in its own key store:

P-256 Key Generation
P-256 keys are generated using the @noble/curves/p256 library and stored in Veramo. Here's a concise guide:
// Generate P-256 key pair (from p256-ecdh.ts)
const privateKeyBytes = p256.utils.randomPrivateKey(); // 32 random bytes
const publicKeyBytes = p256.getPublicKey(privateKeyBytes, false); // Uncompressed format
// Import into Veramo keystore
const importedKey = await agent.keyManagerImport({
privateKeyHex: bytesToHex(privateKeyBytes),
publicKeyHex: bytesToHex(publicKeyBytes),
type: "Secp256r1",
kms: "local",
});Add the generated public key to the key agreements in your DIDDoc (see here. This key generation is typically done once and refreshed on a schedule.
Encryption Process
The encryption process starts when a tap.requirePresentationRequested webhook is received from Notabene. The payload includes the counterparty’s encryption key and a callback URL for the encrypted presentation.
Step 1: Key Retrieval and Validation:
Upon receiving a request for PII, a webhook provides:
- Beneficiary agent ID
- Callback URL
- Counterparty's encryption key details
{"message":"tap.requirePresentationRequested",
"payload":{"beneficiaryAgentID":"did:web:notabene.id:io",
"beneficiaryPresentationDefinition":"https://pd.notabene.id/ivms101/v2/FA-1000.json","callbackUrl":"/entities/did:web:notabene.id:test:dk/transfers/b0d9a75e-aa7d-4ec1-9b56-f4a3e1221185/policies/57214806-58f3-4246-b93e-2bad9b223d58/presentation","encryptionKey":{"controller":"did:web:notabene.id:io","id":"did:web:notabene.id:io#f7b6b6d3c8d55eadff413d9ef6d4a502c72d73273ede5fe5eff9fa393c52262b","publicKeyBase58":"HfyJ6KTuDMHVfPiBoVhFHt1RsZvv8gYmJnSMGEZvkn5g","type":"X25519KeyAgreementKey2019"},"for":"did:web:notabene.id:test:dk","id":"b0d9a75e-aa7d-4ec1-9b56-f4a3e1221185","originatorAgentID":"did:web:notabene.id:test:dk","originatorPresentationDefinition":"https://pd.notabene.id/ivms101/v2/DK-0.json","policyId":"57214806-58f3-4246-b93e-2bad9b223d58"},"version":"1.0.0"}Alternatively, retrieve the key from the hosted DID Doc via a GET request to https://api.eu1.notabene.id/entities/{{entityDID}}/public-keys - reference.
Step 2: Ephemeral Key Generation
Generate a fresh P‑256 key pair for each presentation. The private key is non‑exportable for security.
// Generate ephemeral P-256 key pair using Web Crypto API
const ephemeralKeyPair = await wc.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
false, // private key non-exportable for security
["deriveBits"],
);
// Export ephemeral public key for JWE header
const ephemeralPublicJwk = await wc.subtle.exportKey(
"jwk",
ephemeralKeyPair.publicKey,
);Step 3: ECDH Key Agreement
Import the recipient’s public key and perform ECDH to derive a shared secret.
// Import recipient's public key
const recipientPublicKey = await wc.subtle.importKey(
"jwk",
recipientP256.jwk,
{ name: "ECDH", namedCurve: "P-256" },
false,
[],
);
// Derive shared secret
const sharedSecretBits = await wc.subtle.deriveBits(
{ name: "ECDH", public: recipientPublicKey },
ephemeralKeyPair.privateKey,
256, // 256 bits for P-256
);Step 4: Key Derivation (ConcatKDF)
Expand the shared secret into a 256‑bit AES key using ConcatKDF. In Notabene’s implementation you may set:
APU – SHA‑256 hash of your DID (identifies the sender).
APV – SHA‑256 hash of the recipient key identifier (identifies the recipient).
// Privacy-preserving bindings (optional)
let apu = null, apv = null, ctx = null;
if (senderDid) {
apu = new Uint8Array(
await wc.subtle.digest("SHA-256", new TextEncoder().encode(senderDid)),
);
}
if (recipientKeyId) {
apv = new Uint8Array(
await wc.subtle.digest("SHA-256", new TextEncoder().encode(recipientKeyId)),
);
}
// Derive CEK using RFC 7518 ConcatKDF
const cek = await concatKdf(sharedSecret, {
algId: "A256GCM",
apu: apu, // SHA-256(senderDid) or null
apv: apv, // SHA-256(recipientKeyId) or null
keyDataLenBits: 256,
});Step 5: JWE Construction and AES-GCM Encryption
Compose a JWE protected header specifying alg: "ECDH-ES" and enc: "A256GCM", include the encoded ephemeral public key and (if used) the base64url‑encoded apu and apv. Then generate a 96‑bit initialization vector (IV), encrypt the IVMS payload with AES‑256‑GCM and assemble the compact JWE:
// Build JWE protected header
const header = {
alg: "ECDH-ES", //
enc: "A256GCM",
typ: "JWE",
epk: ephemeralPublicJwk, // Ephemeral public key
// Optional privacy-preserving bindings:
apu: apu ? jose.base64url.encode(apu) : undefined,
apv: apv ? jose.base64url.encode(apv) : undefined,
kid: recipientKeyId,
};
// AES-GCM encryption with AAD = protected header
const iv = wc.getRandomValues(new Uint8Array(12));
const aad = new TextEncoder().encode(headerB64);
const encryptedData = await wc.subtle.encrypt(
{ name: "AES-GCM", iv, additionalData: aad, tagLength: 128 },
aesKey,
plaintext,
);
// JWE compact serialization: header..iv.ciphertext.tag
return `${headerB64}..${ivB64}.${ciphertextB64}.${tagB64}`;
JWE Structure Output
The JWE compact serialization format consists of five base64url‑encoded segments separated by dots: header . encryptedKey . iv . ciphertext . tag. In ECDH‑ES direct mode the encryptedKey segment is empty
JWE = BASE64URL(Header) + '.' +
BASE64URL(EncryptedKey) + '.' +
BASE64URL(IV) + '.' +
BASE64URL(Ciphertext) + '.' +
BASE64URL(AuthTag)
POST the JWE string to the callback URL provided in the webhook. The payload should follow the IVMS‑101 JSON structure.
Payload Example:
{
"ivms101": {
"originator": {
"originatorPersons": [
{
"naturalPerson": {
"geographicAddress": [
{
"streetName": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..EEIUpkWh4TmIazAJ.N106aJWf9UaO6PnneSuqpCm1yTjvwm6VHeAdL95qTeV9I7AXfxVjfbTsPB21a1cKV3GgbOvSodE.jk0xhNYZc7IvZzPIA8qYTA",
"buildingNumber": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..b3ozBv2vEeG6X_Lh.Uq2BvynYIl5iHTlP0Ow4bI0xQ2TB1TCbrISWGm0R6NTKFHa9b0McGnQ8VQDW.Mz6XK5yZL5COEo578s6fvQ",
"postcode": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..4BornFcBu1EAg-m6.POaYAxnm5z6WubEQyufcKOYCdM9tblwVqGrjBsmqIQBG_mlfFpgN-zAJ0-w61A.8-iBJb6L27Ds196W5YMuNg",
"townName": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..kZ9LYFouKCMiBNAs.wwZ0CZL4sBTumj9h13PxsbOkP-Ga2tlEtkuhs-OdoanXFxbCwJdg_6FEB6JMQWj1Gg.TBVGfFnp7j5qDwLMcrMB5A",
"country": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..d_94xQmsjEBTE6EO.HxlEyhBMD-yAYXNdPA5ajjDIoXeR7wUUunDKWK1PxVE8kkTt3IQLyHBT2xg.LaPprf2EEXzi0QkN10rcIw"
}
],
"name": {
"nameIdentifier": [
{
"primaryIdentifier": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..G_iqOLkeULCPCEdg.2MfdlvjE03Dc-YI1-VmtiskZ67hONaJmKmbM6W7MjOj2ZzrVwfUxfLFkDBax.JFKaysFjOTR94j7NF_pwlQ",
"secondaryIdentifier": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..QG6zIn1VntabjLf-.J3dYeRVWq75fcaUHMSPcEe9h1UgpAHg-CJw9MqUH0z-wTPEbF7_DPX_QFHOXlQ.22GVuKcPvH7n8r30SzePHA",
"nameIdentifierType": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..s2K_hfXYYLQBnrk3.Pyej9jui4TQIrNpLUcHuyuWlukwGISqA9iA4NK_KUuSxlTqfCaqJ58Qu0HSwFA.cj3Yl1HYEjKW8EbtE764cQ"
}
]
}
}
}
],
"accountNumber": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..BN9jRcKQ_FETtbUF.ySxqiC1MkMBavZIj7wlXWQ-l_DY85uY0IOgV4lVd6edHXZVx7pSa8IfR6rFBTwjidwPxbvdjJ_u1ag8LhxxEmr5g2Q.Xo3_MM3WCOFpHz5Sa4Zx_A"
},
"beneficiary": {
"beneficiaryPersons": [
{
"naturalPerson": {
"name": {
"nameIdentifier": [
{
"primaryIdentifier": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..k78BNqvEW29fkTnT.DASXFQO0mc-04Pkyuwaq7ATYwnPPVQ3P1JADowljyAuKqq7hnknyZbJwLP0o5_s.QQgdMEyihLL5f_X0OV7fng",
"secondaryIdentifier": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0.._fm8Vy40OTqOO8-G.PrHIV_8eeSkzASvLOo0tJS2T3DmTLBh9pAUORGUZzXHWaT86xecpZ56FoknPg289k3o.Nv5GJ1vnIOIRmoP1A23aHQ",
"nameIdentifierType": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..sCI-fulFBTQhpiuq.wmEtRv_-BDCM8y7QTdbHfquNHuLSGNiK8Zt-2ol6ppnZ1fqXD-bWmdaia9UppQ.sDY0s_9zYTu0EwDB6QtrOg"
}
]
}
}
}
],
"accountNumber": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwic2VuZGVyS2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4IiwicmVjaXBpZW50S2lkIjoiZGlkOndlYjp2YXNwcy5pZDpzZyNhMzZiNTc0ZTZlMzI5ZjZlNzYzODQ0ODhjYzRhN2RmOTc4OGI0M2Q0ZGEyNmZlYTJmODhkYmJmZWRhMmQ4NzU4In0..xBKooIlC5T892Q0G.UeObGwrWPQS7jSQ84EVKdee2O_5o4HasLlybY9ys-Vc1-nhlv2Kw56nhG47H6LOR6EOK7XM2mCIgye0qT6vbJqDkxn8.6sjjlGI4uxeg9zZmRvleig"
}
}
}
Updated about 3 hours ago
