Message Encryption Protocol
Encrypt IVMS101 PII before sending Travel Rule network messages.
PII in Partner -> CryptoSwift messages must be protected with an IVMS101 wrapper schema and encrypted before it is sent to CryptoSwift. The root request body can include transaction metadata, but personal Originator and Beneficiary data must be inside the encrypted IVMS101 payload.
IVMS101 Wrapper
Before encryption, shape the plaintext as an object with an ivms101 root containing the IVMS101 Originator and Beneficiary data:
{
"ivms101": {
"Originator": {
"...": "..."
},
"Beneficiary": {
"...": "..."
}
}
}
Only Originator and Beneficiary data is required for this payload. If your flow includes VASP details, include them inside the wrapped IVMS101 object before encryption.
Key Handshake
The encrypted payload uses a dual-key asymmetric handshake:
| Key | Format | Purpose |
|---|---|---|
| Sender private key | Base64 encoded Ed25519 seed, 32 bytes after base64 decoding. | Converted to a Curve25519/X25519 private key and used to encrypt. |
| Sender public key | Base64 encoded Ed25519 public key. | Sent as originatorVaspPubKey so CryptoSwift can identify the matching public key. |
| Receiver public key | CryptoSwift beneficiary VASP public key from GET /partners/vasps or GET /partners/vasps/by-wallet. | Converted to a Curve25519/X25519 public key and used to encrypt for CryptoSwift. |
Libsodium converts Ed25519 signing keys into Curve25519 keys for crypto_box_easy. CryptoSwift decrypts using its private key and the sender public key.
Encryption Flow
Encryption Helper
Install the dependency:
npm install libsodium-wrappers
Use this complete Node.js/TypeScript helper to produce the encrypted IVMS101 string:
import sodium from 'libsodium-wrappers';
export async function encryptIncomingIvms101(params: {
originatorPrivateKeyB64: string;
beneficiaryPublicKeyB64: string;
ivms101: {
Originator: unknown;
Beneficiary: unknown;
OriginatingVASP?: unknown;
};
}): Promise<string> {
await sodium.ready;
const originatorSeed = Buffer.from(params.originatorPrivateKeyB64, 'base64');
const beneficiaryEd25519PublicKey = Buffer.from(params.beneficiaryPublicKeyB64, 'base64');
const originatorSigningKeyPair =
sodium.crypto_sign_seed_keypair(originatorSeed);
const originatorX25519PrivateKey =
sodium.crypto_sign_ed25519_sk_to_curve25519(
originatorSigningKeyPair.privateKey
);
const beneficiaryX25519PublicKey =
sodium.crypto_sign_ed25519_pk_to_curve25519(
beneficiaryEd25519PublicKey
);
const plaintext = Buffer.from(
JSON.stringify({
ivms101: params.ivms101
}),
'utf8'
);
const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
const ciphertext = sodium.crypto_box_easy(
plaintext,
nonce,
beneficiaryX25519PublicKey,
originatorX25519PrivateKey
);
const encryptedPayload = new Uint8Array(nonce.length + ciphertext.length);
encryptedPayload.set(nonce, 0);
encryptedPayload.set(ciphertext, nonce.length);
return Buffer.from(encryptedPayload).toString('base64');
}
Example usage:
const encryptedIvms101 = await encryptIncomingIvms101({
originatorPrivateKeyB64: '<originator VASP private key>',
beneficiaryPublicKeyB64: '<CryptoSwift beneficiary VASP public key>',
ivms101: {
Originator: {
// IVMS101 originator data
},
Beneficiary: {
// IVMS101 beneficiary data
}
}
});
The helper returns the concatenated nonce + ciphertext bytes as one base64 string. Map that returned string directly into the root-level "ivms101" field of the Partner transaction request.
Send the Encrypted Payload
{
"asset": "BTC",
"amount": 0.1,
"blockchainInfo": {
"...": "..."
},
"ivms101": "<encryptedIvms101>",
"originatorVaspId": "<originator VASP id>",
"originatorVaspPubKey": "<originator VASP public key base64>",
"beneficiaryVaspId": "<CryptoSwift beneficiary VASP id>",
"createdAt": "2026-05-14T12:00:00.000Z"
}
Key points:
- Encrypt only the inner IVMS101 payload, not the whole request body.
- The encrypted plaintext must be shaped as
{ "ivms101": { "Originator": ..., "Beneficiary": ... } }. - The top-level request field
ivms101must be the base64 string returned by the helper. originatorVaspPubKeymust be the public key matching the private key used for encryption.