Skip to content

How to Decrypt PII v2.1

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.

Step 1 — 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:

typescript
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)
python
import base64, json

def get_jwe_kid(jwe_string: str) -> str:
    header_b64 = jwe_string.split(".")[0]
    return json.loads(base64.urlsafe_b64decode(header_b64 + "=="))["kid"]

kid = get_jwe_kid(pii_jwe_string)
private_key = my_key_store.get_private_key(kid)

Step 2 — Decrypt the JWE

Decrypt the JWE using your Enc1 private key. The result is the inner JWS (signed JWT):

typescript
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)
python
from jwcrypto import jwe as jwecrypto

token = jwecrypto.JWE()
token.deserialize(pii_jwe_string, key=private_key)
jws_string = token.payload.decode()

Step 3 — Decode the JWS payload

The inner JWS contains the PII JSON in its payload. Decode the payload to access the PII fields:

typescript
import { decodeJwt } from 'jose'

const piiPayload = decodeJwt(jwsString)
// piiPayload now contains { Initiation: { ... }, Risk: { ... }, iat, exp, iss, ... }
python
import json, base64

def decode_jws_payload(jws_string: str) -> dict:
    payload_b64 = jws_string.split(".")[1]
    return json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))

pii_payload = decode_jws_payload(jws_string)
# pii_payload now contains { "Initiation": { ... }, "Risk": { ... }, "iat": ..., "exp": ..., "iss": ..., ... }

Optional — Verify the TPP's JWS signature

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).

Step 4 — 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.

StageSpec fileSchema
Consentuae-api-hub-consent-manager-openapi.yamlAEBankServiceInitiationRichAuthorizationRequests.AEDomesticPaymentPII
Paymentuae-ozone-connect-bank-service-initiation-openapi.yamlAEBankServiceInitiation.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:

Nebras-Open-Finance/api-specs

Spec files are located under dist/ by category:

StagePath
Consentdist/api-hub/{version}/openapi/uae-api-hub-consent-manager-openapi.yaml
Paymentdist/ozone-connect/{version}/openapi/uae-ozone-connect-bank-service-initiation-openapi.yaml

Errata versions

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: false is set at every level of the PII schema — any unexpected fields will cause validation to fail.
  • required arrays are declared on sub-schemas (e.g. CreditorAccount is required on each creditor entry, SchemeName and Identification are required on account objects) — missing mandatory fields will cause validation to fail.
  • enum constraints restrict values to allowed options (e.g. SchemeName must be IBAN).
  • $ref pointers link to nested schemas (creditor, debtor, risk). For validation to work correctly, all components/schemas entries from the spec MUST be registered with the validator so that $ref pointers 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:

typescript
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')}`)
  }
}
python
import yaml
from jsonschema import validate, ValidationError, RefResolver

# 1. Load the OpenAPI spec and extract the PII schema
with open("uae-api-hub-consent-manager-openapi.yaml") as f:
    spec = yaml.safe_load(f)

pii_schema = spec["components"]["schemas"][
    "AEBankServiceInitiationRichAuthorizationRequests.AEDomesticPaymentPII"
]

# 2. Build a resolver so $ref pointers resolve against the full spec
schema_store = {
    f"#/components/schemas/{name}": schema
    for name, schema in spec["components"]["schemas"].items()
}
resolver = RefResolver.from_schema(spec, store=schema_store)

# 3. Validate the decrypted PII payload
def validate_pii_schema(pii_payload: dict) -> None:
    try:
        validate(instance=pii_payload, schema=pii_schema, resolver=resolver)
    except ValidationError as e:
        raise ValueError(f"PII schema validation failed: {e.message}") from e

Reject invalid PII

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.

Full decryption and validation example

typescript
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
}
python
from jwcrypto import jwe as jwecrypto
import json, base64

def decrypt_and_validate_pii(pii_jwe_string: str, private_key) -> dict:
    # 1. Decrypt the JWE → inner JWS
    token = jwecrypto.JWE()
    token.deserialize(pii_jwe_string, key=private_key)
    jws_string = token.payload.decode()

    # 2. Decode the JWS payload (signature verification is optional — see note above)
    payload_b64 = jws_string.split(".")[1]
    pii_payload = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))

    # 3. Validate against the OpenAPI schema
    validate_pii_schema(pii_payload)

    return pii_payload