Skip to content

Commit cf27cb7

Browse files
committed
Create new security service
1 parent c815272 commit cf27cb7

File tree

1 file changed

+163
-0
lines changed

1 file changed

+163
-0
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* ######
3+
* ######
4+
* ############ ####( ###### #####. ###### ############ ############
5+
* ############# #####( ###### #####. ###### ############# #############
6+
* ###### #####( ###### #####. ###### ##### ###### ##### ######
7+
* ###### ###### #####( ###### #####. ###### ##### ##### ##### ######
8+
* ###### ###### #####( ###### #####. ###### ##### ##### ######
9+
* ############# ############# ############# ############# ##### ######
10+
* ############ ############ ############# ############ ##### ######
11+
* ######
12+
* #############
13+
* ############
14+
* Adyen NodeJS API Library
15+
* Copyright (c) 2025 Adyen B.V.
16+
* This file is open source and available under the MIT license.
17+
* See the LICENSE file for more info.
18+
*/
19+
20+
import { Cipher, createCipheriv, createDecipheriv, createHmac, randomBytes, timingSafeEqual } from "crypto";
21+
import NexoCryptoException from "../services/exception/nexoCryptoException";
22+
import {
23+
MessageHeader,
24+
NexoDerivedKey,
25+
SaleToPOISecuredMessage,
26+
SecurityTrailer,
27+
} from "../typings/terminal/models";
28+
import { EncryptionCredentialDetails } from "./encryptionCredentialDetails";
29+
import InvalidSecurityKeyException from "./exception/invalidSecurityKeyException";
30+
import NexoDerivedKeyGenerator from "./nexoDerivedKeyGenerator";
31+
import { NexoEnum } from "../constants/nexoConstants";
32+
33+
enum Modes {
34+
ENCRYPT,
35+
DECRYPT,
36+
}
37+
38+
/**
39+
* Handles encryption, decryption, and integrity validation
40+
* for Nexo SaleToPOI messages using AES and HMAC.
41+
*
42+
* - Derives keys from {@link EncryptionCredentialDetails}
43+
* - Encrypts and decrypts Nexo messages (AES-256-CBC)
44+
* - Generates and validates HMAC (SHA-256)
45+
* - Constructs and validates {@link SecurityTrailer}
46+
*/
47+
class NexoSecurityManager {
48+
/**
49+
* Encrypts a SaleToPOI message.
50+
*
51+
* @param messageHeader - The Nexo message header
52+
* @param saleToPoiMessageJson - The message body in JSON string format
53+
* @param credentials - The encryption credentials
54+
* @returns A secured SaleToPOI message with encrypted payload and security trailer
55+
*/
56+
public static encrypt(
57+
messageHeader: MessageHeader,
58+
saleToPoiMessageJson: string,
59+
credentials: EncryptionCredentialDetails,
60+
): SaleToPOISecuredMessage {
61+
const derivedKey: NexoDerivedKey = NexoDerivedKeyGenerator.deriveKeyMaterial(credentials.Passphrase);
62+
const saleToPoiMessageByteArray = Buffer.from(saleToPoiMessageJson, "utf-8");
63+
const ivNonce = NexoSecurityManager.generateRandomIvNonce();
64+
const encryptedSaleToPoiMessage = NexoSecurityManager.crypt(
65+
saleToPoiMessageByteArray,
66+
derivedKey,
67+
ivNonce,
68+
Modes.ENCRYPT,
69+
);
70+
const encryptedSaleToPoiMessageHmac = NexoSecurityManager.hmac(saleToPoiMessageByteArray, derivedKey);
71+
72+
const securityTrailer: SecurityTrailer = {
73+
AdyenCryptoVersion: credentials.AdyenCryptoVersion,
74+
Hmac: encryptedSaleToPoiMessageHmac.toString("base64"),
75+
KeyIdentifier: credentials.KeyIdentifier,
76+
KeyVersion: credentials.KeyVersion,
77+
Nonce: ivNonce.toString("base64"),
78+
};
79+
80+
return {
81+
MessageHeader: messageHeader,
82+
NexoBlob: encryptedSaleToPoiMessage.toString("base64"),
83+
SecurityTrailer: securityTrailer,
84+
};
85+
}
86+
87+
/**
88+
* Decrypts a secured SaleToPOI message and validates its HMAC.
89+
*
90+
* @param saleToPoiSecureMessage - The secured message to decrypt
91+
* @param credentials - The encryption credentials
92+
* @throws {InvalidSecurityKeyException} If the credentials are invalid
93+
* @throws {NexoCryptoException} If HMAC validation fails
94+
* @returns The decrypted SaleToPOI message as a UTF-8 string
95+
*/
96+
public static decrypt(
97+
saleToPoiSecureMessage: SaleToPOISecuredMessage,
98+
credentials: EncryptionCredentialDetails,
99+
): string {
100+
NexoSecurityManager.validateEncryptionCredentials(credentials);
101+
102+
const encryptedSaleToPoiMessageByteArray = Buffer.from(saleToPoiSecureMessage.NexoBlob, "base64");
103+
const derivedKey = NexoDerivedKeyGenerator.deriveKeyMaterial(credentials.Passphrase);
104+
const ivNonce = Buffer.from(saleToPoiSecureMessage.SecurityTrailer.Nonce, "base64");
105+
const decryptedSaleToPoiMessageByteArray = NexoSecurityManager.crypt(
106+
encryptedSaleToPoiMessageByteArray,
107+
derivedKey,
108+
ivNonce,
109+
Modes.DECRYPT,
110+
);
111+
112+
const receivedHmac = Buffer.from(saleToPoiSecureMessage.SecurityTrailer.Hmac, "base64");
113+
NexoSecurityManager.validateHmac(receivedHmac, decryptedSaleToPoiMessageByteArray, derivedKey);
114+
115+
return decryptedSaleToPoiMessageByteArray.toString("utf-8");
116+
}
117+
118+
private static validateEncryptionCredentials(credentials: EncryptionCredentialDetails): void {
119+
const isValid =
120+
credentials &&
121+
credentials.Passphrase &&
122+
credentials.KeyIdentifier &&
123+
!isNaN(credentials.KeyVersion) &&
124+
!isNaN(credentials.AdyenCryptoVersion);
125+
if (!isValid) {
126+
throw new InvalidSecurityKeyException("Invalid Encryption Credentials");
127+
}
128+
}
129+
130+
private static crypt(bytes: Buffer, dk: NexoDerivedKey, ivNonce: Buffer, mode: Modes): Buffer {
131+
const actualIV = Buffer.alloc(NexoEnum.IV_LENGTH);
132+
for (let i = 0; i < NexoEnum.IV_LENGTH; i++) {
133+
actualIV[i] = dk.iv[i] ^ ivNonce[i];
134+
}
135+
136+
const cipher = mode === Modes.ENCRYPT
137+
? createCipheriv("aes-256-cbc", dk.cipherKey, actualIV)
138+
: createDecipheriv("aes-256-cbc", dk.cipherKey, actualIV);
139+
140+
let encrypted = (cipher as Cipher).update(bytes);
141+
encrypted = Buffer.concat([encrypted, cipher.final()]);
142+
return encrypted;
143+
}
144+
145+
private static hmac(bytes: Buffer, derivedKey: NexoDerivedKey): Buffer {
146+
const mac = createHmac("sha256", derivedKey.hmacKey);
147+
return mac.update(bytes).digest();
148+
}
149+
150+
private static generateRandomIvNonce(): Buffer {
151+
return randomBytes(NexoEnum.IV_LENGTH);
152+
}
153+
154+
private static validateHmac(receivedHmac: Buffer, decryptedMessage: Buffer, derivedKey: NexoDerivedKey): void {
155+
const hmac = NexoSecurityManager.hmac(decryptedMessage, derivedKey);
156+
157+
if (!timingSafeEqual(hmac, receivedHmac)) {
158+
throw new NexoCryptoException("Hmac validation failed");
159+
}
160+
}
161+
}
162+
163+
export default NexoSecurityManager;

0 commit comments

Comments
 (0)