Encryption

Some Apiture Open Banking APIs pass sensitive data (such as passwords) or personally identifiable data (such as social security numbers). Such data is necessary for digital banking applications: regulations require financial institutions to verify the identity of their customers.

The APIs all operate using transport layer security (TLS or https) which encrypts the request and response data. The APIs provide an additional layer of encryption for this sensitive data in case a client becomes compromized and an attacker can intercept request bodies. if that happens, the sensitive data in the JSON requests is encrypted and secure from theft.

Let’s consider the operation of changing the user’s password. To change a the password for an already authenticated user, the client should POST a request to /my/password. The request body contains both the user’s current password and their new password, but the currentPassword and newPassword string properties in the request body must be encrypted. This tutorial shows how a client can use the Apiture Open Banking APIs encryption protocol to encrypt this sensitive data.

The request body uses the passwordChange schema; an excerpt if that schema is below.

passwordChange:
  title: Password change
  description: >-
    Representation used to change a user password.
    The request must contain the `currentPassword`, the `newPassword`.
    The client encrypts these values using the
    `secret` encryption key and store the corresponding encryption key aliases
    in the `_encryption` metadata property.
  allOf:
    - $ref: 'https://production.api.apiture.com/schemas/common/abstractResource/v2.0.0/model.json'
    - type: object
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          description: >-
            The user's current encrypted user password.
            The client should prompt the user for their current password,
            then encrypt it with the `secret` encryption key.
          type: string
          format: encrypted-password
          x-apiture-encrypt: secret
        newPassword:
          description: >-
            The user's encrypted new password.
             The client should prompt the user for their new password,
             then encrypt it with the `secret` encryption key.
          type: string
          format: encrypted-password
          x-apiture-encrypt: secret
        _encryption:
          description: Metadata about the encrypted `currentPassword` and `newPassword` properties.
          allOf:
            - $ref: 'https://production.api.apiture.com/schemas/common/encryptionMetadata/v1.0.1/model.json'
          example:
            currentPassword: secret-48729783
            newPassword: secret-48729783
  example:
    currentPassword: EDHJKE45785HURYUR89RID7LIULD8973HDODHWLIW474HQ4GP47H
    newPassword: E5UFOUOI45IOFLISKUTYW4U6K34HKFJOE98YW4IYLLIWEO72PHH4
    _encryption:
      currentPassword: secret-48729783
      newPassword: secret-48729783

