Encryption in BaaS API

Summary

Sensitive information must be encrypted at the field level during transmission through BaaS API requests and responses, along with transport TLS encryption. This ensures confidentiality not only between networking devices but also between the sender and the recipient. That information includes, but is not limited to, personally identifiable information (such as Social Security Numbers and dates of birth) and banking details. Encrypted data should be encapsulated within a specially structured JSON object, with all binary content encoded as Base64 strings. Green Dot employs an ECC-based hybrid encryption model to ensure field-level data protection during API communication in both directions.

Encryption Setup

Data is secured through Elliptic Curve Cryptography (ECC), employing the AES-GCM-256 encryption method without padding. The key exchange process utilizes the Elliptic Curve Diffie–Hellman (ECDH) scheme to derive a shared secret key for symmetric AES data encryption and decryption.

📘

Please note that before successfully utilizing encryption with the BaaS API, both Green Dot and a Partner must exchange public encryption keys. These keys will be used for encryption and decryption of both requests and responses in both directions. Contact your Green Dot representative to perform this exchange.

Encryption Workflow

The following workflow is utilized to encrypt and serialize encrypted payloads for use with the BaaS API endpoints:

  1. Generate a shared symmetric key
  2. Encrypt data using symmetric cipher and the shared symmetric key
  3. Prepare an encrypted JSON structure for an API request

Step 1: Generating Shared Symmetric Key

The ECC encryption method involves generating a new ephemeral pair of asymmetric keys for each transaction, which is subsequently utilized to establish a new shared symmetric key for encryption purposes.
Ephemeral keys in ECC encryption are temporary asymmetric key pairs generated for each session to ensure secure communication. By using these keys in the Elliptic Curve Diffie-Hellman (ECDH) protocol, a unique shared secret is derived for encryption, providing enhanced security and preventing reuse of key material across sessions.
This workflow step includes the following three steps (a, b, c):

a) Generate a new ephemeral pair of private/public asymmetric keys using PRIME-256-V1 curve and ECDH exchange.

To implement this process programmatically, begin by referencing the P-256 elliptic curve, also known as prime256v1 or secp256r1.

Following this, create a new ephemeral asymmetric key pair (private and public keys) within the established domain parameters. This ephemeral private key, when combined with the recipient's public key through the Elliptic Curve Diffie-Hellman protocol, will derive a shared secret. This shared secret then serves as the entropy source for creating a session-specific symmetric key—often refined with a key derivation function such as HKDF—to be used for data encryption during the session.

C# (BouncyCastle) example:

// Get the parameters for the P256 curve (also known as prime256v1 or secp256r1)
var x9ecParameters = ECNamedCurveTable.GetByName("prime256v1");
// Create domain parameters for the curve
var domainParameters = new ECNamedDomainParameters(
    SecObjectIdentifiers.SecP256r1,
    x9ecParameters.Curve,
    x9ecParameters.G,
    x9ecParameters.N
);
// Create a key pair generator for the elliptic curve
var keyPairGenerator = new ECKeyPairGenerator();
// Initialize the key pair generator with the domain parameters and a secure random number generator
var generationParameters = new ECKeyGenerationParameters(domainParameters, new SecureRandom());
keyPairGenerator.Init(generationParameters);
// Generate the key pair
AsymmetricCipherKeyPair ephemeralKeyPair = keyPairGenerator.GenerateKeyPair();    

//Get the public key from the ephemeral key pair
var ephemeralPublicKey = (ECPublicKeyParameters) ephemeralKeyPair.Public;
//Create a new public key parameters object for the ephemeral public key
// The "EC" parameter indicates that this is an elliptic curve public key, and the SecP256r1 is the curve used
var ephemeralPublicParams = new ECPublicKeyParameters("EC", ephemeralPublicKey.Q, SecObjectIdentifiers.SecP256r1);
//Create a public key info object from the ephemeral public key parameters
var ephemeralPublicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(ephemeralPublicParams);
// Get the encoded form of the ephemeral public key info
var encodedEphemeralPublicKeyInfo = ephemeralPublicKeyInfo.GetEncoded();

b) Calculate Shared Secret Key

