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.
- package/README.md +95 -0
- package/package.json +33 -0
- package/scratch/migrate-up.mjs +17 -0
- package/src/database/migrate.mjs +138 -0
- package/src/database/schema.mjs +75 -0
- package/src/index.mjs +135 -0
- package/src/routes/audit.mjs +80 -0
- package/src/routes/domains.mjs +80 -0
- package/src/routes/entries.mjs +217 -0
- package/src/routes/grants.mjs +136 -0
- package/src/routes/stats.mjs +50 -0
- package/src/routes/vaults.mjs +156 -0
- package/src/utils/audit.mjs +57 -0
- package/src/utils/auth.mjs +52 -0
- package/src/utils/crypto.mjs +97 -0
|
@@ -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
|
+
}
|