Skip to content

Commit a043394

Browse files
committed
feat: implement AES-256-GCM encryption and decryption functions with error handling
1 parent cbd9d91 commit a043394

3 files changed

Lines changed: 182 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ out
8989

9090
# Nuxt.js build / generate output
9191
.nuxt
92-
dist
92+
9393

9494
# Gatsby files
9595
.cache/

dist/AESEncrypt.d.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Error thrown when the padding in the decrypted data is invalid.
3+
*/
4+
export declare class InvalidPaddingError extends Error {
5+
constructor(message: string);
6+
}
7+
/**
8+
* Encrypts a plaintext string using AES-256-GCM with authenticated encryption (AEAD).
9+
*
10+
* Security features:
11+
* - AES-256-GCM: Provides both confidentiality and authenticity
12+
* - Random IV per encryption: Prevents pattern analysis
13+
* - Random salt: Ensures unique key derivation
14+
* - PBKDF2-SHA256 with 210,000 iterations: Resists brute-force attacks
15+
* - Authentication tag: Detects tampering
16+
*
17+
* Output format: Base64(salt || iv || authTag || ciphertext)
18+
*
19+
* @param plaintext - The sensitive text to encrypt (e.g., bank account number).
20+
* @param password - Strong password/key (min 16 chars recommended).
21+
* @returns Base64-encoded encrypted data with salt, IV, and auth tag.
22+
* @throws {Error} If plaintext or password is empty.
23+
*/
24+
export declare function encryptAES(plaintext: string, password: string): string;
25+
/**
26+
* Decrypts a Base64-encoded AES-256-GCM encrypted string with authentication verification.
27+
*
28+
* Security features:
29+
* - Verifies authentication tag before decryption (tamper detection)
30+
* - Extracts salt and IV from encrypted data
31+
* - Derives the same key using PBKDF2-SHA256
32+
* - Throws error if data has been modified
33+
*
34+
* @param encryptedBase64 - Base64 string containing salt, IV, auth tag, and ciphertext.
35+
* @param password - The same password used for encryption.
36+
* @returns The decrypted plaintext.
37+
* @throws {Error} If encrypted text/password is empty, data is corrupted, or authentication fails.
38+
*/
39+
export declare function decryptAES(encryptedBase64: string, password: string): string;