Once the ephemeral keys are generated, the next step is to derive the shared secret using the Elliptic Curve Diffie-Hellman (ECDH) key exchange protocol. The ephemeral private key generated for the transaction is combined with the counterpart’s public key to produce an agreed shared secret. This shared secret is then used as the basis to derive the symmetric key, typically via a key derivation function such as HKDF, ensuring the key material is strong and unique for each session.

The shared secret key is calculated by utilizing the stored partner's (sender) public asymmetric key in conjunction with the private ephemeral asymmetric key generated in the previous step (a).

To derive the shared secret, you must perform an Elliptic Curve Diffie-Hellman (ECDH) agreement. This involves initializing the ECDH agreement object with the ephemeral private key and then combining it with the recipient's public key. The process results in a value that can be processed into a byte array suitable for cryptographic operations as a shared secret key, and it must be 32 bytes long.

C# (BouncyCastle) example for generating the key and converting it to a byte array:

// This method computes the shared secret using ECDH (Elliptic Curve Diffie-Hellman) agreement.

// Get the private key from the generated ephemeral key pair
ECPrivateKeyParameters privateKey = ephemeralKeyPair.Private;

// Load the recipient public key from a PEM file or other source
ECPublicKeyParameters publicKey = ReadTargetPublicKey(@".\Certs\SampleEcPublic.pem");

var agree = new ECDHCBasicAgreement();
// Initialize the agreement with the private key
agree.Init(privateKey);
// Calculate the shared secret using the public key of the recipient
var sharedSecret = agree.CalculateAgreement(publicKey);
// Convert the shared secret to a byte array without leading zeros
var generated = sharedSecret.ToByteArrayUnsigned();
// Ensure the shared secret is 32 bytes long by padding with leading zeros if necessary
Byte[] sharedSecretBytes = generated.Length == 32 ? generated : Combine(new byte[32 - generated.Length], generated);

c) Generate Shared Symmetric Key

Utilizing the secret key obtained from the preceding step, we conduct several calculations to derive a shared symmetric key, which will be employed for AES encryption in subsequent workflow phases.

To derive this symmetric key, a standard approach is to employ a key derivation function (KDF), using the 32-byte shared secret as the primary input. Often, a Salt and additional context data (such as an algorithm identifier or a nonce) are incorporated into the derivation process, enhancing the uniqueness and resilience of the output key. The output of the KDF should match the requirements of the encryption algorithm, in this case, yielding a 256-bit key for AES.

Generally, the Salt value is the Request ID value (a per-request unique GUID generated by the calling side to track API requests and responses), and the nonSalt value is the assigned Partner Program Code.

An example implementation involves concatenating the shared secret with the relevant parameters, then passing this data through a secure hash function like SHA-256 to generate the encryption key material. This digest process effectively transforms the shared secret into a robust symmetric key, tailored for secure AES encryption and decryption.

C# (BouncyCastle) example for generating the key and converting it to a byte array:

// This method restores the symmetric key used for encryption/decryption based on the shared secret, salt, and non-salted part.

string salt = stringRequestId; // Per-request unique ID/GUID
string nonSalt = stringProgramCode; // Program Code assigned to the project


var encryptionKeyBytes = new byte[32];
// Use BouncyCastle's Sha256Digest to create a message digest for the key derivation function (KDF)
var messageDigest = new Sha256Digest();

// Prepare the KDF input data
byte[] counter = { 0x00, 0x00, 0x00, 0x01 };
// The algorithm ID for AES256 GCM, encoded as ASCII bytes
byte[] algorithmIdBytes = Encoding.ASCII.GetBytes("id-aes256-GCM");
// The length of the algorithm ID in hexadecimal format, encoded as bytes
byte[] algorithmIdLenBytes = Hex.Decode(algorithmIdBytes.Length.ToString("X2"));
// The party U info is the non-salted part of the key derivation, encoded as ASCII bytes
byte[] partyUInfoBytes = Encoding.ASCII.GetBytes(nonSalt);
// The party V info is the salted part of the key derivation, hashed using SHA-256 and encoded as bytes
byte[] partyVInfoBytes = GetHashSha256Bytes(Encoding.UTF8.GetBytes(salt));

