meyi-vault-server-dev 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * AES-256-GCM encryption utilities.
3
+ *
4
+ * Design decisions:
5
+ * - AES-256-GCM provides authenticated encryption — gives both
6
+ * confidentiality AND integrity. A tampered ciphertext will throw
7
+ * on decrypt rather than silently return garbage.
8
+ * - 12-byte IV is the NIST-recommended length for GCM. Generates a
9
+ * fresh random IV per encryption — never reuse under the same key.
10
+ * - 16-byte (128-bit) auth tag — maximum GCM tag length.
11
+ * - All binary values stored as base64 strings for safe JSON/DB transport.
12
+ * - Key stored as a 64-char hex string (32 bytes).
13
+ */
14
+
15
+ import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
16
+
17
+ /**
18
+ * Generate a new random AES-256 vault key.
19
+ * Returns a 64-character hex string.
20
+ */
21
+ export function generateVaultKey() {
22
+ return randomBytes(32).toString('hex')
23
+ }
24
+
25
+ /**
26
+ * Encrypt a UTF-8 plaintext string.
27
+ * @param {string} plaintext
28
+ * @param {string} keyHex 64-char hex string (32 bytes)
29
+ * @returns {{ encrypted_data: string, iv: string, auth_tag: string }}
30
+ */
31
+ export function encrypt(plaintext, keyHex) {
32
+ const key = Buffer.from(keyHex, 'hex')
33
+ if (key.length !== 32) throw new Error('AES key must be 32 bytes (64 hex chars)')
34
+
35
+ const iv = randomBytes(12) // 96-bit IV — NIST recommended for GCM
36
+ const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 })
37
+
38
+ const ciphertext = Buffer.concat([
39
+ cipher.update(plaintext, 'utf8'),
40
+ cipher.final(),
41
+ ])
42
+
43
+ return {
44
+ encrypted_data: ciphertext.toString('base64'),
45
+ iv: iv.toString('base64'),
46
+ auth_tag: cipher.getAuthTag().toString('base64'),
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Decrypt AES-256-GCM ciphertext.
52
+ * Throws ERR_CRYPTO_INVALID_AUTH_TAG if the data was tampered with.
53
+ *
54
+ * @param {string} encryptedData base64 ciphertext
55
+ * @param {string} iv base64 12-byte IV
56
+ * @param {string} authTag base64 16-byte GCM auth tag
57
+ * @param {string} keyHex 64-char hex string
58
+ * @returns {string} decrypted UTF-8 string
59
+ */
60
+ export function decrypt(encryptedData, iv, authTag, keyHex) {
61
+ const key = Buffer.from(keyHex, 'hex')
62
+ if (key.length !== 32) throw new Error('AES key must be 32 bytes')
63
+
64
+ const decipher = createDecipheriv(
65
+ 'aes-256-gcm',
66
+ key,
67
+ Buffer.from(iv, 'base64'),
68
+ { authTagLength: 16 }
69
+ )
70
+ // Must set auth tag before final() — throws if tampered
71
+ decipher.setAuthTag(Buffer.from(authTag, 'base64'))
72
+
73
+ return Buffer.concat([
74
+ decipher.update(Buffer.from(encryptedData, 'base64')),
75
+ decipher.final(), // ← throws if auth tag mismatch
76
+ ]).toString('utf8')
77
+ }
78
+
79
+ /**
80
+ * Convert an expiresIn shorthand to a future Date.
81
+ * Returns null if no expiry requested.
82
+ *
83
+ * @param {'1h'|'8h'|'24h'|'7d'|'30d'|null} expiresIn
84
+ * @returns {Date|null}
85
+ */
86
+ export function expiresInToDate(expiresIn) {
87
+ if (!expiresIn) return null
88
+ const seconds = { '1h': 3600, '8h': 28800, '24h': 86400, '7d': 604800, '30d': 2592000 }
89
+
90
+ if (seconds[expiresIn]) {
91
+ return new Date(Date.now() + seconds[expiresIn] * 1000)
92
+ }
93
+
94
+ // Try parsing as ISO string for custom dates
95
+ const date = new Date(expiresIn)
96
+ return isNaN(date.getTime()) ? null : date
97
+ }