⚠ This is an illustrative example, not production code. SwipeAras is not writing, maintaining, or supporting your integration — this fills a specific gap that neither Adyen's documentation nor a general library covers: converting the Public key format shown in Forge into the JWK format jose (and Adyen's own guide) expects. Have your own engineers review this before using any of it. It is not security advice, and it hasn't been reviewed for your specific application.
Adyen's Card encryption with JWE guide assumes you already have your public key as a JWK, or as an X.509 certificate. Forge's Adyen Integration Details page shows neither — it shows a Public key in the format 10001|B186CD8A... (an exponent and modulus, each in hexadecimal, separated by a pipe). Converting between the two is a small amount of encoding — no cryptography of its own — but it's a step nothing else documents.
import * as jose from 'jose';
/**
* Converts the Public key string from Forge's Adyen Integration Details page
* (format: "10001|B186CD8A...") into the JWK format jose's importJWK expects.
*/
function publicKeyToJwk(publicKeyString: string): { kty: 'RSA'; e: string; n: string } {
const [exponentHex, modulusHex] = publicKeyString.trim().split('|');
if (!exponentHex || !modulusHex) {
throw new Error('Expected format: EXPONENT_HEX|MODULUS_HEX');
}
return {
kty: 'RSA',
e: hexToBase64Url(exponentHex),
n: hexToBase64Url(modulusHex),
};
}
function hexToBase64Url(hex: string): string {
let h = hex.trim();
if (h.length % 2 !== 0) h = '0' + h;
const bytes = new Uint8Array(h.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(h.substr(i * 2, 2), 16);
}
// JWK numbers must be minimal big-endian - strip leading zero bytes.
let start = 0;
while (start < bytes.length - 1 && bytes[start] === 0) start++;
return Buffer.from(bytes.slice(start))
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
/**
* Encrypts card details into the JWE string Adyen expects at
* paymentMethod.encryptedCard. See Adyen's guide for the algorithm choices
* and payload shape - this wraps their example around the key conversion
* above.
*/
async function encryptCardDetails(
publicKeyString: string,
card: { number: string; expiryMonth: string; expiryYear: string; cvc: string }
): Promise<string> {
const jwk = publicKeyToJwk(publicKeyString);
const publicKey = await jose.importJWK(jwk, 'RSA-OAEP-256');
const payload = {
...card,
generationtime: new Date().toISOString(),
};
return await new jose.CompactEncrypt(
new TextEncoder().encode(JSON.stringify(payload))
)
.setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM', version: '1' })
.encrypt(publicKey);
}
Call encryptCardDetails with the Public key string from Forge and the shopper's card details, entered into your own form fields. The returned string is what goes into paymentMethod.encryptedCard on your payment request — see Adyen's guide for the exact request shape.
⚠ Again: example only. This snippet runs and produces a JWE that matches the shape Adyen expects, but "runs" isn't the same as "ready for your application." Error handling, input validation, where this code physically runs (it must be in the browser, never on your server — see From Postman to your own implementation), and how it fits your specific checkout flow are all decisions for your own engineering team.
Adyen: Card encryption with JWE — see "Encrypt card details" for the payload shape and algorithm requirements
jose on GitHub