// Combine all the parts into a single byte array for the key derivation function (KDF)
byte[] kdfBytes = Combine(counter, sharedSecretBytes);
kdfBytes = Combine(kdfBytes, algorithmIdLenBytes);
kdfBytes = Combine(kdfBytes, algorithmIdBytes);
kdfBytes = Combine(kdfBytes, partyUInfoBytes);
kdfBytes = Combine(kdfBytes, partyVInfoBytes);

// Update the message digest with the KDF input data
messageDigest.BlockUpdate(kdfBytes, 0, kdfBytes.Length);
// Finalize the message digest to produce the encryption key bytes
messageDigest.DoFinal(encryptionKeyBytes, 0);

byte[] sharedSymmetricBytes = encryptionKeyBytes;

Step 2: Encrypting sensitive data

BaaS API uses the JSON format for both responses and requests. Sensitive data is formatted and encrypted within the JSON format.

Before encryption, the data to be secured is carefully structured to ensure that all personally identifiable information, such as addresses and contact details, is encapsulated within the JSON body. This structured approach allows for precise encryption and targeted decryption, safeguarding only the sensitive fields while keeping the overall data format intact for interoperability.

An example of sensitive personal data (user profile data in this example) formatted in JSON (before the encryption):

{
    "ProfileData": {
        "Addresses": [
            {
                "IsDefault": true,
                "AddressLine2": "SUITE 560",
                "AddressLine1": "15777  Foothill Blvd",
                "Type": "home",
                "State": "CA",
                "ZipCode": "93600",
                "City": "Sunspot",
                "IsVerified": true
            }
        ],
        "FirstName": "Joe",
        "LastName": "Doe",
        "MiddleName": "A"
    },
    "identifyingData": {
        "ssnSuffix": "",
        "dateOfBirth": "1990-01-01",
        "ssn": "111-11-1111"
    },
    "email": {
        "IsDefault": false,
        "emailAddress": "[email protected]",
        "IsVerified": true
    },
    "phoneNumbers": [
        {
            "number": "7121234567",
            "isDefault": true,
            "IsVerified": true,
            "type": "Mobile"
        }
    ]
}

Figure 1. Unencrypted User Profile Data

To encrypt the textual data, first convert the string into its byte representation. Next, create appropriate encryption parameters and use a cipher to

encrypt the string in its byte form. BaaS API requires data to be encrypted using symmetric AES encryption in GCM mode with no padding. Create a symmetric IV using a 12-byte array (GCM requirement) filled with zeros and an AES key parameter based on the shared symmetric key created in the previous step. Then, initialize the cipher using those parameters.

C# (BouncyCastle) example for encryption using the symmetric key calculated in the previous step:

string stringData = "… JSON structure to encrypt …";
// Convert the JSON string into its byte representation
byte[] cipherDataBytes = Encoding.UTF8.GetBytes(stringData);

// Create the key parameter using the provided symmetric bytes
KeyParameter keyparam = ParameterUtilities.CreateKeyParameter("AES", sharedSymmetricBytes, 0, sharedSymmetricBytes.Length);
// Initialize IV vector for AES encryption
// The IV is typically 12 bytes for GCM mode, but can be 12 bytes for other modes like CBC.
byte[] SymmetricIVBytes = new byte[12]; // 12 bytes of zeros for GCM mode
// Create the parameters with IV using the key parameter and the symmetric IV
ParametersWithIV parameters = new ParametersWithIV(keyparam, SymmetricIVBytes);
// Get the cipher instance for AES GCM mode with no padding
var cipher = CipherUtilities.GetCipher("AES/GCM/NoPadding");
// Initialize the cipher with the parameters
cipher.Init(true, parameters);
// Encrypt the data and get the bytes representing the encrypted output
byte[] outputBytes = cipher.DoFinal(cipherDataBytes);

Step 3: Preparing an encrypted JSON structure for an API request

Once the encrypted byte array has been obtained, the next step is to embed this cipher text into a structured JSON object suitable for API consumption. The encrypted output is converted to a Base64-encoded string to ensure safe transport over protocols that expect text data.

The encrypted data (EncryptedData structure in documentation) must be formatted using the following JSON structure:

{
    "version": string,
    "ephemeralPublicKey": string,
    "publicKeyHash": string,
    "data": string
}

