How to Decrypt PII 3 min read
The PersonalIdentifiableInformation field is a compact JWE (JSON Web Encryption) string. It was encrypted by the TPP using your LFI's public encryption key (Enc1). To decrypt it, you need the corresponding Enc1 private key.
Read the kid from the JWE header
The JWE protected header contains the kid (Key ID) of the encryption key that was used. Decode the first segment of the JWE to identify which private key to use:
function getJweKid(jweString: string): string {
const [headerB64] = jweString.split('.')
const header = JSON.parse(Buffer.from(headerB64, 'base64url').toString())
return header.kid
}
const kid = getJweKid(piiJweString)
const privateKey = myKeyStore.getPrivateKey(kid)
Decrypt the JWE
Decrypt the JWE using your Enc1 private key. The result is the inner JWS (signed JWT):
import { compactDecrypt, importPKCS8 } from 'jose'
const privateKeyPem = myKeyStore.getPrivateKeyPem(kid)
const privateKey = await importPKCS8(privateKeyPem, 'RSA-OAEP-256')
const { plaintext } = await compactDecrypt(piiJweString, privateKey)
const jwsString = new TextDecoder().decode(plaintext)
Decode the JWS payload
The inner JWS contains the PII JSON in its payload. Decode the payload to access the PII fields:
import { decodeJwt } from 'jose'
const piiPayload = decodeJwt(jwsString)
// piiPayload now contains { Initiation: { ... }, Risk: { ... }, iat, exp, iss, ... }
The JWS is signed by the TPP. You may optionally verify this signature against the TPP's public signing key. However, this is not required — the entire request containing the PII field is itself sent as a JWS that the API Hub has already verified was signed by the TPP. The PII therefore cannot have been tampered with in transit.
If you choose to implement JWS verification for defence-in-depth, see Verify TPP Signature (Optional).
Validate the PII against the OpenAPI schema
After decrypting, the LFI MUST validate the PII payload against the relevant OpenAPI schema. The PII has not been validated by the API Hub — schema validation is the LFI's responsibility.
| Stage | Spec file | Schema |
|---|---|---|
| Consent | uae-api-hub-consent-manager-openapi.yaml | AEBankServiceInitiationRichAuthorizationRequests.AEDomesticPaymentPII |
| Payment | uae-ozone-connect-bank-service-initiation-openapi.yaml | AEBankServiceInitiation.AEDomesticPaymentPIIProperties |
Obtaining the OpenAPI specification
The OpenAPI YAML files are the source of truth for PII schemas. They are maintained in the canonical specification repository:
Spec files are located under dist/ by category:
| Stage | Path |
|---|---|
| Consent | dist/api-hub/{version}/openapi/uae-api-hub-consent-manager-openapi.yaml |
| Payment | dist/ozone-connect/{version}/openapi/uae-ozone-connect-bank-service-initiation-openapi.yaml |
Specifications may have errata releases (e.g. v2.1.x-errata1) that contain targeted corrections. When multiple version folders exist for the same major.minor version, use the highest errata that contains the file you need. If a file is not present in an errata folder, fall back to the base version. Always check for errata before bundling a spec into your service.
Validating against the schema
Extract the relevant components/schemas entry from the YAML file and validate the decrypted PII payload against it. The PII schemas in the OpenAPI specification already declare the constraints needed for validation:
additionalProperties: falseis set at every level of the PII schema — any unexpected fields will cause validation to fail.requiredarrays are declared on sub-schemas (e.g.CreditorAccountis required on each creditor entry,SchemeNameandIdentificationare required on account objects) — missing mandatory fields will cause validation to fail.enumconstraints restrict values to allowed options (e.g.SchemeNamemust beIBAN).$refpointers link to nested schemas (creditor, debtor, risk). For validation to work correctly, allcomponents/schemasentries from the spec MUST be registered with the validator so that$refpointers resolve.
When you register the full set of component schemas and compile the PII schema, standard JSON Schema validators (ajv for Node.js, jsonschema for Python) will enforce all of these constraints automatically. No custom validation logic is needed for schema conformance — the OpenAPI spec is the single source of truth.
The following example shows how to validate a domestic payment PII at consent time:
import Ajv from 'ajv'
import { load } from 'js-yaml'
import { readFileSync } from 'fs'
// 1. Load the OpenAPI spec and extract the PII schema
const spec = load(
readFileSync('uae-api-hub-consent-manager-openapi.yaml', 'utf-8')
) as Record<string, any>
const piiSchema =
spec.components.schemas[
'AEBankServiceInitiationRichAuthorizationRequests.AEDomesticPaymentPII'
]
// 2. Build a validator — register all component schemas so $ref resolves
const ajv = new Ajv({ allErrors: true, strict: false })
for (const [name, schema] of Object.entries(spec.components.schemas)) {
ajv.addSchema(schema as object, `#/components/schemas/${name}`)
}
const validate = ajv.compile(piiSchema)
// 3. Validate the decrypted PII payload
function validatePIISchema(piiPayload: Record<string, unknown>): void {
const valid = validate(piiPayload)
if (!valid) {
const errors = validate.errors?.map(e => `${e.instancePath} ${e.message}`)
throw new Error(`PII schema validation failed:\n${errors?.join('\n')}`)
}
}
If the decrypted PII fails schema validation, the LFI MUST reject the consent or payment. Do not attempt to process a payment with malformed PII — return an appropriate error response. See Personal Identifiable Information for the full set of validation rules.
Decryption and validation, end-to-end
import { compactDecrypt, importPKCS8, decodeJwt } from 'jose'
async function decryptAndValidatePII(
piiJweString: string,
kid: string
): Promise<Record<string, unknown>> {
// 1. Load the Enc1 private key matching the kid
const privateKeyPem = myKeyStore.getPrivateKeyPem(kid)
const privateKey = await importPKCS8(privateKeyPem, 'RSA-OAEP-256')
// 2. Decrypt the JWE → inner JWS
const { plaintext } = await compactDecrypt(piiJweString, privateKey)
const jwsString = new TextDecoder().decode(plaintext)
// 3. Decode the JWS payload (signature verification is optional — see note above)
const piiPayload = decodeJwt(jwsString)
// 4. Validate against the OpenAPI schema
validatePIISchema(piiPayload)
return piiPayload
}