Let’s see how the client can create this request body. There are three steps:

  1. The client fetches the public key(s) necessary to encrypt the sensitive pro[erties
  2. The client encrypts the property string values and Base64 encoded the encrypted value
  3. The client adds _encryption metadata to the request which describes how the properies are encrypted

Fetch Encryption Keys

The passwordChange schema contains a x-apiture-encrypt annotation that declares that the request body includes properties encrypted using an encryption key named secret. This annotation lists all the encryption keys used in the request. In most cases, only one encryption key is used for all properties that require encryption, but it is possible that different properties require different encryption keys.

The two properties, currentPassword and newPassword, also each have an x-apiture-encrypt annotations which define the encryption key the client uses to encrypt those properties.

The client should encrypt the current and new password using secret public encryption key obtained from the GET /encryptionKeys. To obtain the secret public keys, call the getEncryptionKeys operation:

GET https:///api.thirdparty.bank/auth/encryptionKeys?keys=secret

(See also the JavaScript example below.)

The encryptionKeys response contains a public key for each of the keys in the request query parameter:

{
  "_profile": "https://api.apiture.com/schemas/common/encryptionKeys/v1.0.0/profile.json",
  "keys": {
    "secret": {
      "name": "secret",
      "publicKey": "-----BEGIN RSA PUBLIC KEY-----\\nMIIBCgKCAQEAl2/fCtf69EnMqw6O/6Wh9wFvKW80jjNfXEWbHh0cnWKW1i0Heg0B...\\n-----END RSA PUBLIC KEY-----",
      "alias": "secret-48729783",
      "createdAt": "2020-10-09T05:01:16.375Z",
      "expiresAt": "2020-10-09T05:01:36.375Z"
    }
  }
}

This response contains a public key (truncated here for brevity) corresponding to the name "secret". Each encryption key has an expiration; the service rotates keys frequently. The alias value, secret-48729783 is the name of the current secret key rotation. We passes the alias back to the service in the _encryption metadata that we add to the response below.

Encrypt Each Property

The client can now encrypt the two properties using that public key. Clients using JavaScript can use the crypto library. Note: The encrypted values must also be Base64 encoded to ensure correct transmission and decoding on the service side. The encrypt function performs both the encryption and Base64 encoding.

const crypto = require('crypto');

const encrypt = (text, key) => {
  var buffer = Buffer.from(text);
  var encrypted = crypto.publicEncrypt(key, buffer);
  return encrypted.toString('base64');
};

const headers = {
  'Accept':'application/hal+json',
  'API-Key': myClientApiKey,
  'Authorization': `Bearer ${currentAuthenticatedUserAccessToken}`
};

$.ajax({
  url: 'https://api.thirdparty.bank/auth/encryptionKeys',
  method: 'get',
  data: '?keys=secret',
  headers: headers,
  success: function(response) {
    // this is a rough outline, but not very robust
    const currentPassword = .... // value from UI form ;
    const newPassword = .... // value from UI form ;
    const secretPublicKey = response.keys.secret.publicKey;
    const secretAlias = response.keys.secret.alias;
    const encrypted_currentPassword = encrypt(currentPassword, secretPublicKey);
    const encrypted_newPassword = encrypt(newPassword, secretPublicKey);
    // more code to be added here; see below...
  },
  error: function(jqXHR jqXHR: jqXHR jqXHR, status: string, err: string) {
       // handle an error here
  }
})

The client then creates a request body with the encrypted properties:

  const request = {
      currentPassword: encrypted_currentPassword,
      newPassword: encrypted_newPassword
      );

Add _encryption Metadata to the Request

Finally, the client adds an _encryption object (encryptionMetadata) that contains metadata about the encrypted values. This is an object which maps the property name of each encrypted property in the JSON object to the alias of the encryption key that the client used to encrypt that property. We used the same encryption key with the alias "secret-48729783" to encrypt both currentPassword and newPassword:

request._encryption = {
  currentPassword: secretAlias, // has the value 'secret-48729783' in this example
  newPassword: secretAlias, // also has the value 'secret-48729783' in this example
};

In general, we must add an _encryption object to each object in the request that has encrypted data, including nested objects. For example, if the request had two objects a and b with encrypted properties x and y respectively:

{
  "a": {
    "x": "secret-value-1"
  },
  "b": {
    "y": "secret-value-1"
  }
}

we must add _encrypted to both a and b after encrypting x and y. (That is, the _encrypted object is always a direct sibling of each encrypted value.)

{
  "a" : {
     "x": "E34D2C1D05644410BB2B06A494D2BD3AC45607F21EA2EB435",
     "_encryption": {
       "x" : "secret-472939"
     }
  },
  "b" : {
     "y": "EA62EE8D14EC8BDF30C3FF4D77BA24AE69344C1E88B7250DA"
     "_encryption": {
       "y" : "secret-472939"
     }
  }
}

The client can then submit the updated request to the changeUserPassword operation.

$.ajax({
  url: 'https://api.thirdparty.bank/auth/my/password',
  method: 'put',
  headers: headers,
  success: function(data) {
    ...
  },
  error: function(jqXHR jqXHR: jqXHR jqXHR, status: string, err: string) {
       // handle an error here
  }
})

Encryption key expiration

The client can reuse the encryption keys, but the keys expire after a period of time (several minutes, but not several hours). The getEncryptionKeys response includes the expiration time for each of the requested keys. The service automatically rotates the keys when they expire and returns a new alias for the key after rotating the key. Thus, subsequent calls to getEncryptionKeys return the same aliases and expirations if the key has not expired, or it may return new aliases and expirations.

Future SDK support

A future release of the Apiture Open Banking APIs software development kit (SDK) will perform this encryption automatically. The client needs to only call the operation and pass in the raw data, and the SDK will fetch the encryption keys and encrypt the data with the public keys.