Figure 2. EncryptedData structure to transport encrypted data

Each field within this JSON object plays a critical role in the integrity and utility of the encrypted message. Careful attention must be given to the correct population of each field, adhering strictly to API requirements for type, encoding, and value constraints. Below, we provide a guide for constructing each attribute, ensuring both cryptographic soundness and interoperability with the server's decryption routines.

This approach allows the sensitive payload to remain confidential while conforming to the API’s expected request schema.

Field-by-Field Preparation Guide

FieldDescription
versionThe version field is a required identifier that specifies the format or protocol version being used for the encrypted payload. This ensures both client and server interpret the payload structure consistently. Set this field to the agreed-upon protocol identifier, such as "EC_v1" by default.
ephemeralPublicKeyThe ephemeralPublicKey field contains the sender's ephemeral public key, which is generated specifically for a single encryption session. This key is essential for Elliptic Curve Diffie-Hellman (ECDH) key exchange and allows the recipient to derive the shared secret needed for decryption.
Generate a new ephemeral elliptic curve key pair (using, for example, the P-256 curve) for each encryption operation.
Extract the public key and encode it using Base64 for safe inclusion in JSON.
Assign the resulting string to the ephemeralPublicKey field.
publicKeyHashThe publicKeyHash field provides a hashed representation of the recipient's public key (or another agreed-upon key), serving as an integrity check and helping to prevent key substitution attacks.
Obtain the recipient's public key in its raw or encoded form.
Apply a cryptographic hash function (e.g., SHA-256) to the public key.
Base64-encode the resulting hash output.
Set this Base64-encoded string as the value for publicKeyHash.
dataThe data field holds the payload that has been encrypted using the derived symmetric key from the ECDH operation. The encrypted result, which is binary, must be converted to a textual form for JSON transport.
Encrypt the original data using the symmetric key derived from ECDH.
Take the resulting encrypted byte array and encode it using Base64.
Insert the Base64-encoded string into the data field.

Figure 3. EncryptedData Structure Fields

All these fields are mandatory for the API to properly interpret, validate, and process the encrypted payload. The precise algorithms and encoding formats should always match those specified in the API documentation or cryptographic protocol in use.

The ephemeralPublicKey field can be formatted using this C# example (BouncyCastle):

//Get the public key from the ephemeral key pair
var ephemeralECPublicKey = (ECPublicKeyParameters) ephemeralKeyPair.Public;
// Create a new structure for the public key parameters using ECDH exchange and the P256 curve to ensure compatibility
var ephemeralPublicParams = new ECPublicKeyParameters("EC", ephemeralECPublicKey.Q, SecObjectIdentifiers.SecP256r1);
//Create a public key info object from the ephemeral public key parameters
var ephemeralPublicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(ephemeralPublicParams);
// Get the encoded form of the ephemeral public key info
var encodedEphemeralPublicKeyInfo = ephemeralPublicKeyInfo.GetEncoded();
// Convert the encoded ephemeral public key to a Base64 string 
string ephemeralPublicKey = Convert.ToBase64String(encodedEphemeralPublicKeyInfo);

The publicKeyHash field can be calculated using an approach described in this C# example (BouncyCastle):

// Load the recipient public key from a PEM file or other source
ECPublicKeyParameters targetPublicKey = ReadTargetPublicKey(@".\Certs\SampleEcPublic.pem");
// Create a new structure for the public key parameters using ECDH exchange and the P256 curve to ensure compatibility
ECPublicKeyParameters param = new ECPublicKeyParameters("EC", targetPublicKey.Q, SecObjectIdentifiers.SecP256r1);
// Create a SubjectPublicKeyInfo object from the ECPublicKeyParameters
var info = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(param);
// Get the encoded form of the public key info
byte[] publicKeyBytes = info.GetEncoded();

// Hash the data using BouncyCastle's SHA256Digest
Sha256Digest sha256Digest = new Sha256Digest();
sha256Digest.BlockUpdate(publicKeyBytes, 0, publicKeyBytes.Length);
byte[] hashedBytes = new byte[sha256Digest.GetDigestSize()];
sha256Digest.DoFinal(hashedBytes, 0);
// Convert the hashed bytes to a Base64 string to use as the public key hash
string publicKeyHash = Convert.ToBase64String(hashedBytes);

