@auxiora/vault 1.0.0

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,51 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { getVaultPath, isWindows } from '@auxiora/core';
4
+ export { getVaultPath };
5
+ export async function readVaultFile(customPath) {
6
+ const vaultPath = customPath || getVaultPath();
7
+ try {
8
+ const content = await fs.readFile(vaultPath, 'utf-8');
9
+ return JSON.parse(content);
10
+ }
11
+ catch (error) {
12
+ if (error.code === 'ENOENT') {
13
+ return null;
14
+ }
15
+ throw error;
16
+ }
17
+ }
18
+ export async function writeVaultFile(vaultFile, customPath) {
19
+ const vaultPath = customPath || getVaultPath();
20
+ const vaultDir = path.dirname(vaultPath);
21
+ // Create parent directories if needed
22
+ await fs.mkdir(vaultDir, { recursive: true });
23
+ // Write the file
24
+ await fs.writeFile(vaultPath, JSON.stringify(vaultFile, null, 2), 'utf-8');
25
+ // Set permissions to 0600 on Unix
26
+ if (!isWindows()) {
27
+ await fs.chmod(vaultPath, 0o600);
28
+ }
29
+ }
30
+ export async function deleteVaultFile(customPath) {
31
+ const vaultPath = customPath || getVaultPath();
32
+ try {
33
+ await fs.unlink(vaultPath);
34
+ }
35
+ catch (error) {
36
+ if (error.code !== 'ENOENT') {
37
+ throw error;
38
+ }
39
+ }
40
+ }
41
+ export async function vaultExists(customPath) {
42
+ const vaultPath = customPath || getVaultPath();
43
+ try {
44
+ await fs.access(vaultPath);
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAUxD,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,UAAmB;IACrD,MAAM,SAAS,GAAG,UAAU,IAAI,YAAY,EAAE,CAAC;IAE/C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAc,CAAC;IAC1C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACvD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAoB,EAAE,UAAmB;IAC5E,MAAM,SAAS,GAAG,UAAU,IAAI,YAAY,EAAE,CAAC;IAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAEzC,sCAAsC;IACtC,MAAM,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9C,iBAAiB;IACjB,MAAM,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAE3E,kCAAkC;IAClC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACjB,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACnC,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,UAAmB;IACvD,MAAM,SAAS,GAAG,UAAU,IAAI,YAAY,EAAE,CAAC;IAE/C,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACvD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,UAAmB;IACnD,MAAM,SAAS,GAAG,UAAU,IAAI,YAAY,EAAE,CAAC;IAE/C,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC3B,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,25 @@
1
+ export declare class VaultError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export interface VaultOptions {
5
+ path?: string;
6
+ }
7
+ export declare class Vault {
8
+ private key;
9
+ private salt;
10
+ private credentials;
11
+ private isUnlocked;
12
+ private vaultPath?;
13
+ constructor(options?: VaultOptions);
14
+ unlock(password: string): Promise<void>;
15
+ lock(): void;
16
+ private ensureUnlocked;
17
+ private save;
18
+ add(name: string, value: string): Promise<void>;
19
+ list(): string[];
20
+ remove(name: string): Promise<boolean>;
21
+ get(name: string): string | undefined;
22
+ has(name: string): boolean;
23
+ changePassword(newPassword: string): Promise<void>;
24
+ }
25
+ //# sourceMappingURL=vault.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault.d.ts","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"AAcA,qBAAa,UAAW,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,KAAK;IAChB,OAAO,CAAC,GAAG,CAAuB;IAClC,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAC,CAAS;gBAEf,OAAO,CAAC,EAAE,YAAY;IAI5B,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkC7C,IAAI,IAAI,IAAI;IAUZ,OAAO,CAAC,cAAc;YAMR,IAAI;IAkBZ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrD,IAAI,IAAI,MAAM,EAAE;IAKV,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAU5C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAKrC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAKpB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAezD"}
package/dist/vault.js ADDED
@@ -0,0 +1,115 @@
1
+ import { deriveKey, generateSalt, encrypt, decrypt, zeroBuffer, } from './crypto.js';
2
+ import { readVaultFile, writeVaultFile } from './storage.js';
3
+ export class VaultError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'VaultError';
7
+ }
8
+ }
9
+ export class Vault {
10
+ key = null;
11
+ salt = null;
12
+ credentials = {};
13
+ isUnlocked = false;
14
+ vaultPath;
15
+ constructor(options) {
16
+ this.vaultPath = options?.path;
17
+ }
18
+ async unlock(password) {
19
+ const vaultFile = await readVaultFile(this.vaultPath);
20
+ if (vaultFile === null) {
21
+ // New vault - create with this password
22
+ this.salt = generateSalt();
23
+ this.key = await deriveKey(password, this.salt);
24
+ this.credentials = {};
25
+ this.isUnlocked = true;
26
+ await this.save();
27
+ return;
28
+ }
29
+ // Existing vault - decrypt
30
+ this.salt = Buffer.from(vaultFile.salt, 'base64');
31
+ this.key = await deriveKey(password, this.salt);
32
+ const encryptedData = {
33
+ iv: Buffer.from(vaultFile.iv, 'base64'),
34
+ ciphertext: Buffer.from(vaultFile.data, 'base64'),
35
+ tag: Buffer.from(vaultFile.tag, 'base64'),
36
+ };
37
+ try {
38
+ const plaintext = decrypt(encryptedData, this.key);
39
+ const data = JSON.parse(plaintext.toString('utf-8'));
40
+ this.credentials = data.credentials;
41
+ this.isUnlocked = true;
42
+ }
43
+ catch {
44
+ this.lock();
45
+ throw new VaultError('Wrong password or corrupted vault');
46
+ }
47
+ }
48
+ lock() {
49
+ if (this.key) {
50
+ zeroBuffer(this.key);
51
+ this.key = null;
52
+ }
53
+ this.salt = null;
54
+ this.credentials = {};
55
+ this.isUnlocked = false;
56
+ }
57
+ ensureUnlocked() {
58
+ if (!this.isUnlocked || !this.key || !this.salt) {
59
+ throw new VaultError('Vault is locked');
60
+ }
61
+ }
62
+ async save() {
63
+ this.ensureUnlocked();
64
+ const data = { credentials: this.credentials };
65
+ const plaintext = Buffer.from(JSON.stringify(data), 'utf-8');
66
+ const encrypted = encrypt(plaintext, this.key);
67
+ const vaultFile = {
68
+ version: 1,
69
+ salt: this.salt.toString('base64'),
70
+ iv: encrypted.iv.toString('base64'),
71
+ data: encrypted.ciphertext.toString('base64'),
72
+ tag: encrypted.tag.toString('base64'),
73
+ };
74
+ await writeVaultFile(vaultFile, this.vaultPath);
75
+ }
76
+ async add(name, value) {
77
+ this.ensureUnlocked();
78
+ this.credentials[name] = value;
79
+ await this.save();
80
+ }
81
+ list() {
82
+ this.ensureUnlocked();
83
+ return Object.keys(this.credentials);
84
+ }
85
+ async remove(name) {
86
+ this.ensureUnlocked();
87
+ if (!(name in this.credentials)) {
88
+ return false;
89
+ }
90
+ delete this.credentials[name];
91
+ await this.save();
92
+ return true;
93
+ }
94
+ get(name) {
95
+ this.ensureUnlocked();
96
+ return this.credentials[name];
97
+ }
98
+ has(name) {
99
+ this.ensureUnlocked();
100
+ return name in this.credentials;
101
+ }
102
+ async changePassword(newPassword) {
103
+ this.ensureUnlocked();
104
+ // Zero the old key
105
+ if (this.key) {
106
+ zeroBuffer(this.key);
107
+ }
108
+ // Generate new salt and derive new key
109
+ this.salt = generateSalt();
110
+ this.key = await deriveKey(newPassword, this.salt);
111
+ // Re-save with new encryption
112
+ await this.save();
113
+ }
114
+ }
115
+ //# sourceMappingURL=vault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault.js","sourceRoot":"","sources":["../src/vault.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@auxiora/vault",
3
+ "version": "1.0.0",
4
+ "description": "Encrypted credential vault with AES-256-GCM and Argon2id",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "argon2": "^0.44.0",
16
+ "@auxiora/core": "1.0.0"
17
+ },
18
+ "engines": {
19
+ "node": ">=22.0.0"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "clean": "rm -rf dist",
24
+ "typecheck": "tsc --noEmit"
25
+ }
26
+ }
@@ -0,0 +1,129 @@
1
+ import * as crypto from 'node:crypto';
2
+
3
+ const ALGORITHM = 'aes-256-gcm';
4
+ const IV_LENGTH = 12;
5
+ const AUTH_TAG_LENGTH = 16;
6
+ const KEY_LENGTH = 32;
7
+
8
+ /**
9
+ * Encrypted envelope containing a data key encrypted by the master key,
10
+ * and the data encrypted by the data key.
11
+ */
12
+ export interface EncryptedEnvelope {
13
+ version: number;
14
+ /** Encrypted data key (base64) */
15
+ encryptedDataKey: string;
16
+ /** IV for data key encryption (base64) */
17
+ dataKeyIv: string;
18
+ /** Auth tag for data key encryption (base64) */
19
+ dataKeyTag: string;
20
+ /** Encrypted payload (base64) */
21
+ encryptedPayload: string;
22
+ /** IV for payload encryption (base64) */
23
+ payloadIv: string;
24
+ /** Auth tag for payload encryption (base64) */
25
+ payloadTag: string;
26
+ }
27
+
28
+ function encryptAes256Gcm(plaintext: Buffer, key: Buffer): { ciphertext: Buffer; iv: Buffer; tag: Buffer } {
29
+ const iv = crypto.randomBytes(IV_LENGTH);
30
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
31
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
32
+ const tag = cipher.getAuthTag();
33
+ return { ciphertext, iv, tag };
34
+ }
35
+
36
+ function decryptAes256Gcm(ciphertext: Buffer, key: Buffer, iv: Buffer, tag: Buffer): Buffer {
37
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
38
+ decipher.setAuthTag(tag);
39
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
40
+ }
41
+
42
+ /**
43
+ * CloudVault provides client-side AES-256-GCM encryption with envelope encryption.
44
+ * All data is encrypted before leaving the client, so the server never sees plaintext.
45
+ */
46
+ export class CloudVault {
47
+ /**
48
+ * Encrypt data using envelope encryption.
49
+ * Generates a random data key, encrypts the payload with it,
50
+ * then encrypts the data key with the master key.
51
+ */
52
+ static encrypt(plaintext: Buffer, masterKey: Buffer): EncryptedEnvelope {
53
+ // Generate a random data key
54
+ const dataKey = crypto.randomBytes(KEY_LENGTH);
55
+
56
+ // Encrypt the payload with the data key
57
+ const payload = encryptAes256Gcm(plaintext, dataKey);
58
+
59
+ // Encrypt the data key with the master key
60
+ const wrappedKey = encryptAes256Gcm(dataKey, masterKey);
61
+
62
+ // Zero the data key
63
+ dataKey.fill(0);
64
+
65
+ return {
66
+ version: 1,
67
+ encryptedDataKey: wrappedKey.ciphertext.toString('base64'),
68
+ dataKeyIv: wrappedKey.iv.toString('base64'),
69
+ dataKeyTag: wrappedKey.tag.toString('base64'),
70
+ encryptedPayload: payload.ciphertext.toString('base64'),
71
+ payloadIv: payload.iv.toString('base64'),
72
+ payloadTag: payload.tag.toString('base64'),
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Decrypt an envelope-encrypted payload.
78
+ */
79
+ static decrypt(envelope: EncryptedEnvelope, masterKey: Buffer): Buffer {
80
+ // Unwrap the data key
81
+ const dataKey = decryptAes256Gcm(
82
+ Buffer.from(envelope.encryptedDataKey, 'base64'),
83
+ masterKey,
84
+ Buffer.from(envelope.dataKeyIv, 'base64'),
85
+ Buffer.from(envelope.dataKeyTag, 'base64'),
86
+ );
87
+
88
+ // Decrypt the payload
89
+ const plaintext = decryptAes256Gcm(
90
+ Buffer.from(envelope.encryptedPayload, 'base64'),
91
+ dataKey,
92
+ Buffer.from(envelope.payloadIv, 'base64'),
93
+ Buffer.from(envelope.payloadTag, 'base64'),
94
+ );
95
+
96
+ // Zero the data key
97
+ dataKey.fill(0);
98
+
99
+ return plaintext;
100
+ }
101
+
102
+ /**
103
+ * Re-encrypt an envelope with a new master key (for key rotation).
104
+ * The data key is unwrapped with the old key and re-wrapped with the new key.
105
+ * The payload itself is not re-encrypted (only the key envelope changes).
106
+ */
107
+ static reEncrypt(envelope: EncryptedEnvelope, oldMasterKey: Buffer, newMasterKey: Buffer): EncryptedEnvelope {
108
+ // Unwrap data key with old master key
109
+ const dataKey = decryptAes256Gcm(
110
+ Buffer.from(envelope.encryptedDataKey, 'base64'),
111
+ oldMasterKey,
112
+ Buffer.from(envelope.dataKeyIv, 'base64'),
113
+ Buffer.from(envelope.dataKeyTag, 'base64'),
114
+ );
115
+
116
+ // Re-wrap data key with new master key
117
+ const wrappedKey = encryptAes256Gcm(dataKey, newMasterKey);
118
+
119
+ // Zero the data key
120
+ dataKey.fill(0);
121
+
122
+ return {
123
+ ...envelope,
124
+ encryptedDataKey: wrappedKey.ciphertext.toString('base64'),
125
+ dataKeyIv: wrappedKey.iv.toString('base64'),
126
+ dataKeyTag: wrappedKey.tag.toString('base64'),
127
+ };
128
+ }
129
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,63 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as argon2 from 'argon2';
3
+ import { zeroBuffer } from '@auxiora/core';
4
+
5
+ const ARGON2_MEMORY_COST = 65536; // 64MB
6
+ const ARGON2_TIME_COST = 3;
7
+ const ARGON2_PARALLELISM = 1;
8
+ const KEY_LENGTH = 32;
9
+ const IV_LENGTH = 12;
10
+ const AUTH_TAG_LENGTH = 16;
11
+
12
+ export async function deriveKey(password: string, salt: Buffer): Promise<Buffer> {
13
+ const key = await argon2.hash(password, {
14
+ type: argon2.argon2id,
15
+ salt,
16
+ memoryCost: ARGON2_MEMORY_COST,
17
+ timeCost: ARGON2_TIME_COST,
18
+ parallelism: ARGON2_PARALLELISM,
19
+ hashLength: KEY_LENGTH,
20
+ raw: true,
21
+ });
22
+ return key;
23
+ }
24
+
25
+ export function generateSalt(): Buffer {
26
+ return crypto.randomBytes(32);
27
+ }
28
+
29
+ export function generateIv(): Buffer {
30
+ return crypto.randomBytes(IV_LENGTH);
31
+ }
32
+
33
+ export interface EncryptedData {
34
+ iv: Buffer;
35
+ ciphertext: Buffer;
36
+ tag: Buffer;
37
+ }
38
+
39
+ export function encrypt(plaintext: Buffer, key: Buffer): EncryptedData {
40
+ const iv = generateIv();
41
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv, {
42
+ authTagLength: AUTH_TAG_LENGTH,
43
+ });
44
+
45
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
46
+ const tag = cipher.getAuthTag();
47
+
48
+ return { iv, ciphertext, tag };
49
+ }
50
+
51
+ export function decrypt(encryptedData: EncryptedData, key: Buffer): Buffer {
52
+ const { iv, ciphertext, tag } = encryptedData;
53
+
54
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv, {
55
+ authTagLength: AUTH_TAG_LENGTH,
56
+ });
57
+ decipher.setAuthTag(tag);
58
+
59
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
60
+ return plaintext;
61
+ }
62
+
63
+ export { zeroBuffer };
package/src/eject.ts ADDED
@@ -0,0 +1,55 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+
4
+ export interface EjectableData {
5
+ version: number;
6
+ exportedAt: string;
7
+ credentials: Record<string, string>;
8
+ metadata?: Record<string, unknown>;
9
+ }
10
+
11
+ /**
12
+ * EjectManager provides data portability — export all tenant data
13
+ * in a portable, decrypted format that can be imported elsewhere.
14
+ */
15
+ export class EjectManager {
16
+ /**
17
+ * Export all credentials to a portable JSON format.
18
+ */
19
+ static exportData(credentials: Record<string, string>, metadata?: Record<string, unknown>): EjectableData {
20
+ return {
21
+ version: 1,
22
+ exportedAt: new Date().toISOString(),
23
+ credentials: { ...credentials },
24
+ metadata,
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Save exported data to a file.
30
+ */
31
+ static async saveToFile(data: EjectableData, filePath: string): Promise<void> {
32
+ const dir = path.dirname(filePath);
33
+ await fs.mkdir(dir, { recursive: true });
34
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
35
+ }
36
+
37
+ /**
38
+ * Load exported data from a file.
39
+ */
40
+ static async loadFromFile(filePath: string): Promise<EjectableData> {
41
+ const content = await fs.readFile(filePath, 'utf-8');
42
+ const data = JSON.parse(content) as EjectableData;
43
+ if (!data.version || !data.credentials) {
44
+ throw new Error('Invalid eject file format');
45
+ }
46
+ return data;
47
+ }
48
+
49
+ /**
50
+ * Import credentials from ejected data into a vault-compatible format.
51
+ */
52
+ static getCredentials(data: EjectableData): Record<string, string> {
53
+ return { ...data.credentials };
54
+ }
55
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { Vault, VaultError, type VaultOptions } from './vault.js';
2
+ export {
3
+ deriveKey,
4
+ encrypt,
5
+ decrypt,
6
+ generateSalt,
7
+ generateIv,
8
+ zeroBuffer,
9
+ } from './crypto.js';
10
+ export { readVaultFile, writeVaultFile, deleteVaultFile, getVaultPath, vaultExists } from './storage.js';
11
+ export { CloudVault, type EncryptedEnvelope } from './cloud-vault.js';
12
+ export { KeyManager, type ExportedKey } from './key-management.js';
13
+ export { EjectManager, type EjectableData } from './eject.js';
@@ -0,0 +1,86 @@
1
+ import * as crypto from 'node:crypto';
2
+
3
+ const PBKDF2_ITERATIONS = 600000;
4
+ const KEY_LENGTH = 32;
5
+ const SALT_LENGTH = 32;
6
+
7
+ export interface ExportedKey {
8
+ version: number;
9
+ salt: string;
10
+ iterations: number;
11
+ keyHash: string;
12
+ }
13
+
14
+ /**
15
+ * KeyManager handles key derivation, rotation, and import/export
16
+ * for client-side cloud vault encryption.
17
+ */
18
+ export class KeyManager {
19
+ /**
20
+ * Derive a 256-bit key from a password using PBKDF2.
21
+ */
22
+ static async deriveKey(password: string, salt?: Buffer): Promise<{ key: Buffer; salt: Buffer }> {
23
+ const keySalt = salt ?? crypto.randomBytes(SALT_LENGTH);
24
+
25
+ const key = await new Promise<Buffer>((resolve, reject) => {
26
+ crypto.pbkdf2(password, keySalt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha512', (err, derivedKey) => {
27
+ if (err) reject(err);
28
+ else resolve(derivedKey);
29
+ });
30
+ });
31
+
32
+ return { key, salt: keySalt };
33
+ }
34
+
35
+ /**
36
+ * Generate a random 256-bit master key.
37
+ */
38
+ static generateKey(): Buffer {
39
+ return crypto.randomBytes(KEY_LENGTH);
40
+ }
41
+
42
+ /**
43
+ * Rotate a key: derive a new key from a new password.
44
+ * Returns both old and new keys for re-encryption.
45
+ */
46
+ static async rotateKey(
47
+ oldPassword: string,
48
+ oldSalt: Buffer,
49
+ newPassword: string,
50
+ ): Promise<{ oldKey: Buffer; newKey: Buffer; newSalt: Buffer }> {
51
+ const { key: oldKey } = await KeyManager.deriveKey(oldPassword, oldSalt);
52
+ const { key: newKey, salt: newSalt } = await KeyManager.deriveKey(newPassword);
53
+
54
+ return { oldKey, newKey, newSalt };
55
+ }
56
+
57
+ /**
58
+ * Export a key's metadata for storage (does NOT export the key itself).
59
+ * Stores the salt and a hash of the derived key for verification.
60
+ */
61
+ static exportKeyMetadata(salt: Buffer, key: Buffer): ExportedKey {
62
+ const keyHash = crypto.createHash('sha256').update(key).digest('hex');
63
+ return {
64
+ version: 1,
65
+ salt: salt.toString('base64'),
66
+ iterations: PBKDF2_ITERATIONS,
67
+ keyHash,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Import key metadata and verify a password against it.
73
+ */
74
+ static async verifyPassword(password: string, exported: ExportedKey): Promise<{ valid: boolean; key: Buffer }> {
75
+ const salt = Buffer.from(exported.salt, 'base64');
76
+ const { key } = await KeyManager.deriveKey(password, salt);
77
+ const keyHash = crypto.createHash('sha256').update(key).digest('hex');
78
+
79
+ if (keyHash === exported.keyHash) {
80
+ return { valid: true, key };
81
+ }
82
+
83
+ key.fill(0);
84
+ return { valid: false, key: Buffer.alloc(0) };
85
+ }
86
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,66 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { getVaultPath, isWindows } from '@auxiora/core';
4
+
5
+ export interface VaultFile {
6
+ version: number;
7
+ salt: string;
8
+ iv: string;
9
+ data: string;
10
+ tag: string;
11
+ }
12
+
13
+ export { getVaultPath };
14
+
15
+ export async function readVaultFile(customPath?: string): Promise<VaultFile | null> {
16
+ const vaultPath = customPath || getVaultPath();
17
+
18
+ try {
19
+ const content = await fs.readFile(vaultPath, 'utf-8');
20
+ return JSON.parse(content) as VaultFile;
21
+ } catch (error) {
22
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
23
+ return null;
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ export async function writeVaultFile(vaultFile: VaultFile, customPath?: string): Promise<void> {
30
+ const vaultPath = customPath || getVaultPath();
31
+ const vaultDir = path.dirname(vaultPath);
32
+
33
+ // Create parent directories if needed
34
+ await fs.mkdir(vaultDir, { recursive: true });
35
+
36
+ // Write the file
37
+ await fs.writeFile(vaultPath, JSON.stringify(vaultFile, null, 2), 'utf-8');
38
+
39
+ // Set permissions to 0600 on Unix
40
+ if (!isWindows()) {
41
+ await fs.chmod(vaultPath, 0o600);
42
+ }
43
+ }
44
+
45
+ export async function deleteVaultFile(customPath?: string): Promise<void> {
46
+ const vaultPath = customPath || getVaultPath();
47
+
48
+ try {
49
+ await fs.unlink(vaultPath);
50
+ } catch (error) {
51
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
52
+ throw error;
53
+ }
54
+ }
55
+ }
56
+
57
+ export async function vaultExists(customPath?: string): Promise<boolean> {
58
+ const vaultPath = customPath || getVaultPath();
59
+
60
+ try {
61
+ await fs.access(vaultPath);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }