passwd-sso-cli 0.4.49 → 0.4.55
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 +10 -2
- package/dist/commands/agent.js +2 -2
- package/dist/commands/env.js +2 -2
- package/dist/commands/export.js +5 -6
- package/dist/commands/get.js +2 -2
- package/dist/commands/list.js +2 -2
- package/dist/commands/run.js +2 -2
- package/dist/commands/totp.js +2 -2
- package/dist/index.js +0 -0
- package/dist/lib/config.js +12 -15
- package/dist/lib/crypto-aad.d.ts +6 -1
- package/dist/lib/crypto-aad.js +8 -2
- package/dist/lib/secure-file.d.ts +16 -0
- package/dist/lib/secure-file.js +41 -0
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { apiRequest, startBackgroundRefresh } from "../lib/api-client.js";
|
|
14
14
|
import { decryptData, hexEncode } from "../lib/crypto.js";
|
|
15
|
-
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
15
|
+
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
16
16
|
import { getEncryptionKey, getUserId, getSecretKeyBytes, setEncryptionKey } from "../lib/vault-state.js";
|
|
17
17
|
import { readPassphrase, unlockWithPassphrase } from "./unlock.js";
|
|
18
18
|
import * as output from "../lib/output.js";
|
|
@@ -50,6 +50,14 @@ function prepareSocket(socketPath) {
|
|
|
50
50
|
process.stderr.write(`Error: Socket directory ${dir} is owned by uid ${dirStat.uid}, expected ${uid}\n`);
|
|
51
51
|
process.exit(1);
|
|
52
52
|
}
|
|
53
|
+
// mkdirSync's mode applies only to a freshly-created dir; a pre-existing dir
|
|
54
|
+
// (e.g. a custom $XDG_RUNTIME_DIR) may be group/other-accessible. Re-stat the
|
|
55
|
+
// mode and reject anything other than 0700 (matches ssh-agent-socket.ts).
|
|
56
|
+
const dirMode = dirStat.mode & 0o7777;
|
|
57
|
+
if (dirMode !== 0o700) {
|
|
58
|
+
process.stderr.write(`Error: Socket directory ${dir} has mode ${dirMode.toString(8)}, expected 700\n`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
53
61
|
// Remove stale socket if present, verify ownership first
|
|
54
62
|
try {
|
|
55
63
|
const sockStat = lstatSync(socketPath);
|
|
@@ -89,7 +97,7 @@ async function handleDecryptRequest(req) {
|
|
|
89
97
|
const userId = getUserId();
|
|
90
98
|
try {
|
|
91
99
|
const aad = entry.aadVersion >= 1 && userId
|
|
92
|
-
? buildPersonalEntryAAD(userId, entry.id)
|
|
100
|
+
? buildPersonalEntryAAD(userId, entry.id, VAULT_TYPE.BLOB)
|
|
93
101
|
: undefined;
|
|
94
102
|
const plaintext = await decryptData(entry.encryptedBlob, encryptionKey, aad);
|
|
95
103
|
const blob = JSON.parse(plaintext);
|
package/dist/commands/agent.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { apiRequest } from "../lib/api-client.js";
|
|
10
10
|
import { decryptData } from "../lib/crypto.js";
|
|
11
|
-
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
11
|
+
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
12
12
|
import { getEncryptionKey, getUserId, isUnlocked } from "../lib/vault-state.js";
|
|
13
13
|
import { autoUnlockIfNeeded } from "./unlock.js";
|
|
14
14
|
import { loadKey, clearKeys } from "../lib/ssh-key-agent.js";
|
|
@@ -60,7 +60,7 @@ export async function agentCommand(opts) {
|
|
|
60
60
|
for (const entry of entries) {
|
|
61
61
|
try {
|
|
62
62
|
const aad = entry.aadVersion >= 1 && userId
|
|
63
|
-
? buildPersonalEntryAAD(userId, entry.id)
|
|
63
|
+
? buildPersonalEntryAAD(userId, entry.id, VAULT_TYPE.BLOB)
|
|
64
64
|
: undefined;
|
|
65
65
|
const plaintext = await decryptData(entry.encryptedBlob, encryptionKey, aad);
|
|
66
66
|
const blob = JSON.parse(plaintext);
|
package/dist/commands/env.js
CHANGED
|
@@ -12,7 +12,7 @@ import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
|
|
|
12
12
|
import { autoUnlockIfNeeded } from "./unlock.js";
|
|
13
13
|
import { getToken } from "../lib/api-client.js";
|
|
14
14
|
import { decryptData } from "../lib/crypto.js";
|
|
15
|
-
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
15
|
+
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
16
16
|
import { BLOCKED_KEYS } from "../lib/blocked-keys.js";
|
|
17
17
|
import * as output from "../lib/output.js";
|
|
18
18
|
export async function envCommand(opts) {
|
|
@@ -72,7 +72,7 @@ export async function envCommand(opts) {
|
|
|
72
72
|
const data = (await res.json());
|
|
73
73
|
let additionalData;
|
|
74
74
|
if (data.aadVersion && data.aadVersion >= 1 && userId) {
|
|
75
|
-
additionalData = buildPersonalEntryAAD(userId, data.id);
|
|
75
|
+
additionalData = buildPersonalEntryAAD(userId, data.id, VAULT_TYPE.BLOB);
|
|
76
76
|
}
|
|
77
77
|
const decrypted = await decryptData(data.encryptedBlob, encryptionKey, additionalData);
|
|
78
78
|
const blob = JSON.parse(decrypted);
|
package/dist/commands/export.js
CHANGED
|
@@ -6,7 +6,8 @@ import { existsSync, lstatSync } from "node:fs";
|
|
|
6
6
|
import { apiRequest } from "../lib/api-client.js";
|
|
7
7
|
import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
|
|
8
8
|
import { decryptData } from "../lib/crypto.js";
|
|
9
|
-
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
9
|
+
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
10
|
+
import { writeSecretFile } from "../lib/secure-file.js";
|
|
10
11
|
import * as output from "../lib/output.js";
|
|
11
12
|
function escapeCSV(value) {
|
|
12
13
|
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
@@ -50,7 +51,7 @@ export async function exportCommand(options) {
|
|
|
50
51
|
for (const entry of entries) {
|
|
51
52
|
try {
|
|
52
53
|
const aad = entry.aadVersion >= 1 && userId
|
|
53
|
-
? buildPersonalEntryAAD(userId, entry.id)
|
|
54
|
+
? buildPersonalEntryAAD(userId, entry.id, VAULT_TYPE.BLOB)
|
|
54
55
|
: undefined;
|
|
55
56
|
const plaintext = await decryptData(entry.encryptedBlob, key, aad);
|
|
56
57
|
decrypted.push(JSON.parse(plaintext));
|
|
@@ -65,8 +66,7 @@ export async function exportCommand(options) {
|
|
|
65
66
|
if (format === "json") {
|
|
66
67
|
const out = JSON.stringify(decrypted, null, 2);
|
|
67
68
|
if (outputPath) {
|
|
68
|
-
|
|
69
|
-
writeFileSync(outputPath, out, { encoding: "utf-8", mode: 0o600 });
|
|
69
|
+
writeSecretFile(outputPath, out);
|
|
70
70
|
output.success(`Exported ${decrypted.length} entries to ${outputPath}`);
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
@@ -87,8 +87,7 @@ export async function exportCommand(options) {
|
|
|
87
87
|
}
|
|
88
88
|
const csvOut = csvRows.join("\n");
|
|
89
89
|
if (outputPath) {
|
|
90
|
-
|
|
91
|
-
writeFileSync(outputPath, csvOut, { encoding: "utf-8", mode: 0o600 });
|
|
90
|
+
writeSecretFile(outputPath, csvOut);
|
|
92
91
|
output.success(`Exported ${decrypted.length} entries to ${outputPath}`);
|
|
93
92
|
}
|
|
94
93
|
else {
|
package/dist/commands/get.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { apiRequest } from "../lib/api-client.js";
|
|
5
5
|
import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
|
|
6
6
|
import { decryptData } from "../lib/crypto.js";
|
|
7
|
-
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
7
|
+
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
8
8
|
import { copyToClipboard } from "../lib/clipboard.js";
|
|
9
9
|
import * as output from "../lib/output.js";
|
|
10
10
|
export async function getCommand(id, options) {
|
|
@@ -22,7 +22,7 @@ export async function getCommand(id, options) {
|
|
|
22
22
|
const entry = res.data;
|
|
23
23
|
try {
|
|
24
24
|
const aad = entry.aadVersion >= 1 && userId
|
|
25
|
-
? buildPersonalEntryAAD(userId, entry.id)
|
|
25
|
+
? buildPersonalEntryAAD(userId, entry.id, VAULT_TYPE.BLOB)
|
|
26
26
|
: undefined;
|
|
27
27
|
const plaintext = await decryptData(entry.encryptedBlob, key, aad);
|
|
28
28
|
const blob = JSON.parse(plaintext);
|
package/dist/commands/list.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { apiRequest } from "../lib/api-client.js";
|
|
5
5
|
import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
|
|
6
6
|
import { decryptData } from "../lib/crypto.js";
|
|
7
|
-
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
7
|
+
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
8
8
|
import * as output from "../lib/output.js";
|
|
9
9
|
export async function listCommand(options) {
|
|
10
10
|
const key = getEncryptionKey();
|
|
@@ -27,7 +27,7 @@ export async function listCommand(options) {
|
|
|
27
27
|
for (const entry of entries) {
|
|
28
28
|
try {
|
|
29
29
|
const aad = entry.aadVersion >= 1 && userId
|
|
30
|
-
? buildPersonalEntryAAD(userId, entry.id)
|
|
30
|
+
? buildPersonalEntryAAD(userId, entry.id, VAULT_TYPE.OVERVIEW)
|
|
31
31
|
: undefined;
|
|
32
32
|
const plaintext = await decryptData(entry.encryptedOverview, key, aad);
|
|
33
33
|
const overview = JSON.parse(plaintext);
|
package/dist/commands/run.js
CHANGED
|
@@ -10,7 +10,7 @@ import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
|
|
|
10
10
|
import { autoUnlockIfNeeded } from "./unlock.js";
|
|
11
11
|
import { getToken } from "../lib/api-client.js";
|
|
12
12
|
import { decryptData } from "../lib/crypto.js";
|
|
13
|
-
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
13
|
+
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
14
14
|
import * as output from "../lib/output.js";
|
|
15
15
|
import { BLOCKED_KEYS } from "../lib/blocked-keys.js";
|
|
16
16
|
export async function runCommand(opts) {
|
|
@@ -74,7 +74,7 @@ export async function runCommand(opts) {
|
|
|
74
74
|
const data = (await res.json());
|
|
75
75
|
let additionalData;
|
|
76
76
|
if (data.aadVersion && data.aadVersion >= 1 && userId) {
|
|
77
|
-
additionalData = buildPersonalEntryAAD(userId, data.id);
|
|
77
|
+
additionalData = buildPersonalEntryAAD(userId, data.id, VAULT_TYPE.BLOB);
|
|
78
78
|
}
|
|
79
79
|
const decrypted = await decryptData(data.encryptedBlob, encryptionKey, additionalData);
|
|
80
80
|
const blob = JSON.parse(decrypted);
|
package/dist/commands/totp.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { apiRequest } from "../lib/api-client.js";
|
|
5
5
|
import { getEncryptionKey, getUserId } from "../lib/vault-state.js";
|
|
6
6
|
import { decryptData } from "../lib/crypto.js";
|
|
7
|
-
import { buildPersonalEntryAAD } from "../lib/crypto-aad.js";
|
|
7
|
+
import { buildPersonalEntryAAD, VAULT_TYPE } from "../lib/crypto-aad.js";
|
|
8
8
|
import { generateTOTPCode } from "../lib/totp.js";
|
|
9
9
|
import { copyToClipboard } from "../lib/clipboard.js";
|
|
10
10
|
import * as output from "../lib/output.js";
|
|
@@ -22,7 +22,7 @@ export async function totpCommand(id, options) {
|
|
|
22
22
|
}
|
|
23
23
|
try {
|
|
24
24
|
const aad = res.data.aadVersion >= 1 && userId
|
|
25
|
-
? buildPersonalEntryAAD(userId, res.data.id)
|
|
25
|
+
? buildPersonalEntryAAD(userId, res.data.id, VAULT_TYPE.BLOB)
|
|
26
26
|
: undefined;
|
|
27
27
|
const plaintext = await decryptData(res.data.encryptedBlob, key, aad);
|
|
28
28
|
const blob = JSON.parse(plaintext);
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/lib/config.js
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Legacy ~/.passwd-sso/ is auto-migrated on first access.
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, lstatSync,
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, lstatSync, unlinkSync, } from "node:fs";
|
|
10
10
|
import { getConfigDir, getDataDir, getConfigFilePath, getCredentialsFilePath, } from "./paths.js";
|
|
11
|
+
import { writeSecretFile, readSecretFile } from "./secure-file.js";
|
|
11
12
|
import { migrateIfNeeded } from "./migrate.js";
|
|
12
13
|
const DEFAULT_CONFIG = {
|
|
13
14
|
serverUrl: "",
|
|
@@ -49,21 +50,10 @@ export function saveConfig(config) {
|
|
|
49
50
|
export function saveCredentials(creds) {
|
|
50
51
|
ensureDataDir();
|
|
51
52
|
const dataDir = getDataDir();
|
|
52
|
-
|
|
53
|
-
if (stat.isSymbolicLink()) {
|
|
53
|
+
if (lstatSync(dataDir).isSymbolicLink()) {
|
|
54
54
|
throw new Error("Data directory is a symlink — refusing to write credentials.");
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
const fd = openSync(credPath, fsConstants.O_WRONLY |
|
|
58
|
-
fsConstants.O_CREAT |
|
|
59
|
-
fsConstants.O_TRUNC |
|
|
60
|
-
(fsConstants.O_NOFOLLOW ?? 0), 0o600);
|
|
61
|
-
try {
|
|
62
|
-
writeSync(fd, JSON.stringify(creds)); // codeql[js/network-data-written-to-file] OAuth tokens are intentionally persisted for session continuity
|
|
63
|
-
}
|
|
64
|
-
finally {
|
|
65
|
-
closeSync(fd);
|
|
66
|
-
}
|
|
56
|
+
writeSecretFile(getCredentialsFilePath(), JSON.stringify(creds));
|
|
67
57
|
}
|
|
68
58
|
/**
|
|
69
59
|
* Load stored credentials. Returns null if the file does not exist,
|
|
@@ -73,7 +63,14 @@ export function saveCredentials(creds) {
|
|
|
73
63
|
export function loadCredentials() {
|
|
74
64
|
migrateIfNeeded();
|
|
75
65
|
try {
|
|
76
|
-
|
|
66
|
+
// Mirror saveCredentials' symlink hardening on the read side: refuse a
|
|
67
|
+
// symlinked data dir, and open the file with O_NOFOLLOW so a pre-planted
|
|
68
|
+
// symlink at the credentials path cannot redirect the read elsewhere.
|
|
69
|
+
const dataDir = getDataDir();
|
|
70
|
+
if (existsSync(dataDir) && lstatSync(dataDir).isSymbolicLink()) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const raw = readSecretFile(getCredentialsFilePath()).trim();
|
|
77
74
|
let parsed;
|
|
78
75
|
try {
|
|
79
76
|
parsed = JSON.parse(raw);
|
package/dist/lib/crypto-aad.d.ts
CHANGED
|
@@ -2,4 +2,9 @@
|
|
|
2
2
|
* AAD (Additional Authenticated Data) builders for AES-256-GCM encryption.
|
|
3
3
|
* Ported from src/lib/crypto-aad.ts for CLI compatibility.
|
|
4
4
|
*/
|
|
5
|
-
export declare
|
|
5
|
+
export declare const VAULT_TYPE: {
|
|
6
|
+
readonly BLOB: "blob";
|
|
7
|
+
readonly OVERVIEW: "overview";
|
|
8
|
+
};
|
|
9
|
+
export type VaultType = (typeof VAULT_TYPE)[keyof typeof VAULT_TYPE];
|
|
10
|
+
export declare function buildPersonalEntryAAD(userId: string, entryId: string, vaultType: VaultType): Uint8Array;
|
package/dist/lib/crypto-aad.js
CHANGED
|
@@ -38,7 +38,13 @@ function buildAADBytes(scope, expectedFieldCount, fields) {
|
|
|
38
38
|
}
|
|
39
39
|
return bytes;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Mirror of src/lib/crypto/crypto-aad.ts VAULT_TYPE. Keep in sync —
|
|
42
|
+
// any change to the wire string values would break decrypt interop.
|
|
43
|
+
export const VAULT_TYPE = {
|
|
44
|
+
BLOB: "blob",
|
|
45
|
+
OVERVIEW: "overview",
|
|
46
|
+
};
|
|
47
|
+
export function buildPersonalEntryAAD(userId, entryId, vaultType) {
|
|
48
|
+
return buildAADBytes(SCOPE_PERSONAL, 3, [userId, entryId, vaultType]);
|
|
43
49
|
}
|
|
44
50
|
//# sourceMappingURL=crypto-aad.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlink-safe file I/O for secret material (credentials, decrypted exports).
|
|
3
|
+
*
|
|
4
|
+
* O_NOFOLLOW refuses to follow a symlink at the final path component, closing
|
|
5
|
+
* the symlink / check-then-write TOCTOU window that plain writeFileSync/
|
|
6
|
+
* readFileSync leave open. Used wherever the CLI persists or reads secrets.
|
|
7
|
+
*
|
|
8
|
+
* MIRROR: e2e/helpers/secure-file.ts holds the same O_NOFOLLOW logic (the cli
|
|
9
|
+
* package and the e2e tree compile under separate tsconfigs and cannot share a
|
|
10
|
+
* module). Keep the O_NOFOLLOW semantics in sync. This copy adds an `encoding`
|
|
11
|
+
* param + codeql annotation the e2e copy intentionally omits.
|
|
12
|
+
*/
|
|
13
|
+
/** Write `data` to `path` with O_NOFOLLOW + the given mode (default 0600). */
|
|
14
|
+
export declare function writeSecretFile(path: string, data: string, mode?: number): void;
|
|
15
|
+
/** Read `path` with O_NOFOLLOW (refuses a symlinked final component). */
|
|
16
|
+
export declare function readSecretFile(path: string, encoding?: BufferEncoding): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlink-safe file I/O for secret material (credentials, decrypted exports).
|
|
3
|
+
*
|
|
4
|
+
* O_NOFOLLOW refuses to follow a symlink at the final path component, closing
|
|
5
|
+
* the symlink / check-then-write TOCTOU window that plain writeFileSync/
|
|
6
|
+
* readFileSync leave open. Used wherever the CLI persists or reads secrets.
|
|
7
|
+
*
|
|
8
|
+
* MIRROR: e2e/helpers/secure-file.ts holds the same O_NOFOLLOW logic (the cli
|
|
9
|
+
* package and the e2e tree compile under separate tsconfigs and cannot share a
|
|
10
|
+
* module). Keep the O_NOFOLLOW semantics in sync. This copy adds an `encoding`
|
|
11
|
+
* param + codeql annotation the e2e copy intentionally omits.
|
|
12
|
+
*/
|
|
13
|
+
import { openSync, writeSync, closeSync, readFileSync, constants as fsConstants, } from "node:fs";
|
|
14
|
+
/** Write `data` to `path` with O_NOFOLLOW + the given mode (default 0600). */
|
|
15
|
+
export function writeSecretFile(path, data, mode = 0o600) {
|
|
16
|
+
const fd = openSync(path, fsConstants.O_WRONLY |
|
|
17
|
+
fsConstants.O_CREAT |
|
|
18
|
+
fsConstants.O_TRUNC |
|
|
19
|
+
(fsConstants.O_NOFOLLOW ?? 0), mode);
|
|
20
|
+
try {
|
|
21
|
+
// Intentional persistence of the caller's own secrets (OAuth tokens /
|
|
22
|
+
// decrypted export) to a 0600 O_NOFOLLOW file. CodeQL js/http-to-file-access
|
|
23
|
+
// is excluded for this in .github/codeql/codeql-config.yml (inline
|
|
24
|
+
// // codeql[...] suppression is not honored by GitHub code scanning here).
|
|
25
|
+
writeSync(fd, data);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
closeSync(fd);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Read `path` with O_NOFOLLOW (refuses a symlinked final component). */
|
|
32
|
+
export function readSecretFile(path, encoding = "utf-8") {
|
|
33
|
+
const fd = openSync(path, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
34
|
+
try {
|
|
35
|
+
return readFileSync(fd, encoding);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=secure-file.js.map
|