The data field contains a Base64-encoded string representing the byte array produced by the encryption method discussed in Step 2. This string can be easily converted, as illustrated in this C# example:

string data = Convert.ToBase64String(outputBytes);

The following C# example demonstrates how to combine all the fields and format an encrypted JSON structure:

EncryptedData encryptedData = new EncryptedData
{
    PublicKeyHash = publicKeyHash,
    Data = data,
    EphemeralPublicKey = ephemeralPublicKey,
    Version = "EC_v1"
};

The EncryptedData structure is defined as follows (C#):

/// <summary>
/// Represents encrypted data along with metadata required for decryption.
/// </summary>
/// <remarks>This class encapsulates the encrypted payload and associated information, such as the
/// version, public key hash, and ephemeral public key, which are necessary for decrypting the data. It is commonly
/// used in cryptographic operations where secure data exchange is required.</remarks>
public class EncryptedData
{
    /// <summary>
    /// Gets or sets the version of the encrypted data format, usually “EC_v1”.
    /// </summary>
    [JsonProperty("version")]
    public string Version { get; set; }

    /// <summary>
    /// Gets or sets the encrypted data associated with the object.
    /// </summary>
    [JsonProperty("data")]
    public string Data { get; set; }

    /// <summary>
    /// Gets or sets the hash of the recipient public key associated with the encrypted data.
    /// </summary>
    [JsonProperty("publicKeyHash")]
    public string PublicKeyHash { get; set; }

    /// <summary>
    /// Gets or sets the ephemeral public key used for secure communication.
    /// </summary>
    [JsonProperty("ephemeralPublicKey")]
    public string EphemeralPublicKey { get; set; }
}

A sample of a complete and serialized EncryptedData structure in JSON:

{
        "version": "EC_v1",
        "ephemeralPublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMHGH+OPmWdtReEPJMEBKNLh5RIv9T+OQ6tQu2aIdby9log/pTucYTUtly0XpW1u9baXpg6VdKoYxMuD3GyPeHA==",
        "publicKeyHash": "KlsZDtZDl4Cn8JN1kuZr9xMohXUlZe0xaghkJGL+lPI=",
        "data": "QEfAiqPPvJYLrVTEzas1bi63AtvEiM0klLKgwiej5CzKO1TThcRdB+w1RrSiTwgttz3BrgJOrT2KLmfz+ACNy0bk9ES62FG/Gk6yGPTL4S9Odk8gHny6eyqVa6AM/Iu5RVXTkmu+Z7AEAzmw+qOQGz8i7rTqazuQZd9yfYEPIWfzpc1FkDp1t5Z2XNbUXY3vsUUbSI0XLs1b2W4zSLpM+gg12GjkadVjRAkgSX0I9jQQj8XwBaY9xpwsaTK7pGOqaeP74EAfkxDvg+uAL1ppcq6OAIVGqWTNzSW3TTPyGpKpZZZtZNQeika1AZjoSZ0JPcKC0d1b3qJ7YMPbyLC18jHKCA9itOJWw3sk+zLh6uvQKM79GP8EtXIsPQ4cbZWX+hQHzbjI5365TDO0lWuAOtiZeO/VifZ1xAOGii6XrQCTziK7ljmN2LMQ1AIOvcDREDfCvZfS5tJWGKdgLXTUGQXzdToy9D7ZQAizXD/E5Nv1GSzRIRUwO+LVNq/TG/2RFqRtB9GScNUps0aGClgUanJbwKT9m3NrIV7ELIpqHQF2JzGXpLLcVooLPoNRu5uQaVqprue3ew9CSmfGt/k9GEwFXO4vItr0B/5SBL4ZkxrUFw7CqJady5i4Go978z4QzIT7XNwQp/UkpD2aLGo06X5s0diuJAdII4OWpt4Vn0BmACQuAC8="
}

Figure 4. EncryptedData Structure Example

Using Encrypted Payload with BaaS API

When preparing a BaaS API endpoint request in JSON format, some fields require encryption and must be presented in the EncryptedData format described previously. To identify those fields, please check the API Reference section. Those fields are also named using the “encryptedXXXXXXXXX” naming convention.

For example, you need to update existing user profile data using the structure provided in Figure 1. Unencrypted User Profile Data and the Update Customer Profile BaaS API endpoint:

{{BaasUrl}}/programs/{{ProgramCode}}/accounts/{{AccountIdentifier}}/users/{{UserIdentifier}}

That endpoint expects the following PUT request formatted in JSON and provided in the body of a HTTP call:

{
    "encryptedUserData": {{encryptedDataProfile}}
}

The "encryptedDataProfile" is an EncryptedData structure, and the main request field that expects the encrypted data is named "encryptedUserData” and must be set to the EncryptedData format.

A complete user data request format would be:

{
    "encryptedUserData": 
    {
        "version": string,
        "ephemeralPublicKey": string,
        "publicKeyHash": string,
        "data": string
    }
}

Here is an example of a correctly formatted User Profile Update request using encryption:

{
    "encryptedUserData": 
    {
        "version": "EC_v1",
        "ephemeralPublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMHGH+OPmWdtReEPJMEBKNLh5RIv9T+OQ6tQu2aIdby9log/pTucYTUtly0XpW1u9baXpg6VdKoYxMuD3GyPeHA==",
        "publicKeyHash": "KlsZDtZDl4Cn8JN1kuZr9xMohXUlZe0xaghkJGL+lPI=",
        "data": "QEfAiqPPvJYLrVTEzas1bi63AtvEiM0klLKgwiej5CzKO1TThcRdB+w1RrSiTwgttz3BrgJOrT2KLmfz+ACNy0bk9ES62FG/Gk6yGPTL4S9Odk8gHny6eyqVa6AM/Iu5RVXTkmu+Z7AEAzmw+qOQGz8i7rTqazuQZd9yfYEPIWfzpc1FkDp1t5Z2XNbUXY3vsUUbSI0XLs1b2W4zSLpM+gg12GjkadVjRAkgSX0I9jQQj8XwBaY9xpwsaTK7pGOqaeP74EAfkxDvg+uAL1ppcq6OAIVGqWTNzSW3TTPyGpKpZZZtZNQeika1AZjoSZ0JPcKC0d1b3qJ7YMPbyLC18jHKCA9itOJWw3sk+zLh6uvQKM79GP8EtXIsPQ4cbZWX+hQHzbjI5365TDO0lWuAOtiZeO/VifZ1xAOGii6XrQCTziK7ljmN2LMQ1AIOvcDREDfCvZfS5tJWGKdgLXTUGQXzdToy9D7ZQAizXD/E5Nv1GSzRIRUwO+LVNq/TG/2RFqRtB9GScNUps0aGClgUanJbwKT9m3NrIV7ELIpqHQF2JzGXpLLcVooLPoNRu5uQaVqprue3ew9CSmfGt/k9GEwFXO4vItr0B/5SBL4ZkxrUFw7CqJady5i4Go978z4QzIT7XNwQp/UkpD2aLGo06X5s0diuJAdII4OWpt4Vn0BmACQuAC8="
    }
}

Decryption Workflow

The workflow for decrypting API response data involves restoring a shared symmetric key and using the same AES cipher with GCM mode. We start by securely obtaining the local private key that corresponds to the ephemeral public key provided in the response structure. Once acquired, the decryption process can be initiated by applying the Elliptic Curve Diffie-Hellman (ECDH) algorithm to derive a shared secret key like it has been done in the encryption workflow. This shared secret key is then used to restore a shared symmetric key to decrypt the encrypted payload, ensuring that the integrity and confidentiality of the data remain intact throughout the API call request and response operations.

The decryption workflow does not require creating a new pair of ephemeral keys, but rather reusing the ephemeral public key stored in the incoming EncryptedData structure (it is possible that a single response may have one or more encrypted structures with different ephemeral public keys which will require recalculating the shared symmetric key).

When requesting sensitive data from a BaaS API endpoint, an encrypted data response should be expected. Please refer to a relevant endpoint reference for identifying encrypted field. Generally, an encrypted field will have a name starting with the “encrypted” prefix, e.g. “encryptedPrivatePaymentInstrumentData” or “encryptedUserData”. For example, requesting payment instrument details can return the following response:

{
    "paymentInstrument": {
        "encryptedPrivatePaymentInstrumentData": {
            "version": "EC_v1",
            "ephemeralPublicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqffAIDe5ckHhlZLqjUnAYiMQeHkAl7XyXTkfshYjOJpXpbkPjBTHJ+gTrGZPY4FTMyNYoQ8uHlvJ9wXzzgIuLQ==",
            "publicKeyHash": "KlsZDtZDl4Cn8JN1kuZr9xMohXUlZe0xaghkJGL+lPI=",
            "data": "k2WuGMT+svYIO2fMMRsKlodqVp8h917By8KnR+XsZbT0LQSaJ7BBrSEHz6CwKfe6HGu0IkSiKzAf+QAaBkyY7O02a71bU9kJqgNmd5Rc1QKZfJwyRIHgpToQD7xIUiWW"
        },
        "paymentIdentifier": "c29064bc-4dd6-4547-9572-4a546ce7cd05",
        "paymentInstrumentIdentifier": "8b7fd8bc-6cbb-4c04-9a2d-efacd76719ac",
}

The encryptedPrivatePaymentInstrumentData field contains an encrypted structure of the type EncryptedData, which must be decrypted using a restored shared symmetric key. To restore this shared symmetric key, we should utilize the ephemeral public key found in the ephemeralPublicKey field, together with our private asymmetric key stored in a local vault. The data requiring decryption is encoded as a Base64 string within the data field of the encryption structure. Please see Figure 2. EncryptedData structure to transport encrypted data and Figure 3. EncryptedData Structure Fields for the reference.

The decryption workflow has the following steps:

  1. Restore a shared symmetric key
  2. Decrypt data using symmetric cipher and the shared symmetric key

Step 1: Restoring Shared Symmetric Key

To restore a shared symmetric key, the local private key is combined with the ephemeral public key using the ECDH algorithm. This computation results in a shared secret, which serves as the foundation for generating the symmetric key.

a) Load Local Private Key

The local private key is generally stored in a secure vault and should not be shared with anybody.

b) Extract Ephemeral Public Key from the EncryptedData structure

