passwd-sso-cli 0.4.67 → 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.
@@ -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
- function escapeCSV(value) {
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 escapeCSV(val.secret ?? "");
79
+ return escapeCsvCompat(val.secret ?? "");
85
80
  }
86
- return escapeCSV(String(val ?? ""));
81
+ return escapeCsvCompat(String(val ?? ""));
87
82
  }).join(","));
88
83
  }
89
84
  const csvOut = csvRows.join("\n");
@@ -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 deriveAuthKey(secretKey: Uint8Array): Promise<CryptoKey>;
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(authKey: CryptoKey): Promise<string>;
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>;
@@ -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
- export async function deriveAuthKey(secretKey) {
69
- const hkdfKey = await crypto.subtle.importKey("raw", toArrayBuffer(secretKey), "HKDF", false, ["deriveKey"]);
70
- return crypto.subtle.deriveKey({
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, { name: "HMAC", hash: "SHA-256", length: AES_KEY_LENGTH }, true, ["sign"]);
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(authKey) {
91
- const rawKey = await crypto.subtle.exportKey("raw", authKey);
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passwd-sso-cli",
3
- "version": "0.4.67",
3
+ "version": "0.4.68",
4
4
  "description": "CLI for passwd-sso password manager",
5
5
  "type": "module",
6
6
  "license": "MIT",