passwd-sso-cli 0.4.66 → 0.4.68
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/dist/commands/agent-decrypt.js +13 -2
- package/dist/commands/agent.js +3 -1
- package/dist/commands/export.js +3 -8
- package/dist/commands/unlock.js +3 -1
- package/dist/index.js +8 -1
- package/dist/lib/api-client.d.ts +1 -0
- package/dist/lib/api-client.js +6 -3
- package/dist/lib/crypto.d.ts +2 -2
- package/dist/lib/crypto.js +9 -7
- package/dist/lib/csv-escape.d.ts +13 -0
- package/dist/lib/csv-escape.js +41 -0
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import { mkdirSync, lstatSync, chmodSync, unlinkSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { z } from "zod";
|
|
13
|
-
import { apiRequest, startBackgroundRefresh } from "../lib/api-client.js";
|
|
13
|
+
import { apiRequest, assertLoggedIn, startBackgroundRefresh } from "../lib/api-client.js";
|
|
14
14
|
import { decryptData, hexEncode } from "../lib/crypto.js";
|
|
15
15
|
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
16
16
|
import { getEncryptionKey, getUserId, getSecretKeyBytes, setEncryptionKey } from "../lib/vault-state.js";
|
|
@@ -153,7 +153,16 @@ export function handleConnection(socket) {
|
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
155
|
else {
|
|
156
|
-
|
|
156
|
+
try {
|
|
157
|
+
response = await handleDecryptRequest(parsed.data);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// Request-layer failures (server down, revoked login) — not parse errors
|
|
161
|
+
response = {
|
|
162
|
+
ok: false,
|
|
163
|
+
error: `Request failed: ${err instanceof Error ? err.message : "unknown error"}`,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
157
166
|
}
|
|
158
167
|
}
|
|
159
168
|
catch (err) {
|
|
@@ -187,6 +196,8 @@ export async function decryptAgentCommand(opts) {
|
|
|
187
196
|
"Run in an interactive terminal.\n");
|
|
188
197
|
process.exit(1);
|
|
189
198
|
}
|
|
199
|
+
// Fail fast before prompting — the passphrase is useless without a login
|
|
200
|
+
assertLoggedIn();
|
|
190
201
|
// In --eval mode, stdout is captured by the shell — write prompt to stderr
|
|
191
202
|
const passphrase = await readPassphrase("Master passphrase: ", { useStderr: opts.eval });
|
|
192
203
|
if (!passphrase) {
|
package/dist/commands/agent.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* substitution returns instead of blocking.
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from "node:child_process";
|
|
15
|
-
import { apiRequest } from "../lib/api-client.js";
|
|
15
|
+
import { apiRequest, assertLoggedIn } from "../lib/api-client.js";
|
|
16
16
|
import { decryptData, hexEncode } from "../lib/crypto.js";
|
|
17
17
|
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
18
18
|
import { getEncryptionKey, getUserId, getSecretKeyBytes, setEncryptionKey, isUnlocked, } from "../lib/vault-state.js";
|
|
@@ -100,6 +100,8 @@ export async function agentCommand(opts) {
|
|
|
100
100
|
// Unlock the vault in this (parent / foreground) process.
|
|
101
101
|
if (!(await autoUnlockIfNeeded())) {
|
|
102
102
|
if (opts.eval && process.stdin.isTTY) {
|
|
103
|
+
// Fail fast before prompting — the passphrase is useless without a login
|
|
104
|
+
assertLoggedIn();
|
|
103
105
|
// Prompt on stderr so stdout stays clean for `eval $(...)` capture.
|
|
104
106
|
const passphrase = await readPassphrase("Master passphrase: ", { useStderr: true });
|
|
105
107
|
if (!passphrase || !(await unlockWithPassphrase(passphrase))) {
|
package/dist/commands/export.js
CHANGED
|
@@ -10,12 +10,7 @@ import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
|
10
10
|
import { writeSecretFile } from "../lib/secure-file.js";
|
|
11
11
|
import * as output from "../lib/output.js";
|
|
12
12
|
import { CLI_API_PATH } from "../lib/api-paths.js";
|
|
13
|
-
|
|
14
|
-
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
15
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
16
|
-
}
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
13
|
+
import { escapeCsvCompat } from "../lib/csv-escape.js";
|
|
19
14
|
export async function exportCommand(options) {
|
|
20
15
|
const key = getEncryptionKey();
|
|
21
16
|
if (!key) {
|
|
@@ -81,9 +76,9 @@ export async function exportCommand(options) {
|
|
|
81
76
|
csvRows.push(headers.map((h) => {
|
|
82
77
|
const val = entry[h];
|
|
83
78
|
if (h === "totp" && val && typeof val === "object") {
|
|
84
|
-
return
|
|
79
|
+
return escapeCsvCompat(val.secret ?? "");
|
|
85
80
|
}
|
|
86
|
-
return
|
|
81
|
+
return escapeCsvCompat(String(val ?? ""));
|
|
87
82
|
}).join(","));
|
|
88
83
|
}
|
|
89
84
|
const csvOut = csvRows.join("\n");
|
package/dist/commands/unlock.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Derives the encryption key and stores it in process memory.
|
|
5
5
|
*/
|
|
6
|
-
import { apiRequest } from "../lib/api-client.js";
|
|
6
|
+
import { apiRequest, assertLoggedIn } from "../lib/api-client.js";
|
|
7
7
|
import { hexDecode, deriveWrappingKey, unwrapSecretKey, deriveEncryptionKey, verifyKey, } from "../lib/crypto.js";
|
|
8
8
|
import { setEncryptionKey, setSecretKeyBytes, isUnlocked } from "../lib/vault-state.js";
|
|
9
9
|
import * as output from "../lib/output.js";
|
|
@@ -100,6 +100,8 @@ export async function unlockCommand() {
|
|
|
100
100
|
output.info("Vault is already unlocked.");
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
+
// Fail fast before prompting — the passphrase is useless without a login
|
|
104
|
+
assertLoggedIn();
|
|
103
105
|
const passphrase = await readPassphrase("Master passphrase: ");
|
|
104
106
|
if (!passphrase) {
|
|
105
107
|
output.error("Passphrase is required.");
|
package/dist/index.js
CHANGED
|
@@ -175,7 +175,14 @@ program
|
|
|
175
175
|
process.exit(1);
|
|
176
176
|
}
|
|
177
177
|
});
|
|
178
|
-
|
|
178
|
+
try {
|
|
179
|
+
await program.parseAsync();
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
// Render action-handler errors as a clean one-liner — never a stack trace
|
|
183
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
179
186
|
// ─── Interactive REPL Mode ──────────────────────────────────────
|
|
180
187
|
async function interactiveMode() {
|
|
181
188
|
output.info("Vault unlocked. Type a command or `help` for options. `lock` to exit.");
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare function setInsecure(enabled: boolean): void;
|
|
|
8
8
|
export declare function getToken(): string | null;
|
|
9
9
|
export declare function setTokenCache(token: string, expiresAt?: string, refreshToken?: string, clientId?: string): void;
|
|
10
10
|
export declare function clearTokenCache(): void;
|
|
11
|
+
export declare function assertLoggedIn(): void;
|
|
11
12
|
export interface ApiResponse<T = unknown> {
|
|
12
13
|
ok: boolean;
|
|
13
14
|
status: number;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -47,6 +47,11 @@ export function clearTokenCache() {
|
|
|
47
47
|
cachedRefreshToken = null;
|
|
48
48
|
cachedClientId = null;
|
|
49
49
|
}
|
|
50
|
+
export function assertLoggedIn() {
|
|
51
|
+
if (!getToken()) {
|
|
52
|
+
throw new Error("Not logged in. Run `passwd-sso login` first.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
50
55
|
function getBaseUrl() {
|
|
51
56
|
const config = loadConfig();
|
|
52
57
|
if (!config.serverUrl) {
|
|
@@ -83,10 +88,8 @@ async function refreshToken() {
|
|
|
83
88
|
}
|
|
84
89
|
}
|
|
85
90
|
export async function apiRequest(path, options = {}) {
|
|
91
|
+
assertLoggedIn();
|
|
86
92
|
let token = getToken();
|
|
87
|
-
if (!token) {
|
|
88
|
-
throw new Error("Not logged in. Run `passwd-sso login` first.");
|
|
89
|
-
}
|
|
90
93
|
if (isTokenExpiringSoon()) {
|
|
91
94
|
const refreshed = await refreshToken();
|
|
92
95
|
if (refreshed) {
|
package/dist/lib/crypto.d.ts
CHANGED
|
@@ -13,9 +13,9 @@ export declare function hexEncode(buf: ArrayBuffer | Uint8Array): string;
|
|
|
13
13
|
export declare function hexDecode(hex: string): Uint8Array;
|
|
14
14
|
export declare function deriveWrappingKey(passphrase: string, accountSalt: Uint8Array): Promise<CryptoKey>;
|
|
15
15
|
export declare function deriveEncryptionKey(secretKey: Uint8Array): Promise<CryptoKey>;
|
|
16
|
-
export declare function
|
|
16
|
+
export declare function deriveAuthKeyBytes(secretKey: Uint8Array): Promise<Uint8Array>;
|
|
17
17
|
export declare function unwrapSecretKey(encrypted: EncryptedData, wrappingKey: CryptoKey): Promise<Uint8Array>;
|
|
18
|
-
export declare function computeAuthHash(
|
|
18
|
+
export declare function computeAuthHash(authKeyBytes: Uint8Array): Promise<string>;
|
|
19
19
|
export declare function verifyKey(encryptionKey: CryptoKey, artifact: EncryptedData): Promise<boolean>;
|
|
20
20
|
export declare function encryptData(plaintext: string, key: CryptoKey, aad?: Uint8Array): Promise<EncryptedData>;
|
|
21
21
|
export declare function decryptData(encrypted: EncryptedData, key: CryptoKey, aad?: Uint8Array): Promise<string>;
|
package/dist/lib/crypto.js
CHANGED
|
@@ -65,15 +65,18 @@ export async function deriveEncryptionKey(secretKey) {
|
|
|
65
65
|
info: textEncode(HKDF_ENC_INFO),
|
|
66
66
|
}, hkdfKey, { name: "AES-GCM", length: AES_KEY_LENGTH }, false, ["encrypt", "decrypt"]);
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// Uses deriveBits (not deriveKey) so no extractable CryptoKey is ever created.
|
|
69
|
+
// Output bytes are identical to the previous exportKey("raw", <HMAC key>) form.
|
|
70
|
+
export async function deriveAuthKeyBytes(secretKey) {
|
|
71
|
+
const hkdfKey = await crypto.subtle.importKey("raw", toArrayBuffer(secretKey), "HKDF", false, ["deriveBits"]);
|
|
72
|
+
const bits = await crypto.subtle.deriveBits({
|
|
71
73
|
name: "HKDF",
|
|
72
74
|
hash: "SHA-256",
|
|
73
75
|
// See deriveEncryptionKey for zero-salt risk acceptance rationale
|
|
74
76
|
salt: new ArrayBuffer(32),
|
|
75
77
|
info: textEncode(HKDF_AUTH_INFO),
|
|
76
|
-
}, hkdfKey,
|
|
78
|
+
}, hkdfKey, AES_KEY_LENGTH);
|
|
79
|
+
return new Uint8Array(bits);
|
|
77
80
|
}
|
|
78
81
|
// ─── Secret Key Management ─────────────────────────────────────
|
|
79
82
|
export async function unwrapSecretKey(encrypted, wrappingKey) {
|
|
@@ -87,9 +90,8 @@ export async function unwrapSecretKey(encrypted, wrappingKey) {
|
|
|
87
90
|
return new Uint8Array(decrypted);
|
|
88
91
|
}
|
|
89
92
|
// ─── Auth Hash ────────────────────────────────────────────────
|
|
90
|
-
export async function computeAuthHash(
|
|
91
|
-
const
|
|
92
|
-
const hash = await crypto.subtle.digest("SHA-256", rawKey);
|
|
93
|
+
export async function computeAuthHash(authKeyBytes) {
|
|
94
|
+
const hash = await crypto.subtle.digest("SHA-256", toArrayBuffer(authKeyBytes));
|
|
93
95
|
return hexEncode(hash);
|
|
94
96
|
}
|
|
95
97
|
// ─── Verification ──────────────────────────────────────────────
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Characters that make a spreadsheet interpret a cell as a formula. */
|
|
2
|
+
export declare const CSV_FORMULA_TRIGGER_RE: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* Escapes a value for a compatibility CSV format that only quote-wraps cells
|
|
5
|
+
* containing a delimiter/quote/newline (Bitwarden-compatible layout), while
|
|
6
|
+
* still neutralizing formula-injection.
|
|
7
|
+
*
|
|
8
|
+
* RS6 ordering: the `"` → `""` doubling is applied before the formula-prefix
|
|
9
|
+
* decision, and the prefix is added inside the quotes — a formula-triggering
|
|
10
|
+
* cell is always quote-wrapped so the leading `'` cannot be stripped by a bare
|
|
11
|
+
* unquoted value.
|
|
12
|
+
*/
|
|
13
|
+
export declare function escapeCsvCompat(value: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Shared CSV formula-injection (CSV injection) neutralization for the CLI.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors src/lib/format/csv-escape.ts byte-for-byte behavior. The CLI is a
|
|
4
|
+
// separate ESM package and cannot import the app module, so the trigger set
|
|
5
|
+
// and escape logic are duplicated here; the parity test in
|
|
6
|
+
// cli/src/__tests__/unit/csv-escape.test.ts pins them to the same cases as the
|
|
7
|
+
// app twin so the two implementations cannot drift silently.
|
|
8
|
+
//
|
|
9
|
+
// A cell whose value begins with one of `= + - @ \t \r` is interpreted as a
|
|
10
|
+
// formula by Excel / Google Sheets / LibreOffice when the exported file is
|
|
11
|
+
// opened, enabling data exfiltration (HYPERLINK / WEBSERVICE) or command
|
|
12
|
+
// execution (DDE). Prefixing such a cell with a single quote makes the
|
|
13
|
+
// spreadsheet treat it as literal text.
|
|
14
|
+
//
|
|
15
|
+
// Leading whitespace (spaces, tabs, newlines) before the trigger char is also
|
|
16
|
+
// caught (`\s*` prefix) — spreadsheets still evaluate ` =1+1` as a formula,
|
|
17
|
+
// so a value with leading whitespace and a trigger char needs the same guard.
|
|
18
|
+
/** Characters that make a spreadsheet interpret a cell as a formula. */
|
|
19
|
+
export const CSV_FORMULA_TRIGGER_RE = /^\s*[=+\-@\t\r]/;
|
|
20
|
+
/**
|
|
21
|
+
* Escapes a value for a compatibility CSV format that only quote-wraps cells
|
|
22
|
+
* containing a delimiter/quote/newline (Bitwarden-compatible layout), while
|
|
23
|
+
* still neutralizing formula-injection.
|
|
24
|
+
*
|
|
25
|
+
* RS6 ordering: the `"` → `""` doubling is applied before the formula-prefix
|
|
26
|
+
* decision, and the prefix is added inside the quotes — a formula-triggering
|
|
27
|
+
* cell is always quote-wrapped so the leading `'` cannot be stripped by a bare
|
|
28
|
+
* unquoted value.
|
|
29
|
+
*/
|
|
30
|
+
export function escapeCsvCompat(value) {
|
|
31
|
+
const needsQuote = value.includes(",") ||
|
|
32
|
+
value.includes('"') ||
|
|
33
|
+
value.includes("\n") ||
|
|
34
|
+
CSV_FORMULA_TRIGGER_RE.test(value);
|
|
35
|
+
if (!needsQuote)
|
|
36
|
+
return value;
|
|
37
|
+
const escaped = value.replace(/"/g, '""');
|
|
38
|
+
const prefixed = CSV_FORMULA_TRIGGER_RE.test(escaped) ? `'${escaped}` : escaped;
|
|
39
|
+
return `"${prefixed}"`;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=csv-escape.js.map
|