The ephemeral public key is retrieved from the incoming EncryptedData structure from the ephemeralPublicKey field (see Figure 3. EncryptedData Structure Fields).

This C# (BouncyCastle) example shows how it can be done using BouncyCastle factory:

// Convert the Base64-encoded ephemeral public key from the EncryptedData object to a byte array
Byte[] decodedEphemeralPublicKey = Convert.FromBase64String(encryptedData.EphemeralPublicKey);
// Create an ephemeral public key object from the decoded byte array
var ephemeralPublicKey = (ECPublicKeyParameters)PublicKeyFactory.CreateKey(decodedEphemeralPublicKey);

c) Calculate Shared Secret Key

As in Step 1.b of the Encryption Workflow, the shared secret key is derived by employing two keys (a private key of one party and a public key of another party). In a decryption case, this involves using our local private key in conjunction with the ephemeral public key contained within the encrypted response structure.

This C# example demonstrates how it can be done with BouncyCastle using a cryptography agreement:

    // This method computes the shared secret using ECDH (Elliptic Curve Diffie-Hellman) agreement.

    // Load the local private key from a secure vault
    ECPrivateKeyParameters privateKey = LoadAsymmetricPrivateKey(@".\Certs\private_key.pem");

    var agree = new ECDHCBasicAgreement();
    // Initialize the agreement with the private key
    agree.Init(privateKey);
    // Calculate the shared secret using the public key of the recipient
    var sharedSecret = agree.CalculateAgreement(ephemeralPublicKey);
    // Convert the shared secret to a byte array without leading zeros
    var generated = sharedSecret.ToByteArrayUnsigned();
    // Ensure the shared secret is 32 bytes long by padding with leading zeros if necessary
    Byte[] sharedSecretBytes = generated.Length == 32 ? generated : Combine(new byte[32 - generated.Length], generated);

