creditdaddy 1.2.0 → 1.3.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.
package/src/zcrypto.js CHANGED
@@ -1,160 +1,160 @@
1
- /**
2
- * ZCode(智谱 GLM / Z.ai 桌面客户端)本机凭据加解密 — 移植自 zcode-switch(MIT)src-tauri/src/zcrypto.rs。
3
- *
4
- * 凭据文件 ~/.zcode/v2/credentials.json 里每个值可能是:
5
- * - 明文字符串
6
- * - "enc:v1:<nonce>.<tag>.<密文>"(均为 URL-safe base64,无 padding),AES-256-GCM,
7
- * key = SHA256(secret),secret 默认 "zcode-credential-fallback:<平台>:<home>:<用户名>",
8
- * 可用环境变量 ZCODE_CREDENTIAL_SECRET 覆盖(客户端本身也读这个变量)。
9
- * 纯函数、无 I/O,供 zcodeLocal.js 调用,也便于单测。
10
- */
11
-
12
- import crypto from 'node:crypto';
13
-
14
- export const ENC_PREFIX = 'enc:v1:';
15
-
16
- /** node/rust 平台名 → zcode 使用的平台标识(win32 / darwin / linux) */
17
- export function nodePlatformFor(platform) {
18
- if (platform === 'windows') return 'win32';
19
- if (platform === 'macos') return 'darwin';
20
- return platform;
21
- }
22
-
23
- /** 未设置 ZCODE_CREDENTIAL_SECRET 时的回退密钥来源 */
24
- export function defaultSecret(home, { platform = process.platform, username } = {}) {
25
- const envSecret = process.env.ZCODE_CREDENTIAL_SECRET;
26
- if (envSecret) return envSecret;
27
- const user = username || process.env.USERNAME || process.env.USER || process.env.LOGNAME || 'unknown';
28
- return `zcode-credential-fallback:${platform}:${home}:${user}`;
29
- }
30
-
31
- function deriveKey(secret) {
32
- return crypto.createHash('sha256').update(secret, 'utf8').digest();
33
- }
34
-
35
- export function isEncrypted(v) {
36
- return typeof v === 'string' && v.startsWith(ENC_PREFIX);
37
- }
38
-
39
- function b64u(s) {
40
- return Buffer.from(s, 'base64url');
41
- }
42
-
43
- /** 解密单个 "enc:v1:…" 值;输入非该格式时报错(调用方按需 try/catch 或用 safeDecrypt) */
44
- export function decryptWithSecret(value, secret) {
45
- if (!isEncrypted(value)) throw new Error('不是 enc:v1 格式');
46
- const parts = value.slice(ENC_PREFIX.length).split('.');
47
- if (parts.length !== 3) throw new Error('enc:v1 格式不正确');
48
- const [nonceB64, tagB64, ctB64] = parts;
49
- const nonce = b64u(nonceB64);
50
- if (nonce.length !== 12) throw new Error('nonce 长度异常');
51
- const tag = b64u(tagB64);
52
- const ct = b64u(ctB64);
53
- const decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(secret), nonce);
54
- decipher.setAuthTag(tag);
55
- try {
56
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
57
- } catch {
58
- throw new Error('解密失败(密钥不匹配或数据损坏)');
59
- }
60
- }
61
-
62
- /** 加密成 "enc:v1:…"(与 decryptWithSecret 互逆):浏览器登录拿到的凭据按客户端格式落盘,切换时才能被 ZCode 读懂 */
63
- export function encryptWithSecret(plain, secret) {
64
- const nonce = crypto.randomBytes(12);
65
- const cipher = crypto.createCipheriv('aes-256-gcm', deriveKey(secret), nonce);
66
- const ct = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
67
- return ENC_PREFIX + [nonce, cipher.getAuthTag(), ct].map((b) => b.toString('base64url')).join('.');
68
- }
69
-
70
- /** 值可能加密也可能明文,统一转明文;失败返回 null(不抛错) */
71
- export function safeDecrypt(value, secret) {
72
- if (typeof value !== 'string') return null;
73
- if (!isEncrypted(value)) return value;
74
- try { return decryptWithSecret(value, secret); } catch { return null; }
75
- }
76
-
77
- /** 明文/加密的 JSON 字符串 → 解析后的对象;失败返回 null */
78
- export function decryptJsonOpt(value, secret) {
79
- const plain = safeDecrypt(value, secret);
80
- if (plain === null) return null;
81
- try { return JSON.parse(plain); } catch { return null; }
82
- }
83
-
84
- /** 解码 JWT payload(不校验签名,仅用于展示/识别用户,与 zcode 客户端自身用途一致) */
85
- export function decodeJwt(jwt) {
86
- try {
87
- const seg = String(jwt).split('.')[1];
88
- if (!seg) return null;
89
- return JSON.parse(Buffer.from(seg, 'base64url').toString('utf8'));
90
- } catch {
91
- return null;
92
- }
93
- }
94
-
95
- /**
96
- * 从凭据对象里提取账号身份:{ provider, username, displayName, email, userId }。
97
- * 逻辑对齐 zcrypto.rs identity_with_secret:先看 active_provider 的 user_info,
98
- * 找不到 user_id 时退而解码 access_token 这个 JWT。
99
- */
100
- export function identityWithSecret(creds, secret) {
101
- const id = { provider: 'bigmodel', username: null, displayName: null, email: null, userId: null };
102
- if (!creds || typeof creds !== 'object') return id;
103
- const apRaw = creds['oauth:active_provider'];
104
- if (typeof apRaw === 'string') {
105
- const plain = isEncrypted(apRaw) ? safeDecrypt(apRaw, secret) : apRaw;
106
- if (plain) id.provider = plain;
107
- }
108
- const ui = decryptJsonOpt(creds[`oauth:${id.provider}:user_info`], secret);
109
- if (ui && typeof ui === 'object') {
110
- id.username = typeof ui.username === 'string' ? ui.username : null;
111
- id.displayName = typeof ui.displayName === 'string' ? ui.displayName : null;
112
- id.email = typeof ui.email === 'string' ? ui.email
113
- : typeof ui.rawProfile?.email === 'string' ? ui.rawProfile.email : null;
114
- if (ui.id !== undefined && ui.id !== null) {
115
- id.userId = typeof ui.id === 'string' ? ui.id : JSON.stringify(ui.id);
116
- }
117
- }
118
- if (!id.userId) {
119
- const at = creds[`oauth:${id.provider}:access_token`];
120
- const plain = typeof at === 'string' ? (isEncrypted(at) ? safeDecrypt(at, secret) : at) : null;
121
- const jwt = plain ? decodeJwt(plain) : null;
122
- if (jwt) id.userId = jwt.user_id ?? jwt.sub ?? null;
123
- }
124
- return id;
125
- }
126
-
127
- export function identityLabel(id) {
128
- return id.displayName || id.username || id.email || null;
129
- }
130
-
131
- /** 是否已登录:有任意 oauth:*:access_token,或有非空 zcodejwttoken */
132
- export function isLoggedIn(creds) {
133
- if (!creds || typeof creds !== 'object') return false;
134
- if (Object.keys(creds).some((k) => k.startsWith('oauth:') && k.endsWith(':access_token'))) return true;
135
- const jwt = creds.zcodejwttoken;
136
- return typeof jwt === 'string' && jwt.trim().length > 0;
137
- }
138
-
139
- const DEVICE_KEY_PREFIX = 'web-remote-control:';
140
-
141
- /**
142
- * 账号去重用的规范化哈希:剔除设备相关键(web-remote-control:…,含远程控制中继密钥,
143
- * 这些跟着设备走而不是账号,不能影响“是否是同一个登录”的判断)后对 JSON 取 SHA256。
144
- */
145
- function stableStringify(v) {
146
- if (Array.isArray(v)) return '[' + v.map(stableStringify).join(',') + ']';
147
- if (v && typeof v === 'object') {
148
- return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + stableStringify(v[k])).join(',') + '}';
149
- }
150
- return JSON.stringify(v);
151
- }
152
-
153
- export function canonicalHash(creds) {
154
- let obj = creds;
155
- if (creds && typeof creds === 'object' && !Array.isArray(creds)) {
156
- obj = Object.fromEntries(Object.entries(creds).filter(([k]) => !k.startsWith(DEVICE_KEY_PREFIX)));
157
- }
158
- // 键序无关的稳定序列化:credentials.json 被 ZCode 重写后键序可能变,但同一登录应得同一 hash
159
- return crypto.createHash('sha256').update(stableStringify(obj)).digest('hex');
160
- }
1
+ /**
2
+ * ZCode(智谱 GLM / Z.ai 桌面客户端)本机凭据加解密 — 移植自 zcode-switch(MIT)src-tauri/src/zcrypto.rs。
3
+ *
4
+ * 凭据文件 ~/.zcode/v2/credentials.json 里每个值可能是:
5
+ * - 明文字符串
6
+ * - "enc:v1:<nonce>.<tag>.<密文>"(均为 URL-safe base64,无 padding),AES-256-GCM,
7
+ * key = SHA256(secret),secret 默认 "zcode-credential-fallback:<平台>:<home>:<用户名>",
8
+ * 可用环境变量 ZCODE_CREDENTIAL_SECRET 覆盖(客户端本身也读这个变量)。
9
+ * 纯函数、无 I/O,供 zcodeLocal.js 调用,也便于单测。
10
+ */
11
+
12
+ import crypto from 'node:crypto';
13
+
14
+ export const ENC_PREFIX = 'enc:v1:';
15
+
16
+ /** node/rust 平台名 → zcode 使用的平台标识(win32 / darwin / linux) */
17
+ export function nodePlatformFor(platform) {
18
+ if (platform === 'windows') return 'win32';
19
+ if (platform === 'macos') return 'darwin';
20
+ return platform;
21
+ }
22
+
23
+ /** 未设置 ZCODE_CREDENTIAL_SECRET 时的回退密钥来源 */
24
+ export function defaultSecret(home, { platform = process.platform, username } = {}) {
25
+ const envSecret = process.env.ZCODE_CREDENTIAL_SECRET;
26
+ if (envSecret) return envSecret;
27
+ const user = username || process.env.USERNAME || process.env.USER || process.env.LOGNAME || 'unknown';
28
+ return `zcode-credential-fallback:${platform}:${home}:${user}`;
29
+ }
30
+
31
+ function deriveKey(secret) {
32
+ return crypto.createHash('sha256').update(secret, 'utf8').digest();
33
+ }
34
+
35
+ export function isEncrypted(v) {
36
+ return typeof v === 'string' && v.startsWith(ENC_PREFIX);
37
+ }
38
+
39
+ function b64u(s) {
40
+ return Buffer.from(s, 'base64url');
41
+ }
42
+
43
+ /** 解密单个 "enc:v1:…" 值;输入非该格式时报错(调用方按需 try/catch 或用 safeDecrypt) */
44
+ export function decryptWithSecret(value, secret) {
45
+ if (!isEncrypted(value)) throw new Error('不是 enc:v1 格式');
46
+ const parts = value.slice(ENC_PREFIX.length).split('.');
47
+ if (parts.length !== 3) throw new Error('enc:v1 格式不正确');
48
+ const [nonceB64, tagB64, ctB64] = parts;
49
+ const nonce = b64u(nonceB64);
50
+ if (nonce.length !== 12) throw new Error('nonce 长度异常');
51
+ const tag = b64u(tagB64);
52
+ const ct = b64u(ctB64);
53
+ const decipher = crypto.createDecipheriv('aes-256-gcm', deriveKey(secret), nonce);
54
+ decipher.setAuthTag(tag);
55
+ try {
56
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
57
+ } catch {
58
+ throw new Error('解密失败(密钥不匹配或数据损坏)');
59
+ }
60
+ }
61
+
62
+ /** 加密成 "enc:v1:…"(与 decryptWithSecret 互逆):浏览器登录拿到的凭据按客户端格式落盘,切换时才能被 ZCode 读懂 */
63
+ export function encryptWithSecret(plain, secret) {
64
+ const nonce = crypto.randomBytes(12);
65
+ const cipher = crypto.createCipheriv('aes-256-gcm', deriveKey(secret), nonce);
66
+ const ct = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]);
67
+ return ENC_PREFIX + [nonce, cipher.getAuthTag(), ct].map((b) => b.toString('base64url')).join('.');
68
+ }
69
+
70
+ /** 值可能加密也可能明文,统一转明文;失败返回 null(不抛错) */
71
+ export function safeDecrypt(value, secret) {
72
+ if (typeof value !== 'string') return null;
73
+ if (!isEncrypted(value)) return value;
74
+ try { return decryptWithSecret(value, secret); } catch { return null; }
75
+ }
76
+
77
+ /** 明文/加密的 JSON 字符串 → 解析后的对象;失败返回 null */
78
+ export function decryptJsonOpt(value, secret) {
79
+ const plain = safeDecrypt(value, secret);
80
+ if (plain === null) return null;
81
+ try { return JSON.parse(plain); } catch { return null; }
82
+ }
83
+
84
+ /** 解码 JWT payload(不校验签名,仅用于展示/识别用户,与 zcode 客户端自身用途一致) */
85
+ export function decodeJwt(jwt) {
86
+ try {
87
+ const seg = String(jwt).split('.')[1];
88
+ if (!seg) return null;
89
+ return JSON.parse(Buffer.from(seg, 'base64url').toString('utf8'));
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 从凭据对象里提取账号身份:{ provider, username, displayName, email, userId }。
97
+ * 逻辑对齐 zcrypto.rs identity_with_secret:先看 active_provider 的 user_info,
98
+ * 找不到 user_id 时退而解码 access_token 这个 JWT。
99
+ */
100
+ export function identityWithSecret(creds, secret) {
101
+ const id = { provider: 'bigmodel', username: null, displayName: null, email: null, userId: null };
102
+ if (!creds || typeof creds !== 'object') return id;
103
+ const apRaw = creds['oauth:active_provider'];
104
+ if (typeof apRaw === 'string') {
105
+ const plain = isEncrypted(apRaw) ? safeDecrypt(apRaw, secret) : apRaw;
106
+ if (plain) id.provider = plain;
107
+ }
108
+ const ui = decryptJsonOpt(creds[`oauth:${id.provider}:user_info`], secret);
109
+ if (ui && typeof ui === 'object') {
110
+ id.username = typeof ui.username === 'string' ? ui.username : null;
111
+ id.displayName = typeof ui.displayName === 'string' ? ui.displayName : null;
112
+ id.email = typeof ui.email === 'string' ? ui.email
113
+ : typeof ui.rawProfile?.email === 'string' ? ui.rawProfile.email : null;
114
+ if (ui.id !== undefined && ui.id !== null) {
115
+ id.userId = typeof ui.id === 'string' ? ui.id : JSON.stringify(ui.id);
116
+ }
117
+ }
118
+ if (!id.userId) {
119
+ const at = creds[`oauth:${id.provider}:access_token`];
120
+ const plain = typeof at === 'string' ? (isEncrypted(at) ? safeDecrypt(at, secret) : at) : null;
121
+ const jwt = plain ? decodeJwt(plain) : null;
122
+ if (jwt) id.userId = jwt.user_id ?? jwt.sub ?? null;
123
+ }
124
+ return id;
125
+ }
126
+
127
+ export function identityLabel(id) {
128
+ return id.displayName || id.username || id.email || null;
129
+ }
130
+
131
+ /** 是否已登录:有任意 oauth:*:access_token,或有非空 zcodejwttoken */
132
+ export function isLoggedIn(creds) {
133
+ if (!creds || typeof creds !== 'object') return false;
134
+ if (Object.keys(creds).some((k) => k.startsWith('oauth:') && k.endsWith(':access_token'))) return true;
135
+ const jwt = creds.zcodejwttoken;
136
+ return typeof jwt === 'string' && jwt.trim().length > 0;
137
+ }
138
+
139
+ const DEVICE_KEY_PREFIX = 'web-remote-control:';
140
+
141
+ /**
142
+ * 账号去重用的规范化哈希:剔除设备相关键(web-remote-control:…,含远程控制中继密钥,
143
+ * 这些跟着设备走而不是账号,不能影响“是否是同一个登录”的判断)后对 JSON 取 SHA256。
144
+ */
145
+ function stableStringify(v) {
146
+ if (Array.isArray(v)) return '[' + v.map(stableStringify).join(',') + ']';
147
+ if (v && typeof v === 'object') {
148
+ return '{' + Object.keys(v).sort().map((k) => JSON.stringify(k) + ':' + stableStringify(v[k])).join(',') + '}';
149
+ }
150
+ return JSON.stringify(v);
151
+ }
152
+
153
+ export function canonicalHash(creds) {
154
+ let obj = creds;
155
+ if (creds && typeof creds === 'object' && !Array.isArray(creds)) {
156
+ obj = Object.fromEntries(Object.entries(creds).filter(([k]) => !k.startsWith(DEVICE_KEY_PREFIX)));
157
+ }
158
+ // 键序无关的稳定序列化:credentials.json 被 ZCode 重写后键序可能变,但同一登录应得同一 hash
159
+ return crypto.createHash('sha256').update(stableStringify(obj)).digest('hex');
160
+ }