Documentation Index
Fetch the complete documentation index at: https://developers.circle.com/llms.txt
Use this file to discover all available pages before exploring further.
This guide explains how to set up encryption and decryption between an OFI and a
BFI during the RFI process so that a file can be passed securely. This
encryption varies from the method used to encrypt JSON communications between
OFI and BFI, but shares some features. For compactness, files are encrypted
using AES, and then the key is encrypted using JWE. Both are then transmitted to
the BFI for a two-stage decryption process.
In the CPN system, the OFI encrypts a payload with a randomly generated AES key.
This key is then encrypted with the BFI’s public key using
JSON Web Encryption. The
encrypted AES key and encrypted file payload are transmitted to the BFI. The BFI
decrypts the AES key using their private JWK and uses it to decrypt the file
contents.
Steps
The following sections describe the steps necessary to encrypt a file sent from
an OFI to a BFI through the RFI endpoint.
Step 1: Generate a random 128-bit AES key
Using your chosen implementation language, generate a random 128-bit AES key for
AES-128-GCM encryption.
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
/**
* @return A SecretKey for AES encryption.
* @throws GeneralSecurityException if the AES algorithm is not available.
*/
public static SecretKey generateAesKey() throws GeneralSecurityException {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
keyGenerator.init(128, new SecureRandom());
return keyGenerator.generateKey();
}
Step 2: Generate a 12-byte IV
Using your chosen implementation language, generate a 12-byte IV.
import java.security.SecureRandom;
/**
* @return A 12-byte array containing the IV.
*/
public static byte[] generateIv() {
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
return iv;
}
Step 3: Encrypt the file contents
Encrypt the file contents using AES-128-GCM using the key and IV from the
previous steps.
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.GeneralSecurityException;
/**
* @param plaintextPayload The raw data to encrypt.
* @param aesKey The AES key to use for encryption.
* @param iv The 12-byte Initialization Vector.
* @return The encrypted data, including the GCM authentication tag.
* @throws GeneralSecurityException if a cryptographic error occurs.
*/
public static byte[] encryptPayload(byte[] plaintextPayload, SecretKey aesKey, byte[] iv) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmParameterSpec);
return cipher.doFinal(plaintextPayload);
}
Step 4: Encrypt the AES key
Using the JWK data from the quote response, encrypt the AES key that was used to
encrypt the file contents with the following parameters using JWE:
- Algorithm: ECDH-ES+A128KW
- Encryption method: A128GCM
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWEEncrypter;
import com.nimbusds.jose.JWEHeader;
import com.nimbusds.jose.JWEObject;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.ECDHEncrypter;
import java.util.Base64;
import javax.crypto.SecretKey;
import java.security.interfaces.ECPublicKey;
/**
* @param aesKey The AES key to wrap.
* @param bfiPublicKey The recipient's Elliptic Curve public key.
* @return A compact, serialized JWE string representing the encrypted key.
* @throws JOSEException if an error occurs during JWE creation or encryption.
*/
public static String wrapAesKey(SecretKey aesKey, ECPublicKey bfiPublicKey) throws JOSEException {
// Create the JWEHeader using ECDH_ES+AS128KW and AES-128-GCM
JWEHeader header = new JWEHeader(
JWEAlgorithm.ECDH_ES_A128KW,
EncryptionMethod.A128GCM
);
// Base64 encode the AES key and wrap in JSON string
String base64AesKey = Base64.getEncoder().encodeToString(aesKey.getEncoded());
ObjectMapper objectMapper = new ObjectMapper();
String jsonPayload = objectMapper.writeValueAsString(base64AesKey);
Payload jwePayload = new Payload(jsonPayload);
JWEObject jweObject = new JWEObject(header, jwePayload);
JWEEncrypter encrypter = new ECDHEncrypter(bfiPublicKey);
jweObject.encrypt(encrypter);
return jweObject.serialize();
}
Step 5: Transmit the encrypted payload
After performing the encryption steps from the previous steps, you should have
three components:
- The AES-encrypted file content
- The JWE string containing the encrypted AES key
- The base64-encoded 12-byte IV
Use these components to create a multipart/form-data request to the
upload RFI file endpoint. A
200 response from the API indicates that the encryption was performed
correctly and the BFI can decrypt the file’s contents.
Important: Don’t manually set the Content-Type header. Let your HTTP
client library set it automatically. The header must include a boundary
parameter like:Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
If you manually set Content-Type: multipart/form-data, the request will fail.
import okhttp3.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.util.Base64;
import java.util.UUID;
/**
* Upload an encrypted RFI file using multipart/form-data.
*
* @param paymentId The payment UUID.
* @param rfiId The RFI UUID.
* @param encryptedFile The encrypted file to upload.
* @param fileName The original file name.
* @param fileType The file type (e.g., "PDF", "JPEG").
* @param fileKey The file key (e.g., "ID_DOCUMENT", "PROOF_OF_ADDRESS_DOCUMENT").
* @param encryptedAesKey The JWE-wrapped AES key.
* @param iv The Base64-encoded initialization vector.
* @param accessToken The Bearer token for authentication.
* @throws IOException if the HTTP request fails.
*/
public static void uploadRfiFile(UUID paymentId, UUID rfiId, File encryptedFile,
String fileName, String fileType, String fileKey,
String encryptedAesKey, byte[] iv, String accessToken) throws IOException {
OkHttpClient client = new OkHttpClient();
ObjectMapper objectMapper = new ObjectMapper();
// Create data objects
FileMetadata fileMetadata = new FileMetadata(fileName, fileType, fileKey);
FileEncryption encryption = new FileEncryption(encryptedAesKey, Base64.getEncoder().encodeToString(iv));
// Build multipart request
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("fileMetadata", objectMapper.writeValueAsString(fileMetadata))
.addFormDataPart("encryption", objectMapper.writeValueAsString(encryption))
.addFormDataPart("encryptedFile", fileName,
RequestBody.create(encryptedFile, MediaType.parse("application/octet-stream")))
.build();
Request request = new Request.Builder()
.url(String.format("https://api.circle.com/v1/payments/%s/rfis/%s/files", paymentId, rfiId))
.header("Authorization", "Bearer " + accessToken)
.post(requestBody)
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Upload failed: " + response.code());
}
}
}
public static class FileMetadata {
public final String fileName;
public final String fileType;
public final String fileKey;
public FileMetadata(String fileName, String fileType, String fileKey) {
this.fileName = fileName;
this.fileType = fileType;
this.fileKey = fileKey;
}
}
public static class FileEncryption {
public final String encryptedAesKey;
public final String iv;
public FileEncryption(String encryptedAesKey, String iv) {
this.encryptedAesKey = encryptedAesKey;
this.iv = iv;
}
}
An example request body is shown below:
------WebKitFormBoundary
Content-Disposition: form-data; name="fileMetadata"
Content-Type: application/json
{
"fileName": "example.pdf",
"fileType": "application/pdf",
"fileKey": "PROOF_OF_ADDRESS"
}
------WebKitFormBoundary
Content-Disposition: form-data; name="encryption"
Content-Type: application/json
{
"encryptedAesKey": "<base64-encoded-encrypted-aes-key>",
"iv": "<base64-encoded-iv-for-file-encryption>",
}
------WebKitFormBoundary
Content-Disposition: form-data; name="encryptedFile"; filename="encrypted_data.bin"
Content-Type: application/octet-stream
[AES ENCRYPTED BINARY FILE DATA]