@tuskydp/cli 0.2.1 → 0.4.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/src/commands/account.d.ts.map +1 -1
  3. package/dist/src/commands/account.js +0 -1
  4. package/dist/src/commands/account.js.map +1 -1
  5. package/dist/src/commands/auth.d.ts.map +1 -1
  6. package/dist/src/commands/auth.js +8 -5
  7. package/dist/src/commands/auth.js.map +1 -1
  8. package/dist/src/commands/download.d.ts +1 -0
  9. package/dist/src/commands/download.d.ts.map +1 -1
  10. package/dist/src/commands/download.js +35 -22
  11. package/dist/src/commands/download.js.map +1 -1
  12. package/dist/src/commands/export.d.ts +9 -24
  13. package/dist/src/commands/export.d.ts.map +1 -1
  14. package/dist/src/commands/export.js +31 -59
  15. package/dist/src/commands/export.js.map +1 -1
  16. package/dist/src/commands/files.d.ts.map +1 -1
  17. package/dist/src/commands/files.js +91 -12
  18. package/dist/src/commands/files.js.map +1 -1
  19. package/dist/src/commands/folder.d.ts +3 -0
  20. package/dist/src/commands/folder.d.ts.map +1 -0
  21. package/dist/src/commands/folder.js +151 -0
  22. package/dist/src/commands/folder.js.map +1 -0
  23. package/dist/src/commands/mcp.d.ts.map +1 -1
  24. package/dist/src/commands/mcp.js +15 -9
  25. package/dist/src/commands/mcp.js.map +1 -1
  26. package/dist/src/commands/rehydrate.d.ts +1 -0
  27. package/dist/src/commands/rehydrate.d.ts.map +1 -1
  28. package/dist/src/commands/rehydrate.js +15 -7
  29. package/dist/src/commands/rehydrate.js.map +1 -1
  30. package/dist/src/commands/sui.d.ts +3 -0
  31. package/dist/src/commands/sui.d.ts.map +1 -0
  32. package/dist/src/commands/sui.js +64 -0
  33. package/dist/src/commands/sui.js.map +1 -0
  34. package/dist/src/commands/trash.d.ts +3 -0
  35. package/dist/src/commands/trash.d.ts.map +1 -0
  36. package/dist/src/commands/trash.js +109 -0
  37. package/dist/src/commands/trash.js.map +1 -0
  38. package/dist/src/commands/upload.d.ts +4 -0
  39. package/dist/src/commands/upload.d.ts.map +1 -1
  40. package/dist/src/commands/upload.js +82 -27
  41. package/dist/src/commands/upload.js.map +1 -1
  42. package/dist/src/commands/vault.d.ts.map +1 -1
  43. package/dist/src/commands/vault.js +2 -24
  44. package/dist/src/commands/vault.js.map +1 -1
  45. package/dist/src/commands/wallet.d.ts +3 -0
  46. package/dist/src/commands/wallet.d.ts.map +1 -0
  47. package/dist/src/commands/wallet.js +126 -0
  48. package/dist/src/commands/wallet.js.map +1 -0
  49. package/dist/src/commands/webhook.d.ts +3 -0
  50. package/dist/src/commands/webhook.d.ts.map +1 -0
  51. package/dist/src/commands/webhook.js +172 -0
  52. package/dist/src/commands/webhook.js.map +1 -0
  53. package/dist/src/config.d.ts +2 -2
  54. package/dist/src/config.d.ts.map +1 -1
  55. package/dist/src/config.js +2 -3
  56. package/dist/src/config.js.map +1 -1
  57. package/dist/src/index.js +19 -9
  58. package/dist/src/index.js.map +1 -1
  59. package/dist/src/lib/resolve.d.ts.map +1 -1
  60. package/dist/src/lib/resolve.js +4 -5
  61. package/dist/src/lib/resolve.js.map +1 -1
  62. package/dist/src/mcp/context.d.ts +1 -9
  63. package/dist/src/mcp/context.d.ts.map +1 -1
  64. package/dist/src/mcp/context.js +1 -2
  65. package/dist/src/mcp/context.js.map +1 -1
  66. package/dist/src/mcp/server.d.ts.map +1 -1
  67. package/dist/src/mcp/server.js +2 -59
  68. package/dist/src/mcp/server.js.map +1 -1
  69. package/dist/src/mcp/tools/account.d.ts.map +1 -1
  70. package/dist/src/mcp/tools/account.js +1 -3
  71. package/dist/src/mcp/tools/account.js.map +1 -1
  72. package/dist/src/mcp/tools/files.d.ts +2 -3
  73. package/dist/src/mcp/tools/files.d.ts.map +1 -1
  74. package/dist/src/mcp/tools/files.js +46 -49
  75. package/dist/src/mcp/tools/files.js.map +1 -1
  76. package/dist/src/mcp/tools/vaults.js +2 -2
  77. package/dist/src/mcp/tools/vaults.js.map +1 -1
  78. package/dist/src/seal.d.ts +16 -0
  79. package/dist/src/seal.d.ts.map +1 -1
  80. package/dist/src/seal.js +23 -0
  81. package/dist/src/seal.js.map +1 -1
  82. package/dist/src/tui/files-panel.d.ts +31 -2
  83. package/dist/src/tui/files-panel.d.ts.map +1 -1
  84. package/dist/src/tui/files-panel.js +119 -13
  85. package/dist/src/tui/files-panel.js.map +1 -1
  86. package/dist/src/tui/index.d.ts.map +1 -1
  87. package/dist/src/tui/index.js +252 -48
  88. package/dist/src/tui/index.js.map +1 -1
  89. package/dist/src/tui/overview.d.ts.map +1 -1
  90. package/dist/src/tui/overview.js +21 -9
  91. package/dist/src/tui/overview.js.map +1 -1
  92. package/dist/src/tui/trash-screen.d.ts +4 -0
  93. package/dist/src/tui/trash-screen.d.ts.map +1 -0
  94. package/dist/src/tui/trash-screen.js +190 -0
  95. package/dist/src/tui/trash-screen.js.map +1 -0
  96. package/dist/src/tui/vaults-panel.d.ts +8 -0
  97. package/dist/src/tui/vaults-panel.d.ts.map +1 -1
  98. package/dist/src/tui/vaults-panel.js +45 -6
  99. package/dist/src/tui/vaults-panel.js.map +1 -1
  100. package/dist/src/version.d.ts +2 -0
  101. package/dist/src/version.d.ts.map +1 -0
  102. package/dist/src/version.js +21 -0
  103. package/dist/src/version.js.map +1 -0
  104. package/package.json +3 -3
  105. package/src/__tests__/seal.test.ts +7 -54
  106. package/src/commands/account.ts +0 -1
  107. package/src/commands/auth.ts +7 -5
  108. package/src/commands/download.ts +38 -28
  109. package/src/commands/export.ts +37 -81
  110. package/src/commands/files.ts +95 -11
  111. package/src/commands/folder.ts +169 -0
  112. package/src/commands/mcp.ts +16 -10
  113. package/src/commands/rehydrate.ts +15 -8
  114. package/src/commands/sui.ts +69 -0
  115. package/src/commands/trash.ts +121 -0
  116. package/src/commands/upload.ts +98 -31
  117. package/src/commands/vault.ts +2 -23
  118. package/src/commands/wallet.ts +183 -0
  119. package/src/commands/webhook.ts +193 -0
  120. package/src/config.ts +3 -4
  121. package/src/index.ts +19 -10
  122. package/src/lib/resolve.ts +3 -4
  123. package/src/mcp/context.ts +1 -11
  124. package/src/mcp/server.ts +2 -70
  125. package/src/mcp/tools/account.ts +1 -3
  126. package/src/mcp/tools/files.ts +50 -63
  127. package/src/mcp/tools/vaults.ts +3 -3
  128. package/src/seal.ts +34 -1
  129. package/src/tui/files-panel.ts +140 -14
  130. package/src/tui/index.ts +264 -52
  131. package/src/tui/overview.ts +20 -9
  132. package/src/tui/trash-screen.ts +203 -0
  133. package/src/tui/vaults-panel.ts +55 -6
  134. package/src/version.ts +21 -0
  135. package/vitest.config.ts +1 -0
  136. package/dist/src/client.d.ts +0 -120
  137. package/dist/src/client.d.ts.map +0 -1
  138. package/dist/src/client.js +0 -152
  139. package/dist/src/client.js.map +0 -1
  140. package/dist/src/commands/decrypt.d.ts +0 -15
  141. package/dist/src/commands/decrypt.d.ts.map +0 -1
  142. package/dist/src/commands/decrypt.js +0 -224
  143. package/dist/src/commands/decrypt.js.map +0 -1
  144. package/dist/src/commands/encryption.d.ts +0 -3
  145. package/dist/src/commands/encryption.d.ts.map +0 -1
  146. package/dist/src/commands/encryption.js +0 -254
  147. package/dist/src/commands/encryption.js.map +0 -1
  148. package/dist/src/crypto.d.ts +0 -16
  149. package/dist/src/crypto.d.ts.map +0 -1
  150. package/dist/src/crypto.js +0 -95
  151. package/dist/src/crypto.js.map +0 -1
  152. package/dist/src/lib/keyring.d.ts +0 -4
  153. package/dist/src/lib/keyring.d.ts.map +0 -1
  154. package/dist/src/lib/keyring.js +0 -49
  155. package/dist/src/lib/keyring.js.map +0 -1
  156. package/src/__tests__/crypto.test.ts +0 -315
  157. package/src/commands/decrypt.ts +0 -276
  158. package/src/commands/encryption.ts +0 -305
  159. package/src/crypto.ts +0 -130
  160. package/src/lib/keyring.ts +0 -50
