1import * as Lambda from "@/AWS/Lambda";
2import * as PaymentCryptography from "@/AWS/PaymentCryptography";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Redacted from "effect/Redacted";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10
11const main = path.resolve(import.meta.dirname, "handler.ts");
12
13// Decoded distilled Sensitive* outputs are Redacted at runtime.
14const unwrap = (value: string | Redacted.Redacted<string>): string =>
15 Redacted.isRedacted(value) ? Redacted.value(value) : value;
16
17// Deterministic IV for the CBC round-trip routes (test-only).
18const IV = "00000000000000000000000000000000";
19
20export class PaymentCryptographyTestFunction extends Lambda.Function<Lambda.Function>()(
21 "PaymentCryptographyTestFunction",
22) {}
23
24export default PaymentCryptographyTestFunction.make(
25 {
26 main,
27 functionUrl: true,
28 timeout: Duration.seconds(30),
29 },
30 Effect.gen(function* () {
31 const dataKey = yield* PaymentCryptography.Key("DataKey", {
32 keyAttributes: {
33 keyAlgorithm: "AES_128",
34 keyClass: "SYMMETRIC_KEY",
35 keyUsage: "TR31_D0_SYMMETRIC_DATA_ENCRYPTION_KEY",
36 keyModesOfUse: {
37 encrypt: true,
38 decrypt: true,
39 wrap: true,
40 unwrap: true,
41 },
42 },
43 });
44 // DUKPT Base Derivation Key — the ReEncryptData incoming side (the
45 // service rejects symmetric->symmetric re-encryption; the operation
46 // translates DUKPT terminal ciphertext to a working key).
47 const bdk = yield* PaymentCryptography.Key("Bdk", {
48 keyAttributes: {
49 keyAlgorithm: "TDES_2KEY",
50 keyClass: "SYMMETRIC_KEY",
51 keyUsage: "TR31_B0_BASE_DERIVATION_KEY",
52 keyModesOfUse: { deriveKey: true },
53 },
54 });
55 const macKey = yield* PaymentCryptography.Key("MacKey", {
56 keyAttributes: {
57 keyAlgorithm: "HMAC_SHA256",
58 keyClass: "SYMMETRIC_KEY",
59 keyUsage: "TR31_M7_HMAC_KEY",
60 keyModesOfUse: { generate: true, verify: true },
61 },
62 });
63 // Card Verification Key for CVV2 generation/verification.
64 const cvk = yield* PaymentCryptography.Key("Cvk", {
65 keyAttributes: {
66 keyAlgorithm: "TDES_2KEY",
67 keyClass: "SYMMETRIC_KEY",
68 keyUsage: "TR31_C0_CARD_VERIFICATION_KEY",
69 keyModesOfUse: { generate: true, verify: true },
70 },
71 });
72 // PIN Encryption Keys — PIN blocks are encrypted/translated under these.
73 const pek = yield* PaymentCryptography.Key("Pek", {
74 keyAttributes: {
75 keyAlgorithm: "TDES_2KEY",
76 keyClass: "SYMMETRIC_KEY",
77 keyUsage: "TR31_P0_PIN_ENCRYPTION_KEY",
78 keyModesOfUse: {
79 encrypt: true,
80 decrypt: true,
81 wrap: true,
82 unwrap: true,
83 },
84 },
85 });
86 const pek2 = yield* PaymentCryptography.Key("Pek2", {
87 keyAttributes: {
88 keyAlgorithm: "TDES_2KEY",
89 keyClass: "SYMMETRIC_KEY",
90 keyUsage: "TR31_P0_PIN_ENCRYPTION_KEY",
91 keyModesOfUse: {
92 encrypt: true,
93 decrypt: true,
94 wrap: true,
95 unwrap: true,
96 },
97 },
98 });
99 // Visa PIN Verification Key — generates/verifies PVVs.
100 const pvk = yield* PaymentCryptography.Key("Pvk", {
101 keyAttributes: {
102 keyAlgorithm: "TDES_2KEY",
103 keyClass: "SYMMETRIC_KEY",
104 keyUsage: "TR31_V2_VISA_PIN_VERIFICATION_KEY",
105 keyModesOfUse: { generate: true, verify: true },
106 },
107 });
108 // Asymmetric signing key pair — GetPublicKeyCertificate target.
109 const signKey = yield* PaymentCryptography.Key("SignKey", {
110 keyAttributes: {
111 keyAlgorithm: "ECC_NIST_P256",
112 keyClass: "ASYMMETRIC_KEY_PAIR",
113 keyUsage: "TR31_S0_ASYMMETRIC_KEY_FOR_DIGITAL_SIGNATURE",
114 // S0 key pairs generated in-service sign with the private half; the
115 // service rejects sign+verify combined on this usage.
116 keyModesOfUse: { sign: true },
117 },
118 });
119
120 const encryptData = yield* PaymentCryptography.EncryptData(dataKey);
121 const encryptDukpt = yield* PaymentCryptography.EncryptData(bdk);
122 const decryptData = yield* PaymentCryptography.DecryptData(dataKey);
123 const generateMac = yield* PaymentCryptography.GenerateMac(macKey);
124 const verifyMac = yield* PaymentCryptography.VerifyMac(macKey);
125 const reEncryptData = yield* PaymentCryptography.ReEncryptData(
126 bdk,
127 dataKey,
128 );
129 const generateCardValidationData =
130 yield* PaymentCryptography.GenerateCardValidationData(cvk);
131 const verifyCardValidationData =
132 yield* PaymentCryptography.VerifyCardValidationData(cvk);
133 const generatePinData = yield* PaymentCryptography.GeneratePinData(
134 pvk,
135 pek,
136 );
137 const verifyPinData = yield* PaymentCryptography.VerifyPinData(pvk, pek);
138 const translatePinData = yield* PaymentCryptography.TranslatePinData(
139 pek,
140 pek2,
141 );
142 const getPublicKeyCertificate =
143 yield* PaymentCryptography.GetPublicKeyCertificate(signKey);
144
145 return {
146 fetch: Effect.gen(function* () {
147 const request = yield* HttpServerRequest;
148 const pathname = new URL(request.originalUrl).pathname;
149
150 if (request.method === "GET" && pathname === "/health") {
151 return yield* HttpServerResponse.json({ ok: true });
152 }
153
154 if (request.method === "POST" && pathname === "/encrypt-decrypt") {
155 const body = (yield* request.json) as unknown as {
156 plainTextHex: string;
157 };
158 const encrypted = yield* encryptData({
159 PlainText: body.plainTextHex,
160 EncryptionAttributes: {
161 Symmetric: { Mode: "CBC", InitializationVector: IV },
162 },
163 });
164 const cipherText = unwrap(encrypted.CipherText);
165 const decrypted = yield* decryptData({
166 CipherText: cipherText,
167 DecryptionAttributes: {
168 Symmetric: { Mode: "CBC", InitializationVector: IV },
169 },
170 });
171 return yield* HttpServerResponse.json({
172 keyArn: encrypted.KeyArn,
173 cipherText,
174 plainText: unwrap(decrypted.PlainText),
175 });
176 }
177
178 if (request.method === "POST" && pathname === "/re-encrypt") {
179 const body = (yield* request.json) as unknown as {
180 plainTextHex: string;
181 };
182 // Simulate a DUKPT terminal: encrypt under a key derived from the
183 // BDK at a fixed Key Serial Number.
184 const KSN = "FFFF9876543210E00001";
185 const encrypted = yield* encryptDukpt({
186 PlainText: body.plainTextHex,
187 EncryptionAttributes: {
188 Dukpt: { KeySerialNumber: KSN, Mode: "CBC" },
189 },
190 });
191 // Translate the DUKPT ciphertext to the symmetric working key
192 // inside the service — the plaintext never leaves Payment
193 // Cryptography.
194 const reEncrypted = yield* reEncryptData({
195 CipherText: unwrap(encrypted.CipherText),
196 IncomingEncryptionAttributes: {
197 Dukpt: { KeySerialNumber: KSN, Mode: "CBC" },
198 },
199 OutgoingEncryptionAttributes: {
200 Symmetric: { Mode: "CBC", InitializationVector: IV },
201 },
202 });
203 const decrypted = yield* decryptData({
204 CipherText: unwrap(reEncrypted.CipherText),
205 DecryptionAttributes: {
206 Symmetric: { Mode: "CBC", InitializationVector: IV },
207 },
208 });
209 return yield* HttpServerResponse.json({
210 outgoingKeyArn: reEncrypted.KeyArn,
211 plainText: unwrap(decrypted.PlainText),
212 });
213 }
214
215 if (request.method === "POST" && pathname === "/card") {
216 const body = (yield* request.json) as unknown as {
217 pan: string;
218 expiry: string;
219 };
220 const generated = yield* generateCardValidationData({
221 PrimaryAccountNumber: body.pan,
222 GenerationAttributes: {
223 CardVerificationValue2: { CardExpiryDate: body.expiry },
224 },
225 });
226 const cvv2 = unwrap(generated.ValidationData);
227 const verified = yield* verifyCardValidationData({
228 PrimaryAccountNumber: body.pan,
229 VerificationAttributes: {
230 CardVerificationValue2: { CardExpiryDate: body.expiry },
231 },
232 ValidationData: cvv2,
233 });
234 // A wrong CVV2 must fail with the typed
235 // VerificationFailedException.
236 const tamperedCvv2 = cvv2
237 .split("")
238 .map((d) => String((Number(d) + 1) % 10))
239 .join("");
240 const tampered = yield* verifyCardValidationData({
241 PrimaryAccountNumber: body.pan,
242 VerificationAttributes: {
243 CardVerificationValue2: { CardExpiryDate: body.expiry },
244 },
245 ValidationData: tamperedCvv2,
246 }).pipe(
247 Effect.map(() => "verified"),
248 Effect.catchTag("VerificationFailedException", () =>
249 Effect.succeed("verification-failed"),
250 ),
251 );
252 return yield* HttpServerResponse.json({
253 cvv2,
254 verifiedKeyArn: verified.KeyArn,
255 tampered,
256 });
257 }
258
259 if (request.method === "POST" && pathname === "/pin") {
260 const body = (yield* request.json) as unknown as { pan: string };
261 // Issue a Visa PIN: the PVK generates the PVV, the PEK encrypts
262 // the PIN block.
263 const generated = yield* generatePinData({
264 GenerationAttributes: { VisaPin: { PinVerificationKeyIndex: 1 } },
265 PrimaryAccountNumber: body.pan,
266 PinBlockFormat: "ISO_FORMAT_0",
267 });
268 const encryptedPinBlock = unwrap(generated.EncryptedPinBlock);
269 const pvv =
270 generated.PinData.VerificationValue === undefined
271 ? ""
272 : unwrap(generated.PinData.VerificationValue);
273 const verified = yield* verifyPinData({
274 VerificationAttributes: {
275 VisaPin: { PinVerificationKeyIndex: 1, VerificationValue: pvv },
276 },
277 EncryptedPinBlock: encryptedPinBlock,
278 PrimaryAccountNumber: body.pan,
279 PinBlockFormat: "ISO_FORMAT_0",
280 });
281 // Translate the PIN block from pek to pek2 (acquirer forwarding).
282 const translated = yield* translatePinData({
283 IncomingTranslationAttributes: {
284 IsoFormat0: { PrimaryAccountNumber: body.pan },
285 },
286 OutgoingTranslationAttributes: {
287 IsoFormat0: { PrimaryAccountNumber: body.pan },
288 },
289 EncryptedPinBlock: encryptedPinBlock,
290 });
291 return yield* HttpServerResponse.json({
292 pvv,
293 verificationKeyArn: verified.VerificationKeyArn,
294 translatedKeyArn: translated.KeyArn,
295 translatedPinBlock: unwrap(translated.PinBlock),
296 });
297 }
298
299 if (request.method === "GET" && pathname === "/public-key-cert") {
300 const certificate = yield* getPublicKeyCertificate();
301 return yield* HttpServerResponse.json({
302 keyCertificate: certificate.KeyCertificate,
303 keyCertificateChain: certificate.KeyCertificateChain,
304 });
305 }
306
307 if (request.method === "POST" && pathname === "/mac") {
308 const body = (yield* request.json) as unknown as {
309 messageDataHex: string;
310 };
311 // The hash function comes from the key's HMAC_SHA256 algorithm;
312 // the MAC attribute is plain "HMAC".
313 const generated = yield* generateMac({
314 MessageData: body.messageDataHex,
315 GenerationAttributes: { Algorithm: "HMAC" },
316 });
317 const mac = unwrap(generated.Mac);
318 const verified = yield* verifyMac({
319 MessageData: body.messageDataHex,
320 Mac: mac,
321 VerificationAttributes: { Algorithm: "HMAC" },
322 });
323 // A tampered MAC must fail with the typed
324 // VerificationFailedException.
325 const tamperedMac = (mac.startsWith("0") ? "1" : "0") + mac.slice(1);
326 const tampered = yield* verifyMac({
327 MessageData: body.messageDataHex,
328 Mac: tamperedMac,
329 VerificationAttributes: { Algorithm: "HMAC" },
330 }).pipe(
331 Effect.map(() => "verified"),
332 Effect.catchTag("VerificationFailedException", () =>
333 Effect.succeed("verification-failed"),
334 ),
335 );
336 return yield* HttpServerResponse.json({
337 mac,
338 verifiedKeyArn: verified.KeyArn,
339 tampered,
340 });
341 }
342
343 return yield* HttpServerResponse.json(
344 { error: "Not found" },
345 { status: 404 },
346 );
347 }).pipe(Effect.orDie),
348 };
349 }).pipe(
350 Effect.provide(
351 Layer.mergeAll(
352 PaymentCryptography.EncryptDataHttp,
353 PaymentCryptography.DecryptDataHttp,
354 PaymentCryptography.GenerateMacHttp,
355 PaymentCryptography.VerifyMacHttp,
356 PaymentCryptography.ReEncryptDataHttp,
357 PaymentCryptography.GenerateCardValidationDataHttp,
358 PaymentCryptography.VerifyCardValidationDataHttp,
359 PaymentCryptography.GeneratePinDataHttp,
360 PaymentCryptography.VerifyPinDataHttp,
361 PaymentCryptography.TranslatePinDataHttp,
362 PaymentCryptography.GetPublicKeyCertificateHttp,
363 ),
364 ),
365 ),
366);