d) Restore Shared Symmetric Key

Having established the shared secret key, we proceed to restore the shared symmetric key by employing the same method used in Encryption Workflow Step 1.c.

C# (BouncyCastle) example for generating the key and converting it to a byte array:

    // This method restores the symmetric key used for encryption/decryption based on the shared secret, salt, and non-salted part.

    string salt = stringRequestId; // Per-request unique ID/GUID
    string nonSalt = stringProgramCode; // Program Code assigned to the project


    var encryptionKeyBytes = new byte[32];
    // Use BouncyCastle's Sha256Digest to create a message digest for the key derivation function (KDF)
    var messageDigest = new Sha256Digest();

    // Prepare the KDF input data
    byte[] counter = { 0x00, 0x00, 0x00, 0x01 };
    // The algorithm ID for AES256 GCM, encoded as ASCII bytes
    byte[] algorithmIdBytes = Encoding.ASCII.GetBytes("id-aes256-GCM");
    // The length of the algorithm ID in hexadecimal format, encoded as bytes
    byte[] algorithmIdLenBytes = Hex.Decode(algorithmIdBytes.Length.ToString("X2"));
    // The party U info is the non-salted part of the key derivation, encoded as ASCII bytes
    byte[] partyUInfoBytes = Encoding.ASCII.GetBytes(nonSalt);
    // The party V info is the salted part of the key derivation, hashed using SHA-256 and encoded as bytes
    byte[] partyVInfoBytes = GetHashSha256Bytes(Encoding.UTF8.GetBytes(salt));

    // Combine all the parts into a single byte array for the key derivation function (KDF)
    byte[] kdfBytes = Combine(counter, sharedSecretBytes);
    kdfBytes = Combine(kdfBytes, algorithmIdLenBytes);
    kdfBytes = Combine(kdfBytes, algorithmIdBytes);
    kdfBytes = Combine(kdfBytes, partyUInfoBytes);
    kdfBytes = Combine(kdfBytes, partyVInfoBytes);

    // Update the message digest with the KDF input data
    messageDigest.BlockUpdate(kdfBytes, 0, kdfBytes.Length);
    // Finalize the message digest to produce the encryption key bytes
    messageDigest.DoFinal(encryptionKeyBytes, 0);

    byte[] sharedSymmetricBytes = encryptionKeyBytes;   