@@ -1,305 +0,0 @@
1
- import type { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import inquirer from 'inquirer';
4
- import { getSDKClientFromParent } from '../sdk.js';
5
- import {
6
- deriveMasterKey,
7
- computeVerifier,
8
- verifyPassphrase,
9
- generateSalt,
10
- generateMasterKey,
11
- generateRecoveryKey,
12
- wrapMasterKey,
13
- unwrapMasterKey,
14
- } from '../crypto.js';
15
- import { storeMasterKey, loadMasterKey, clearSession } from '../lib/keyring.js';
16
-
17
- export function registerEncryptionCommands(program: Command) {
18
- const encryption = program.command('encryption').description('Manage E2E encryption');
19
-
20
- encryption.command('setup')
21
- .description('Set encryption passphrase (first time only)')
22
- .action(async () => {
23
- const sdk = getSDKClientFromParent(encryption);
24
-
25
- // Check if already set up
26
- const { setupComplete } = await sdk.account.getEncryptionParams();
27
- if (setupComplete) {
28
- console.log(chalk.yellow('Encryption is already set up.'));
29
- console.log(chalk.dim('Use: tusky encryption change-passphrase'));
30
- return;
31
- }
32
-
33
- // Get passphrase
34
- const answers = await inquirer.prompt([
35
- {
36
- type: 'password',
37
- name: 'passphrase',
38
- message: 'Enter encryption passphrase:',
39
- mask: '*',
40
- validate: (input: string) => input.length >= 8 || 'Passphrase must be at least 8 characters',
41
- },
42
- {
43
- type: 'password',
44
- name: 'confirm',
45
- message: 'Confirm passphrase:',
46
- mask: '*',
47
- },
48
- ]);
49
-
50
- if (answers.passphrase !== answers.confirm) {
51
- console.error(chalk.red('Passphrases do not match.'));
52
- return;
53
- }
54
-
55
- // Generate random master key and derive wrapping key from passphrase
56
- const masterKey = generateMasterKey();
57
- const salt = generateSalt();
58
- const wrappingKey = deriveMasterKey(answers.passphrase, salt);
59
- const verifier = computeVerifier(wrappingKey);
60
-
61
- // Wrap master key with wrapping key (for passphrase unlock)
62
- const encryptedMasterKey = wrapMasterKey(masterKey, wrappingKey);
63
-
64
- // Wrap master key with recovery key (for recovery flow)
65
- const recoveryKeyRaw = generateRecoveryKey();
66
- const wrappedMasterKeyBackup = wrapMasterKey(masterKey, recoveryKeyRaw);
67
- const recoveryKeyBase64 = recoveryKeyRaw.toString('base64');
68
-
69
- // Send to server
70
- await sdk.account.setupEncryption({
71
- salt: salt.toString('base64'),
72
- verifier: verifier.toString('base64'),
73
- encryptedMasterKey: encryptedMasterKey.toString('base64'),
74
- wrappedMasterKeyBackup: wrappedMasterKeyBackup.toString('base64'),
75
- });
76
-
77
- // Store master key in session
78
- storeMasterKey(masterKey);
79
-
80
- // Display recovery key
81
- console.log('');
82
- console.log(chalk.yellow.bold('SAVE YOUR RECOVERY KEY — it will not be shown again:'));
83
- console.log('');
84
- console.log(chalk.bold(` ${recoveryKeyBase64}`));
85
- console.log('');
86
-
87
- const confirmSaved = await inquirer.prompt([{
88
- type: 'confirm',
89
- name: 'saved',
90
- message: 'Have you saved your recovery key?',
91
- default: false,
92
- }]);
93
-
94
- if (!confirmSaved.saved) {
95
- console.log(chalk.yellow('Please save your recovery key before continuing!'));
96
- console.log(chalk.bold(` ${recoveryKeyBase64}`));
97
- }
98
-
99
- console.log(chalk.green('Encryption configured successfully.'));
100
- });
101
-
102
- encryption.command('unlock')
103
- .description('Unlock encryption for this session')
104
- .option('--passphrase <passphrase>', 'Passphrase (non-interactive; also reads TUSKYDP_PASSWORD env var)')
105
- .action(async (options: { passphrase?: string }) => {
106
- const sdk = getSDKClientFromParent(encryption);
107
-
108
- const { setupComplete, salt, verifier, encryptedMasterKey } = await sdk.account.getEncryptionParams();
109
- if (!setupComplete) {
110
- console.error(chalk.red('Encryption not set up. Run: tusky encryption setup'));
111
- return;
112
- }
113
-
114
- // Resolve passphrase: flag > env var > interactive prompt
115
- let passphrase = options.passphrase ?? process.env.TUSKYDP_PASSWORD;
116
- if (!passphrase) {
117
- const answers = await inquirer.prompt([{
118
- type: 'password',
119
- name: 'passphrase',
120
- message: 'Enter encryption passphrase:',
121
- mask: '*',
122
- }]);
123
- passphrase = answers.passphrase as string;
124
- }
125
-
126
- const saltBuffer = Buffer.from(salt!, 'base64');
127
- const verifierBuffer = Buffer.from(verifier!, 'base64');
128
- const wrappingKey = deriveMasterKey(passphrase, saltBuffer);
129
-
130
- if (!verifyPassphrase(wrappingKey, verifierBuffer)) {
131
- console.error(chalk.red('Invalid passphrase.'));
132
- process.exit(1);
133
- }
134
-
135
- // Unwrap the real master key (or fall back to legacy where wrapping key = master key)
136
- let masterKey: Buffer;
137
- if (encryptedMasterKey) {
138
- masterKey = unwrapMasterKey(Buffer.from(encryptedMasterKey, 'base64'), wrappingKey);
139
- } else {
140
- masterKey = wrappingKey;
141
- }
142
-
143
- storeMasterKey(masterKey);
144
- console.log(chalk.green('Encryption session unlocked.'));
145
- });
146
-
147
- encryption.command('status')
148
- .description('Show encryption status')
149
- .action(async () => {
150
- const sdk = getSDKClientFromParent(encryption);
151
- const { setupComplete } = await sdk.account.getEncryptionParams();
152
- const sessionActive = loadMasterKey() !== null;
153
-
154
- console.log(`Encryption setup: ${setupComplete ? chalk.green('Complete') : chalk.yellow('Not set up')}`);
155
- console.log(`Session unlocked: ${sessionActive ? chalk.green('Yes') : chalk.yellow('No')}`);
156
- });
157
-
158
- encryption.command('change-passphrase')
159
- .description('Change encryption passphrase')
160
- .action(async () => {
161
- const sdk = getSDKClientFromParent(encryption);
162
-
163
- const { setupComplete, salt, verifier, encryptedMasterKey } = await sdk.account.getEncryptionParams();
164
- if (!setupComplete) {
165
- console.error(chalk.red('Encryption not set up. Run: tusky encryption setup'));
166
- return;
167
- }
168
-
169
- const answers = await inquirer.prompt([
170
- {
171
- type: 'password',
172
- name: 'currentPassphrase',
173
- message: 'Enter current passphrase:',
174
- mask: '*',
175
- },
176
- {
177
- type: 'password',
178
- name: 'newPassphrase',
179
- message: 'Enter new passphrase:',
180
- mask: '*',
181
- validate: (input: string) => input.length >= 8 || 'Passphrase must be at least 8 characters',
182
- },
183
- {
184
- type: 'password',
185
- name: 'confirmNew',
186
- message: 'Confirm new passphrase:',
187
- mask: '*',
188
- },
189
- ]);
190
-
191
- if (answers.newPassphrase !== answers.confirmNew) {
192
- console.error(chalk.red('New passphrases do not match.'));
193
- return;
194
- }
195
-
196
- // Verify current passphrase
197
- const currentSalt = Buffer.from(salt!, 'base64');
198
- const currentVerifier = Buffer.from(verifier!, 'base64');
199
- const currentWrappingKey = deriveMasterKey(answers.currentPassphrase, currentSalt);
200
-
201
- if (!verifyPassphrase(currentWrappingKey, currentVerifier)) {
202
- console.error(chalk.red('Invalid current passphrase.'));
203
- return;
204
- }
205
-
206
- // Unwrap the real master key (or use wrapping key as legacy fallback)
207
- let masterKey: Buffer;
208
- if (encryptedMasterKey) {
209
- masterKey = unwrapMasterKey(Buffer.from(encryptedMasterKey, 'base64'), currentWrappingKey);
210
- } else {
211
- masterKey = currentWrappingKey;
212
- }
213
-
214
- // Derive new wrapping key, re-wrap master key
215
- const newSalt = generateSalt();
216
- const newWrappingKey = deriveMasterKey(answers.newPassphrase, newSalt);
217
- const newVerifier = computeVerifier(newWrappingKey);
218
- const newEncryptedMasterKey = wrapMasterKey(masterKey, newWrappingKey);
219
-
220
- // Generate new recovery key and wrap master key with it
221
- const newRecoveryKey = generateRecoveryKey();
222
- const newWrappedBackup = wrapMasterKey(masterKey, newRecoveryKey);
223
-
224
- await sdk.account.resetEncryption({
225
- salt: newSalt.toString('base64'),
226
- verifier: newVerifier.toString('base64'),
227
- encryptedMasterKey: newEncryptedMasterKey.toString('base64'),
228
- wrappedMasterKeyBackup: newWrappedBackup.toString('base64'),
229
- });
230
-
231
- storeMasterKey(masterKey);
232
-
233
- console.log(chalk.yellow.bold('NEW RECOVERY KEY (save it!):'));
234
- console.log(chalk.bold(` ${newRecoveryKey.toString('base64')}`));
235
- console.log(chalk.green('Passphrase changed successfully.'));
236
- });
237
-
238
- encryption.command('recover')
239
- .description('Recover master key with recovery key')
240
- .action(async () => {
241
- const sdk = getSDKClientFromParent(encryption);
242
-
243
- const answers = await inquirer.prompt([
244
- {
245
- type: 'input',
246
- name: 'recoveryKey',
247
- message: 'Enter your recovery key (base64):',
248
- },
249
- {
250
- type: 'password',
251
- name: 'newPassphrase',
252
- message: 'Enter new passphrase:',
253
- mask: '*',
254
- validate: (input: string) => input.length >= 8 || 'Passphrase must be at least 8 characters',
255
- },
256
- {
257
- type: 'password',
258
- name: 'confirmNew',
259
- message: 'Confirm new passphrase:',
260
- mask: '*',
261
- },
262
- ]);
263
-
264
- if (answers.newPassphrase !== answers.confirmNew) {
265
- console.error(chalk.red('Passphrases do not match.'));
266
- return;
267
- }
268
-
269
- // Get wrapped master key backup from server
270
- const { wrappedMasterKeyBackup } = await sdk.account.getRecoveryData();
271
- const wrappedData = Buffer.from(wrappedMasterKeyBackup!, 'base64');
272
- const recoveryKeyRaw = Buffer.from(answers.recoveryKey, 'base64');
273
-
274
- let masterKey: Buffer;
275
- try {
276
- masterKey = unwrapMasterKey(wrappedData, recoveryKeyRaw);
277
- } catch {
278
- console.error(chalk.red('Invalid recovery key.'));
279
- return;
280
- }
281
-
282
- // Derive new wrapping key from new passphrase, wrap the RECOVERED master key
283
- const newSalt = generateSalt();
284
- const newWrappingKey = deriveMasterKey(answers.newPassphrase, newSalt);
285
- const newVerifier = computeVerifier(newWrappingKey);
286
- const newEncryptedMasterKey = wrapMasterKey(masterKey, newWrappingKey);
287
-
288
- // Generate new recovery key and wrap master key with it
289
- const newRecoveryKey = generateRecoveryKey();
290
- const newWrappedBackup = wrapMasterKey(masterKey, newRecoveryKey);
291
-
292
- await sdk.account.resetEncryption({
293
- salt: newSalt.toString('base64'),
294
- verifier: newVerifier.toString('base64'),
295
- encryptedMasterKey: newEncryptedMasterKey.toString('base64'),
296
- wrappedMasterKeyBackup: newWrappedBackup.toString('base64'),
297
- });
298
-
299
- storeMasterKey(masterKey);
300
-
301
- console.log(chalk.yellow.bold('NEW RECOVERY KEY (save it!):'));
302
- console.log(chalk.bold(` ${newRecoveryKey.toString('base64')}`));
303
- console.log(chalk.green('Recovery successful. Passphrase reset.'));
304
- });
305
- }
package/src/crypto.ts DELETED
@@ -1,130 +0,0 @@
1
- import {
2
- createHash,
3
- createHmac,
4
- randomBytes,
5
- createCipheriv,
6
- createDecipheriv,
7
- pbkdf2Sync,
8
- } from 'crypto';
9
-
10
- const PBKDF2_ITERATIONS = 600_000;
11
- const AES_ALGO = 'aes-256-gcm' as const;
12
- const VERIFIER_CONTEXT = 'tuskydp-key-verifier-v1';
13
-
14
- export function deriveMasterKey(passphrase: string, salt: Buffer): Buffer {
15
- return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, 32, 'sha256');
16
- }
17
-
18
- export function computeVerifier(masterKey: Buffer): Buffer {
19
- return createHmac('sha256', masterKey)
20
- .update(VERIFIER_CONTEXT)
21
- .digest();
22
- }
23
-
24
- export function verifyPassphrase(masterKey: Buffer, expectedVerifier: Buffer): boolean {
25
- const computed = computeVerifier(masterKey);
26
- if (computed.length !== expectedVerifier.length) return false;
27
- let diff = 0;
28
- for (let i = 0; i < computed.length; i++) diff |= computed[i] ^ expectedVerifier[i];
29
- return diff === 0;
30
- }
31
-
32
- export function generateSalt(): Buffer {
33
- return randomBytes(16);
34
- }
35
-
36
- export function generateRecoveryKey(): Buffer {
37
- return randomBytes(32);
38
- }
39
-
40
- export function generateMasterKey(): Buffer {
41
- return randomBytes(32);
42
- }
43
-
44
- export function wrapMasterKey(masterKey: Buffer, recoveryKey: Buffer): Buffer {
45
- const wrapIv = randomBytes(12);
46
- const cipher = createCipheriv(AES_ALGO, recoveryKey, wrapIv);
47
- const wrapped = Buffer.concat([cipher.update(masterKey), cipher.final()]);
48
- const tag = cipher.getAuthTag();
49
- return Buffer.concat([wrapIv, wrapped, tag]);
50
- }
51
-
52
- export function unwrapMasterKey(wrappedData: Buffer, recoveryKey: Buffer): Buffer {
53
- const wrapIv = wrappedData.subarray(0, 12);
54
- const wrapped = wrappedData.subarray(12, wrappedData.length - 16);
55
- const tag = wrappedData.subarray(wrappedData.length - 16);
56
- const decipher = createDecipheriv(AES_ALGO, recoveryKey, wrapIv);
57
- decipher.setAuthTag(tag);
58
- return Buffer.concat([decipher.update(wrapped), decipher.final()]);
59
- }
60
-
61
- export function encryptBuffer(plaintext: Buffer, masterKey: Buffer): {
62
- ciphertext: Buffer;
63
- wrappedKey: string;
64
- iv: string;
65
- plaintextChecksum: string;
66
- } {
67
- // Hash plaintext
68
- const plaintextChecksum = createHash('sha256').update(plaintext).digest('hex');
69
-
70
- // Generate random per-file key and IV
71
- const fileKey = randomBytes(32);
72
- const fileIv = randomBytes(12);
73
-
74
- // Encrypt file
75
- const cipher = createCipheriv(AES_ALGO, fileKey, fileIv);
76
- const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
77
- const authTag = cipher.getAuthTag();
78
- const ciphertext = Buffer.concat([encrypted, authTag]);
79
-
80
- // Wrap file key with master key
81
- const wrapIv = randomBytes(12);
82
- const wrapCipher = createCipheriv(AES_ALGO, masterKey, wrapIv);
83
- const wrappedKeyData = Buffer.concat([wrapCipher.update(fileKey), wrapCipher.final()]);
84
- const wrapTag = wrapCipher.getAuthTag();
85
- const wrappedKeyFull = Buffer.concat([wrapIv, wrappedKeyData, wrapTag]);
86
-
87
- return {
88
- ciphertext,
89
- wrappedKey: wrappedKeyFull.toString('base64'),
90
- iv: fileIv.toString('base64'),
91
- plaintextChecksum,
92
- };
93
- }
94
-
95
- export function decryptBuffer(
96
- ciphertext: Buffer,
97
- wrappedKeyBase64: string,
98
- ivBase64: string,
99
- masterKey: Buffer,
100
- expectedChecksum?: string,
101
- ): Buffer {
102
- // Unwrap file key
103
- const wrappedKeyFull = Buffer.from(wrappedKeyBase64, 'base64');
104
- const wrapIv = wrappedKeyFull.subarray(0, 12);
105
- const wrappedKeyData = wrappedKeyFull.subarray(12, wrappedKeyFull.length - 16);
106
- const wrapTag = wrappedKeyFull.subarray(wrappedKeyFull.length - 16);
107
-
108
- const unwrapDecipher = createDecipheriv(AES_ALGO, masterKey, wrapIv);
109
- unwrapDecipher.setAuthTag(wrapTag);
110
- const fileKey = Buffer.concat([unwrapDecipher.update(wrappedKeyData), unwrapDecipher.final()]);
111
-
112
- // Decrypt file
113
- const fileIv = Buffer.from(ivBase64, 'base64');
114
- const authTag = ciphertext.subarray(ciphertext.length - 16);
115
- const encryptedData = ciphertext.subarray(0, ciphertext.length - 16);
116
-
117
- const decipher = createDecipheriv(AES_ALGO, fileKey, fileIv);
118
- decipher.setAuthTag(authTag);
119
- const plaintext = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
120
-
121
- // Verify integrity
122
- if (expectedChecksum) {
123
- const actualChecksum = createHash('sha256').update(plaintext).digest('hex');
124
- if (actualChecksum !== expectedChecksum) {
125
- throw new Error('Integrity check failed — file may be corrupted');
126
- }
127
- }
128
-
129
- return plaintext;
130
- }
@@ -1,50 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
2
- import { join } from 'path';
3
- import { homedir } from 'os';
4
- import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'crypto';
5
-
6
- const SESSION_DIR = join(homedir(), '.tusky');
7
- const SESSION_FILE = join(SESSION_DIR, 'session.enc');
8
-
9
- // Derive a machine-specific key from hostname + user
10
- function getMachineKey(): Buffer {
11
- const machineId = `${homedir()}-tusky-session-key`;
12
- return createHash('sha256').update(machineId).digest();
13
- }
14
-
15
- export function storeMasterKey(masterKey: Buffer): void {
16
- mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
17
- const key = getMachineKey();
18
- const iv = randomBytes(12);
19
- const cipher = createCipheriv('aes-256-gcm', key, iv);
20
- const encrypted = Buffer.concat([cipher.update(masterKey), cipher.final()]);
21
- const tag = cipher.getAuthTag();
22
- const data = Buffer.concat([iv, tag, encrypted]);
23
- writeFileSync(SESSION_FILE, data, { mode: 0o600 });
24
- }
25
-
26
- export function loadMasterKey(): Buffer | null {
27
- if (!existsSync(SESSION_FILE)) return null;
28
- try {
29
- const data = readFileSync(SESSION_FILE);
30
- const key = getMachineKey();
31
- const iv = data.subarray(0, 12);
32
- const tag = data.subarray(12, 28);
33
- const encrypted = data.subarray(28);
34
- const decipher = createDecipheriv('aes-256-gcm', key, iv);
35
- decipher.setAuthTag(tag);
36
- return Buffer.concat([decipher.update(encrypted), decipher.final()]);
37
- } catch {
38
- return null;
39
- }
40
- }
41
-
42
- export function clearSession(): void {
43
- try {
44
- if (existsSync(SESSION_FILE)) {
45
- unlinkSync(SESSION_FILE);
46
- }
47
- } catch {
48
- // Ignore
49
- }
50
- }