@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,4 +0,0 @@
1
- export declare function storeMasterKey(masterKey: Buffer): void;
2
- export declare function loadMasterKey(): Buffer | null;
3
- export declare function clearSession(): void;
4
- //# sourceMappingURL=keyring.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"keyring.d.ts","sourceRoot":"","sources":["../../../src/lib/keyring.ts"],"names":[],"mappings":"AAcA,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAStD;AAED,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAc7C;AAED,wBAAgB,YAAY,IAAI,IAAI,CAQnC"}
@@ -1,49 +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
- const SESSION_DIR = join(homedir(), '.tusky');
6
- const SESSION_FILE = join(SESSION_DIR, 'session.enc');
7
- // Derive a machine-specific key from hostname + user
8
- function getMachineKey() {
9
- const machineId = `${homedir()}-tusky-session-key`;
10
- return createHash('sha256').update(machineId).digest();
11
- }
12
- export function storeMasterKey(masterKey) {
13
- mkdirSync(SESSION_DIR, { recursive: true, mode: 0o700 });
14
- const key = getMachineKey();
15
- const iv = randomBytes(12);
16
- const cipher = createCipheriv('aes-256-gcm', key, iv);
17
- const encrypted = Buffer.concat([cipher.update(masterKey), cipher.final()]);
18
- const tag = cipher.getAuthTag();
19
- const data = Buffer.concat([iv, tag, encrypted]);
20
- writeFileSync(SESSION_FILE, data, { mode: 0o600 });
21
- }
22
- export function loadMasterKey() {
23
- if (!existsSync(SESSION_FILE))
24
- return null;
25
- try {
26
- const data = readFileSync(SESSION_FILE);
27
- const key = getMachineKey();
28
- const iv = data.subarray(0, 12);
29
- const tag = data.subarray(12, 28);
30
- const encrypted = data.subarray(28);
31
- const decipher = createDecipheriv('aes-256-gcm', key, iv);
32
- decipher.setAuthTag(tag);
33
- return Buffer.concat([decipher.update(encrypted), decipher.final()]);
34
- }
35
- catch {
36
- return null;
37
- }
38
- }
39
- export function clearSession() {
40
- try {
41
- if (existsSync(SESSION_FILE)) {
42
- unlinkSync(SESSION_FILE);
43
- }
44
- }
45
- catch {
46
- // Ignore
47
- }
48
- }
49
- //# sourceMappingURL=keyring.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"keyring.js","sourceRoot":"","sources":["../../../src/lib/keyring.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACpF,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAEnF,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AAC9C,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;AAEtD,qDAAqD;AACrD,SAAS,aAAa;IACpB,MAAM,SAAS,GAAG,GAAG,OAAO,EAAE,oBAAoB,CAAC;IACnD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,MAAM,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;IAC5B,MAAM,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC;IACjD,aAAa,CAAC,YAAY,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;QACxC,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;QAC5B,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAClC,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QAC1D,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACzB,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC;QACH,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC7B,UAAU,CAAC,YAAY,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;AACH,CAAC"}
@@ -1,315 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- deriveMasterKey,
4
- computeVerifier,
5
- verifyPassphrase,
6
- generateSalt,
7
- generateMasterKey,
8
- generateRecoveryKey,
9
- wrapMasterKey,
10
- unwrapMasterKey,
11
- encryptBuffer,
12
- decryptBuffer,
13
- } from '../crypto.js';
14
-
15
- describe('deriveMasterKey', () => {
16
- it('produces deterministic 32-byte output', () => {
17
- const salt = Buffer.from('a'.repeat(32), 'hex');
18
- const key1 = deriveMasterKey('passphrase', salt);
19
- const key2 = deriveMasterKey('passphrase', salt);
20
- expect(key1).toEqual(key2);
21
- expect(key1.length).toBe(32);
22
- });
23
-
24
- it('different passphrase produces different key', () => {
25
- const salt = Buffer.from('a'.repeat(32), 'hex');
26
- const key1 = deriveMasterKey('pass1', salt);
27
- const key2 = deriveMasterKey('pass2', salt);
28
- expect(key1).not.toEqual(key2);
29
- });
30
-
31
- it('different salt produces different key', () => {
32
- const salt1 = Buffer.from('a'.repeat(32), 'hex');
33
- const salt2 = Buffer.from('b'.repeat(32), 'hex');
34
- const key1 = deriveMasterKey('pass', salt1);
35
- const key2 = deriveMasterKey('pass', salt2);
36
- expect(key1).not.toEqual(key2);
37
- });
38
- });
39
-
40
- describe('computeVerifier / verifyPassphrase', () => {
41
- it('verifier matches correct key', () => {
42
- const key = generateMasterKey();
43
- const verifier = computeVerifier(key);
44
- expect(verifyPassphrase(key, verifier)).toBe(true);
45
- });
46
-
47
- it('verifier rejects wrong key', () => {
48
- const key1 = generateMasterKey();
49
- const key2 = generateMasterKey();
50
- const verifier = computeVerifier(key1);
51
- expect(verifyPassphrase(key2, verifier)).toBe(false);
52
- });
53
-
54
- it('verifier is 32 bytes (SHA-256)', () => {
55
- const key = generateMasterKey();
56
- const verifier = computeVerifier(key);
57
- expect(verifier.length).toBe(32);
58
- });
59
- });
60
-
61
- describe('generateSalt / generateMasterKey / generateRecoveryKey', () => {
62
- it('generateSalt returns 16 bytes', () => {
63
- expect(generateSalt().length).toBe(16);
64
- });
65
-
66
- it('generateMasterKey returns 32 bytes', () => {
67
- expect(generateMasterKey().length).toBe(32);
68
- });
69
-
70
- it('generateRecoveryKey returns 32 bytes', () => {
71
- expect(generateRecoveryKey().length).toBe(32);
72
- });
73
-
74
- it('generates different values each time', () => {
75
- const a = generateSalt();
76
- const b = generateSalt();
77
- expect(a).not.toEqual(b);
78
- });
79
- });
80
-
81
- describe('wrapMasterKey / unwrapMasterKey', () => {
82
- it('round-trips successfully', () => {
83
- const masterKey = generateMasterKey();
84
- const recoveryKey = generateRecoveryKey();
85
- const wrapped = wrapMasterKey(masterKey, recoveryKey);
86
- const unwrapped = unwrapMasterKey(wrapped, recoveryKey);
87
- expect(unwrapped).toEqual(masterKey);
88
- });
89
-
90
- it('wrong recovery key throws', () => {
91
- const masterKey = generateMasterKey();
92
- const recoveryKey = generateRecoveryKey();
93
- const wrongKey = generateRecoveryKey();
94
- const wrapped = wrapMasterKey(masterKey, recoveryKey);
95
- expect(() => unwrapMasterKey(wrapped, wrongKey)).toThrow();
96
- });
97
- });
98
-
99
- describe('encryptBuffer / decryptBuffer', () => {
100
- it('round-trips successfully', () => {
101
- const masterKey = generateMasterKey();
102
- const plaintext = Buffer.from('Hello, TuskyDP!');
103
- const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(plaintext, masterKey);
104
- const decrypted = decryptBuffer(ciphertext, wrappedKey, iv, masterKey, plaintextChecksum);
105
- expect(decrypted).toEqual(plaintext);
106
- });
107
-
108
- it('wrong master key throws', () => {
109
- const masterKey = generateMasterKey();
110
- const wrongKey = generateMasterKey();
111
- const plaintext = Buffer.from('secret data');
112
- const { ciphertext, wrappedKey, iv } = encryptBuffer(plaintext, masterKey);
113
- expect(() => decryptBuffer(ciphertext, wrappedKey, iv, wrongKey)).toThrow();
114
- });
115
-
116
- it('verifies checksum on corrupted data', () => {
117
- const masterKey = generateMasterKey();
118
- const plaintext = Buffer.from('important data');
119
- const { ciphertext, wrappedKey, iv } = encryptBuffer(plaintext, masterKey);
120
- // Pass a wrong checksum
121
- expect(() => decryptBuffer(ciphertext, wrappedKey, iv, masterKey, 'wrong_checksum')).toThrow(
122
- 'Integrity check failed',
123
- );
124
- });
125
-
126
- it('handles empty buffer', () => {
127
- const masterKey = generateMasterKey();
128
- const plaintext = Buffer.alloc(0);
129
- const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(plaintext, masterKey);
130
- const decrypted = decryptBuffer(ciphertext, wrappedKey, iv, masterKey, plaintextChecksum);
131
- expect(decrypted).toEqual(plaintext);
132
- });
133
-
134
- it('handles large buffer', () => {
135
- const masterKey = generateMasterKey();
136
- const plaintext = Buffer.alloc(1024 * 1024, 0xab); // 1 MB
137
- const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(plaintext, masterKey);
138
- const decrypted = decryptBuffer(ciphertext, wrappedKey, iv, masterKey, plaintextChecksum);
139
- expect(decrypted).toEqual(plaintext);
140
- });
141
-
142
- it('ciphertext is larger than plaintext (auth tag)', () => {
143
- const masterKey = generateMasterKey();
144
- const plaintext = Buffer.from('test');
145
- const { ciphertext } = encryptBuffer(plaintext, masterKey);
146
- expect(ciphertext.length).toBeGreaterThan(plaintext.length);
147
- });
148
- });
149
-
150
- /**
151
- * Cross-platform interop test vectors.
152
- *
153
- * These tests use hardcoded inputs and expected outputs generated from Node.js crypto.
154
- * Any implementation (Rust, Python, Go, browser WebCrypto) of TuskyDP E2E encryption
155
- * MUST produce identical outputs for the same inputs.
156
- *
157
- * Algorithms:
158
- * - deriveMasterKey: PBKDF2-SHA256, 600,000 iterations, 32-byte output
159
- * - computeVerifier: HMAC-SHA256 with context string "tuskydp-key-verifier-v1"
160
- * - wrapMasterKey/unwrapMasterKey: AES-256-GCM, 12-byte IV, 16-byte auth tag
161
- * Wire format: [IV (12) | ciphertext (32) | authTag (16)] = 60 bytes
162
- * - encryptBuffer/decryptBuffer: AES-256-GCM per-file key + AES-256-GCM key wrapping
163
- * Ciphertext format: [encrypted data | authTag (16)]
164
- * WrappedKey format (base64): [wrapIV (12) | wrapped fileKey (32) | wrapAuthTag (16)]
165
- */
166
- describe('cross-platform interop test vectors', () => {
167
- // --- Test Vector Set 1 ---
168
- const TV1 = {
169
- passphrase: 'correct horse battery staple',
170
- salt: '0102030405060708090a0b0c0d0e0f10',
171
- derivedKey: '0008e69b89ffac1aa7bb1f44289ba65afaa711dd450f0aab6c322e4cd57bb216',
172
- verifier: 'b15d5b00572798fd1f13667d790823b8756ba166c61cbdf99e2bbcfefa30baa6',
173
- };
174
-
175
- // --- Test Vector Set 2 ---
176
- const TV2 = {
177
- passphrase: 'hunter2',
178
- salt: 'deadbeefcafebabe1234567890abcdef',
179
- derivedKey: '0992dbbb89cef243b310b48482d8c73ed8389ee29fb1f5a25c70552f5f01c6b8',
180
- verifier: '5387d9046eafd061d4473e2b87d885a325630b3ae4f2cb11b94d3cf3cd81b35e',
181
- };
182
-
183
- // --- Key wrapping vector ---
184
- const WRAP_VECTOR = {
185
- masterKey: '0011223344556677889900aabbccddeeff0011223344556677889900aabbccdd',
186
- recoveryKey: 'aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899',
187
- // Pre-computed wrapped output (includes IV + ciphertext + authTag)
188
- wrappedFull: 'AAECAwQFBgcICQoL5f88lTmbbm1t74wjfzEkZC6OBtte9PbLWIrK80jgH2CbiTyQ+3FYtoN2oaMj40Rg',
189
- };
190
-
191
- // --- File encryption vector ---
192
- const ENCRYPT_VECTOR = {
193
- plaintext: 'Hello, TuskyDP! This is a test vector for cross-platform interop.',
194
- plaintextChecksum: 'dd543914f5d1af720f853daad21143e7b9376ea26c7c3e208390cafc223ea1f7',
195
- masterKey: '0011223344556677889900aabbccddeeff0011223344556677889900aabbccdd',
196
- // Pre-computed ciphertext (encrypted data + authTag)
197
- ciphertext:
198
- 'B5MbJQBGzwGMbS9gj5V0xY2GoxzWGUx3ujkR2s9kRpmSIGKAt9pR8BE8VFc7X8Q0AcLukUQDy4R5MhryTj0rFWEGv4uS3WQzDizQDnYTe8uT',
199
- // Pre-computed wrapped file key (wrapIV + wrapped + wrapAuthTag)
200
- wrappedKey: 'FRQTEhEQEA8ODQwK5oDpgjZPzbX4cHXCntl3HkaI+jWLqmRv7cxPzoein4YMkzJn9PAt3Qf4RRMiP0Yl',
201
- // File IV
202
- iv: 'CgsMDQ4PEBESExQV',
203
- };
204
-
205
- describe('deriveMasterKey vectors', () => {
206
- it('vector 1: "correct horse battery staple"', () => {
207
- const salt = Buffer.from(TV1.salt, 'hex');
208
- const key = deriveMasterKey(TV1.passphrase, salt);
209
- expect(key.toString('hex')).toBe(TV1.derivedKey);
210
- });
211
-
212
- it('vector 2: "hunter2"', () => {
213
- const salt = Buffer.from(TV2.salt, 'hex');
214
- const key = deriveMasterKey(TV2.passphrase, salt);
215
- expect(key.toString('hex')).toBe(TV2.derivedKey);
216
- });
217
- });
218
-
219
- describe('computeVerifier vectors', () => {
220
- it('vector 1: verifier from derived key 1', () => {
221
- const masterKey = Buffer.from(TV1.derivedKey, 'hex');
222
- const verifier = computeVerifier(masterKey);
223
- expect(verifier.toString('hex')).toBe(TV1.verifier);
224
- });
225
-
226
- it('vector 2: verifier from derived key 2', () => {
227
- const masterKey = Buffer.from(TV2.derivedKey, 'hex');
228
- const verifier = computeVerifier(masterKey);
229
- expect(verifier.toString('hex')).toBe(TV2.verifier);
230
- });
231
-
232
- it('verifyPassphrase accepts correct verifier', () => {
233
- const masterKey = Buffer.from(TV1.derivedKey, 'hex');
234
- const verifier = Buffer.from(TV1.verifier, 'hex');
235
- expect(verifyPassphrase(masterKey, verifier)).toBe(true);
236
- });
237
-
238
- it('verifyPassphrase rejects wrong verifier', () => {
239
- const masterKey = Buffer.from(TV1.derivedKey, 'hex');
240
- const verifier = Buffer.from(TV2.verifier, 'hex');
241
- expect(verifyPassphrase(masterKey, verifier)).toBe(false);
242
- });
243
- });
244
-
245
- describe('unwrapMasterKey vector', () => {
246
- it('unwraps pre-computed wrapped key to original master key', () => {
247
- const recoveryKey = Buffer.from(WRAP_VECTOR.recoveryKey, 'hex');
248
- const wrappedData = Buffer.from(WRAP_VECTOR.wrappedFull, 'base64');
249
- const unwrapped = unwrapMasterKey(wrappedData, recoveryKey);
250
- expect(unwrapped.toString('hex')).toBe(WRAP_VECTOR.masterKey);
251
- });
252
-
253
- it('wrapped data has correct wire format length (60 bytes)', () => {
254
- const wrappedData = Buffer.from(WRAP_VECTOR.wrappedFull, 'base64');
255
- // 12 (IV) + 32 (encrypted key) + 16 (auth tag) = 60
256
- expect(wrappedData.length).toBe(60);
257
- });
258
- });
259
-
260
- describe('decryptBuffer vector', () => {
261
- it('decrypts pre-computed ciphertext to original plaintext', () => {
262
- const masterKey = Buffer.from(ENCRYPT_VECTOR.masterKey, 'hex');
263
- const ciphertext = Buffer.from(ENCRYPT_VECTOR.ciphertext, 'base64');
264
- const decrypted = decryptBuffer(
265
- ciphertext,
266
- ENCRYPT_VECTOR.wrappedKey,
267
- ENCRYPT_VECTOR.iv,
268
- masterKey,
269
- ENCRYPT_VECTOR.plaintextChecksum,
270
- );
271
- expect(decrypted.toString('utf8')).toBe(ENCRYPT_VECTOR.plaintext);
272
- });
273
-
274
- it('decrypts without checksum verification', () => {
275
- const masterKey = Buffer.from(ENCRYPT_VECTOR.masterKey, 'hex');
276
- const ciphertext = Buffer.from(ENCRYPT_VECTOR.ciphertext, 'base64');
277
- const decrypted = decryptBuffer(
278
- ciphertext,
279
- ENCRYPT_VECTOR.wrappedKey,
280
- ENCRYPT_VECTOR.iv,
281
- masterKey,
282
- );
283
- expect(decrypted.toString('utf8')).toBe(ENCRYPT_VECTOR.plaintext);
284
- });
285
-
286
- it('rejects tampered ciphertext', () => {
287
- const masterKey = Buffer.from(ENCRYPT_VECTOR.masterKey, 'hex');
288
- const ciphertext = Buffer.from(ENCRYPT_VECTOR.ciphertext, 'base64');
289
- // Flip one byte in the encrypted data
290
- ciphertext[0] ^= 0xff;
291
- expect(() =>
292
- decryptBuffer(ciphertext, ENCRYPT_VECTOR.wrappedKey, ENCRYPT_VECTOR.iv, masterKey),
293
- ).toThrow();
294
- });
295
-
296
- it('rejects tampered wrapped key', () => {
297
- const masterKey = Buffer.from(ENCRYPT_VECTOR.masterKey, 'hex');
298
- const ciphertext = Buffer.from(ENCRYPT_VECTOR.ciphertext, 'base64');
299
- // Corrupt the wrapped key
300
- const badWrappedKey = Buffer.from(ENCRYPT_VECTOR.wrappedKey, 'base64');
301
- badWrappedKey[15] ^= 0xff;
302
- expect(() =>
303
- decryptBuffer(ciphertext, badWrappedKey.toString('base64'), ENCRYPT_VECTOR.iv, masterKey),
304
- ).toThrow();
305
- });
306
-
307
- it('plaintextChecksum matches SHA-256 of plaintext', () => {
308
- const { createHash } = require('crypto');
309
- const expectedChecksum = createHash('sha256')
310
- .update(Buffer.from(ENCRYPT_VECTOR.plaintext, 'utf8'))
311
- .digest('hex');
312
- expect(expectedChecksum).toBe(ENCRYPT_VECTOR.plaintextChecksum);
313
- });
314
- });
315
- });
@@ -1,276 +0,0 @@
1
- /**
2
- * `tusky decrypt` — Decrypt a file downloaded from Walrus.
3
- *
4
- * Works in two modes:
5
- * 1. With --export <manifest.json> — reads wrappedKey/iv from the export
6
- * manifest, derives master key from passphrase + salt. Fully offline.
7
- * 2. Without --export — fetches encryption params from the Tusky API and
8
- * looks up the file metadata by ID. Requires API access.
9
- *
10
- * In both modes, the passphrase is resolved from:
11
- * --passphrase flag > TUSKYDP_PASSWORD env var > interactive prompt
12
- */
13
-
14
- import type { Command } from 'commander';
15
- import { readFileSync, writeFileSync, existsSync } from 'fs';
16
- import { resolve, basename } from 'path';
17
- import chalk from 'chalk';
18
- import inquirer from 'inquirer';
19
- import { getApiUrl, getApiKey } from '../config.js';
20
- import { createSDKClient } from '../sdk.js';
21
- import { createSpinner } from '../lib/progress.js';
22
- import { loadMasterKey } from '../lib/keyring.js';
23
- import {
24
- deriveMasterKey,
25
- verifyPassphrase,
26
- unwrapMasterKey,
27
- decryptBuffer,
28
- } from '../crypto.js';
29
- import type { ExportManifest, ExportedFile } from './export.js';
30
-
31
- // ---------------------------------------------------------------------------
32
- // Helpers
33
- // ---------------------------------------------------------------------------
34
-
35
- /**
36
- * Resolve the master key. Priority:
37
- * 1. Session keyring (~/.tusky/session.enc)
38
- * 2. Passphrase derivation (requires API or --salt/--encrypted-master-key)
39
- */
40
- async function resolveMasterKey(options: {
41
- passphrase?: string;
42
- salt?: string;
43
- verifier?: string;
44
- encryptedMasterKey?: string;
45
- }): Promise<Buffer> {
46
- // Try session keyring first
47
- const sessionKey = loadMasterKey();
48
- if (sessionKey) return sessionKey;
49
-
50
- // Need passphrase
51
- let passphrase = options.passphrase ?? process.env.TUSKYDP_PASSWORD;
52
- if (!passphrase) {
53
- const answers = await inquirer.prompt([{
54
- type: 'password',
55
- name: 'passphrase',
56
- message: 'Enter encryption passphrase:',
57
- mask: '*',
58
- }]);
59
- passphrase = answers.passphrase as string;
60
- }
61
-
62
- if (!options.salt) {
63
- throw new Error('Cannot derive master key: salt is required. Use --export with an export manifest, or ensure the Tusky API is accessible.');
64
- }
65
-
66
- const salt = Buffer.from(options.salt, 'base64');
67
- const wrappingKey = deriveMasterKey(passphrase, salt);
68
-
69
- // Verify if we have a verifier
70
- if (options.verifier) {
71
- const verifierBuf = Buffer.from(options.verifier, 'base64');
72
- if (!verifyPassphrase(wrappingKey, verifierBuf)) {
73
- throw new Error('Invalid passphrase.');
74
- }
75
- }
76
-
77
- // Unwrap master key if account has one
78
- if (options.encryptedMasterKey) {
79
- return unwrapMasterKey(Buffer.from(options.encryptedMasterKey, 'base64'), wrappingKey);
80
- }
81
-
82
- // Legacy: wrapping key IS the master key
83
- return wrappingKey;
84
- }
85
-
86
- // ---------------------------------------------------------------------------
87
- // Command registration
88
- // ---------------------------------------------------------------------------
89
-
90
- export function registerDecryptCommand(program: Command) {
91
- program
92
- .command('decrypt <encrypted-file>')
93
- .description('Decrypt a file downloaded from Walrus using your encryption passphrase')
94
- .option('-o, --output <path>', 'Output path for decrypted file')
95
- .option('--file-id <id>', 'Tusky file ID (to look up wrappedKey/iv from API)')
96
- .option('--export <path>', 'Path to tusky-export.json manifest (for offline decryption)')
97
- .option('--passphrase <passphrase>', 'Encryption passphrase (also reads TUSKYDP_PASSWORD env var)')
98
- .option('--wrapped-key <key>', 'Per-file wrapped key (base64, from export manifest)')
99
- .option('--iv <iv>', 'Per-file encryption IV (base64, from export manifest)')
100
- .option('--checksum <sha256>', 'Expected plaintext SHA-256 checksum (hex)')
101
- .action(async (encryptedFile: string, options: {
102
- output?: string;
103
- fileId?: string;
104
- export?: string;
105
- passphrase?: string;
106
- wrappedKey?: string;
107
- iv?: string;
108
- checksum?: string;
109
- }) => {
110
- const spinner = createSpinner('Preparing decryption...');
111
- spinner.start();
112
-
113
- try {
114
- // Resolve the encrypted file
115
- const inputPath = resolve(encryptedFile);
116
- if (!existsSync(inputPath)) {
117
- spinner.fail(`File not found: ${inputPath}`);
118
- return;
119
- }
120
-
121
- let wrappedKey: string;
122
- let iv: string;
123
- let checksum: string | undefined = options.checksum;
124
- let fileName: string = basename(inputPath).replace(/\.enc$/, '');
125
- let encryptionParams: { salt?: string; verifier?: string; encryptedMasterKey?: string } = {};
126
-
127
- // ── Mode 1: Export manifest ──────────────────────────────────
128
- if (options.export) {
129
- spinner.text = 'Reading export manifest...';
130
- const manifestPath = resolve(options.export);
131
- if (!existsSync(manifestPath)) {
132
- spinner.fail(`Export manifest not found: ${manifestPath}`);
133
- return;
134
- }
135
-
136
- const manifest: ExportManifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
137
- let exportedFile: ExportedFile | undefined;
138
-
139
- if (options.fileId) {
140
- exportedFile = manifest.files.find((f) => f.fileId === options.fileId);
141
- } else {
142
- // Try to match by filename
143
- const inputBase = basename(inputPath);
144
- exportedFile = manifest.files.find((f) =>
145
- f.name === inputBase || f.name === fileName,
146
- );
147
-
148
- if (!exportedFile && manifest.files.length === 1) {
149
- exportedFile = manifest.files[0];
150
- }
151
- }
152
-
153
- if (!exportedFile) {
154
- spinner.fail(
155
- options.fileId
156
- ? `File ID ${options.fileId} not found in export manifest.`
157
- : `Could not match "${basename(inputPath)}" to a file in the export manifest. Use --file-id to specify.`,
158
- );
159
- return;
160
- }
161
-
162
- if (!exportedFile.encrypted) {
163
- spinner.fail(`File "${exportedFile.name}" is not encrypted (public vault). No decryption needed.`);
164
- return;
165
- }
166
-
167
- if (!exportedFile.wrappedKey || !exportedFile.encryptionIv) {
168
- spinner.fail(`File "${exportedFile.name}" is missing encryption metadata in the export.`);
169
- return;
170
- }
171
-
172
- wrappedKey = exportedFile.wrappedKey;
173
- iv = exportedFile.encryptionIv;
174
- checksum = checksum ?? exportedFile.plaintextChecksumSha256 ?? undefined;
175
- fileName = exportedFile.name;
176
-
177
- // Read account-level encryption params from manifest
178
- if (manifest.encryption) {
179
- encryptionParams = {
180
- salt: manifest.encryption.salt ?? undefined,
181
- verifier: manifest.encryption.verifier ?? undefined,
182
- encryptedMasterKey: manifest.encryption.encryptedMasterKey ?? undefined,
183
- };
184
- }
185
-
186
- // ── Mode 2: Direct flags ─────────────────────────────────────
187
- } else if (options.wrappedKey && options.iv) {
188
- wrappedKey = options.wrappedKey;
189
- iv = options.iv;
190
-
191
- // ── Mode 3: Fetch from API ───────────────────────────────────
192
- } else if (options.fileId) {
193
- spinner.text = 'Fetching file metadata from API...';
194
- const apiUrl = getApiUrl(program.opts().apiUrl);
195
- const apiKey = getApiKey(program.opts().apiKey);
196
- const sdk = createSDKClient(apiUrl, apiKey);
197
-
198
- const file = await sdk.files.get(options.fileId);
199
- if (!file.encrypted) {
200
- spinner.fail('This file is not encrypted. No decryption needed.');
201
- return;
202
- }
203
- if (!file.wrappedKey || !file.encryptionIv) {
204
- spinner.fail('File is missing encryption metadata.');
205
- return;
206
- }
207
-
208
- wrappedKey = file.wrappedKey;
209
- iv = file.encryptionIv;
210
- checksum = checksum ?? file.plaintextChecksumSha256 ?? undefined;
211
- fileName = file.name;
212
-
213
- // Also fetch encryption params for master key derivation
214
- const params = await sdk.account.getEncryptionParams();
215
- encryptionParams = {
216
- salt: params.salt ?? undefined,
217
- verifier: params.verifier ?? undefined,
218
- encryptedMasterKey: params.encryptedMasterKey ?? undefined,
219
- };
220
-
221
- } else {
222
- spinner.fail(
223
- 'Need encryption metadata. Use one of:\n' +
224
- ' --export <manifest.json> (offline, from tusky export)\n' +
225
- ' --file-id <id> (online, fetches from API)\n' +
226
- ' --wrapped-key <key> --iv <iv> (manual, base64 values)',
227
- );
228
- return;
229
- }
230
-
231
- // If we don't have encryption params yet, try API
232
- if (!encryptionParams.salt) {
233
- try {
234
- const apiUrl = getApiUrl(program.opts().apiUrl);
235
- const apiKey = getApiKey(program.opts().apiKey);
236
- const sdk = createSDKClient(apiUrl, apiKey);
237
- const params = await sdk.account.getEncryptionParams();
238
- encryptionParams = {
239
- salt: params.salt ?? undefined,
240
- verifier: params.verifier ?? undefined,
241
- encryptedMasterKey: params.encryptedMasterKey ?? undefined,
242
- };
243
- } catch {
244
- // API unavailable — that's ok if we have a session key
245
- }
246
- }
247
-
248
- // Resolve master key
249
- spinner.text = 'Deriving encryption key...';
250
- const masterKey = await resolveMasterKey({
251
- passphrase: options.passphrase,
252
- ...encryptionParams,
253
- });
254
-
255
- // Read and decrypt
256
- spinner.text = 'Decrypting...';
257
- const ciphertext = readFileSync(inputPath);
258
- const plaintext = decryptBuffer(ciphertext, wrappedKey, iv, masterKey, checksum);
259
-
260
- // Write output
261
- const outputPath = options.output ? resolve(options.output) : resolve(fileName);
262
- writeFileSync(outputPath, plaintext);
263
-
264
- spinner.succeed(`Decrypted -> ${outputPath} (${plaintext.length} bytes)`);
265
-
266
- if (checksum) {
267
- console.log(chalk.dim(' Integrity check passed (SHA-256)'));
268
- }
269
- } catch (err: any) {
270
- spinner.fail(`Decryption failed: ${err.message}`);
271
- if (err.message.includes('Unsupported state') || err.message.includes('auth tag')) {
272
- console.log(chalk.dim(' This usually means the passphrase is incorrect or the file is corrupted.'));
273
- }
274
- }
275
- });
276
- }