dist/AESEncrypt.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.decryptAES = exports.encryptAES = exports.InvalidPaddingError = void 0;
4+
const crypto_1 = require("crypto");
5+
// Security constants optimized for banking/financial data
6+
const ITERATION_COUNT = 210000; // OWASP 2023 recommendation for PBKDF2-SHA256
7+
const SALT_LENGTH = 32; // 256 bits for salt
8+
const KEY_LENGTH = 32; // AES-256 requires 32 bytes
9+
const IV_LENGTH = 12; // GCM standard IV length (96 bits)
10+
const AUTH_TAG_LENGTH = 16; // 128 bits authentication tag
11+
/**
12+
* Error thrown when the padding in the decrypted data is invalid.
13+
*/
14+
class InvalidPaddingError extends Error {
15+
constructor(message) {
16+
super(message);
17+
this.name = 'InvalidPaddingError';
18+
}
19+
}
20+
exports.InvalidPaddingError = InvalidPaddingError;
21+
/**
22+
* Derives a cryptographically strong encryption key from the provided password using PBKDF2-SHA256.
23+
* Uses a random salt to ensure unique key derivation for each encryption operation.
24+
*
25+
* @param password - The input password/key as a Buffer.
26+
* @param salt - Random salt for key derivation (must be stored with encrypted data).
27+
* @returns The derived 256-bit encryption key.
28+
* @throws {Error} If the input password is empty or salt is invalid.
29+
*/
30+
function deriveKey(password, salt) {
31+
if (password.length === 0) {
32+
throw new Error('Password cannot be empty');
33+
}
34+
if (salt.length !== SALT_LENGTH) {
35+
throw new Error(`Salt must be ${SALT_LENGTH} bytes`);
36+
}
37+
// PBKDF2 with SHA-256 and high iteration count (OWASP 2023 recommendation)
38+
return (0, crypto_1.pbkdf2Sync)(password, salt, ITERATION_COUNT, KEY_LENGTH, 'sha256');
39+
}
40+
/**
41+
* Encrypts a plaintext string using AES-256-GCM with authenticated encryption (AEAD).
42+
*
43+
* Security features:
44+
* - AES-256-GCM: Provides both confidentiality and authenticity
45+
* - Random IV per encryption: Prevents pattern analysis
46+
* - Random salt: Ensures unique key derivation
47+
* - PBKDF2-SHA256 with 210,000 iterations: Resists brute-force attacks
48+
* - Authentication tag: Detects tampering
49+
*
50+
* Output format: Base64(salt || iv || authTag || ciphertext)
51+
*
52+
* @param plaintext - The sensitive text to encrypt (e.g., bank account number).
53+
* @param password - Strong password/key (min 16 chars recommended).
54+
* @returns Base64-encoded encrypted data with salt, IV, and auth tag.
55+
* @throws {Error} If plaintext or password is empty.
56+
*/
57+
function encryptAES(plaintext, password) {
58+
if (!plaintext) {
59+
throw new Error('Plaintext cannot be empty');
60+
}
61+
if (!password) {
62+
throw new Error('Password cannot be empty');
63+
}
64+
if (password.length < 16) {
65+
throw new Error('Password must be at least 16 characters for banking-grade security');
66+
}
67+
// Generate cryptographically secure random values
68+
const salt = (0, crypto_1.randomBytes)(SALT_LENGTH);
69+
const iv = (0, crypto_1.randomBytes)(IV_LENGTH);
70+
// Derive encryption key from password
71+
const passwordBuffer = Buffer.from(password, 'utf8');
72+
const key = deriveKey(passwordBuffer, salt);
73+
// Encrypt with AES-256-GCM (Authenticated Encryption)
74+
const cipher = (0, crypto_1.createCipheriv)('aes-256-gcm', key, iv);
75+
const plaintextBuffer = Buffer.from(plaintext, 'utf8');
76+
const encrypted = Buffer.concat([
77+
cipher.update(plaintextBuffer),
78+
cipher.final()
79+
]);
80+
// Get authentication tag (proves data integrity)
81+
const authTag = cipher.getAuthTag();
82+
// Combine: salt || iv || authTag || encrypted
83+
// This allows decryption without storing salt/IV separately
84+
const combined = Buffer.concat([salt, iv, authTag, encrypted]);
85+
return combined.toString('base64');
86+
}
87+
exports.encryptAES = encryptAES;
88+
/**
89+
* Decrypts a Base64-encoded AES-256-GCM encrypted string with authentication verification.
90+
*
91+
* Security features:
92+
* - Verifies authentication tag before decryption (tamper detection)
93+
* - Extracts salt and IV from encrypted data
94+
* - Derives the same key using PBKDF2-SHA256
95+
* - Throws error if data has been modified
96+
*
97+
* @param encryptedBase64 - Base64 string containing salt, IV, auth tag, and ciphertext.
98+
* @param password - The same password used for encryption.
99+
* @returns The decrypted plaintext.
100+
* @throws {Error} If encrypted text/password is empty, data is corrupted, or authentication fails.
101+
*/
102+
function decryptAES(encryptedBase64, password) {
103+
if (!encryptedBase64) {
104+
throw new Error('Encrypted text cannot be empty');
105+
}
106+
if (!password) {
107+
throw new Error('Password cannot be empty');
108+
}
109+
const combined = Buffer.from(encryptedBase64, 'base64');
110+
// Minimum size check: salt + iv + authTag + at least 1 byte of data
111+
const minSize = SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH + 1;
112+
if (combined.length < minSize) {
113+
throw new Error('Invalid encrypted data: too short');
114+
}
115+
// Extract components from combined buffer
116+
let offset = 0;
117+
const salt = combined.subarray(offset, offset + SALT_LENGTH);
118+
offset += SALT_LENGTH;
119+
const iv = combined.subarray(offset, offset + IV_LENGTH);
120+
offset += IV_LENGTH;
121+
const authTag = combined.subarray(offset, offset + AUTH_TAG_LENGTH);
122+
offset += AUTH_TAG_LENGTH;
123+
const encrypted = combined.subarray(offset);
124+
// Derive the same key using extracted salt
125+
const passwordBuffer = Buffer.from(password, 'utf8');
126+
const key = deriveKey(passwordBuffer, salt);
127+
// Decrypt with authentication verification
128+
const decipher = (0, crypto_1.createDecipheriv)('aes-256-gcm', key, iv);
129+
decipher.setAuthTag(authTag);
130+
try {
131+
const decrypted = Buffer.concat([
132+
decipher.update(encrypted),
133+
decipher.final() // This will throw if authentication fails
134+
]);
135+
return decrypted.toString('utf8');
136+
}
137+
catch (error) {
138+
// Authentication failure or corrupted data
139+
throw new Error('Decryption failed: Invalid password or data has been tampered with');
140+
}
141+
}
142+
exports.decryptAES = decryptAES;

0 commit comments

Comments
 (0)