Step 2: Decrypting Data Using Symmetric Cipher

Once the shared symmetric key is in place, the encrypted data is passed through a symmetric cipher algorithm, such as AES, leveraging the shared key. This decryption process translates the encrypted payload back into its original, readable format. The process guarantees data integrity, confirming that no tampering occurred during transmission.

The decryption procedure begins with obtaining the encrypted data in the form of a Base64-encoded string from the Data field of the EncryptedData structure. This Base64 string is decoded to produce a byte array, which represents the encrypted data.

With the symmetric key and IV in place, the cipher algorithm is initialized to perform the decryption of the JSON string payload, then the decryption method is invoked over the encrypted byte array. The resulting decrypted bytes can be decoded into a JSON string for further processing and deserialization.

C# (BouncyCastle) example for decryption using the symmetric key calculated in the previous step:

    // We need to decrypt the Data field of the EncryptedData structure    
    string stringData = encryptedData.Data;
    // Decode the Base64 string into the byte array
    byte[] cipherDataBytes = Convert.FromBase64String(stringData);

    // Create the key parameter using the provided symmetric bytes
    KeyParameter keyparam = ParameterUtilities.CreateKeyParameter("AES", sharedSymmetricBytes, 0, sharedSymmetricBytes.Length);
    // Initialize IV vector for AES encryption
    // The IV is typically 12 bytes for GCM mode, but can be 16 bytes for other modes like CBC.    
    byte[] SymmetricIVBytes = new byte[12]; // 12 bytes of zeros for GCM mode
    // Create the parameters with IV using the key parameter and the symmetric IV
    ParametersWithIV parameters = new ParametersWithIV(keyparam, SymmetricIVBytes);
    // Get the cipher instance for AES GCM mode with no padding
    var cipher = CipherUtilities.GetCipher("AES/GCM/NoPadding");
    // Initialize the cipher with the parameters
    cipher.Init(true, parameters);
    // Dencrypt the data and get the bytes representing the encrypted output
    byte[] outputBytes = cipher.DoFinal(cipherDataBytes);
    // Convert the decrypted byte array to a JSON string
    string json = Encoding.UTF8.GetString(decryptedBytes);