cicy-desktop 2.1.362 → 2.1.364
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/package.json +1 -1
- package/src/inject/no-passkey.js +39 -0
- package/src/main.js +34 -2
- package/src/utils/window-utils.js +14 -0
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// document-start 注入:让页面认为这台机器没有可用的通行密钥验证器。
|
|
5
|
+
//
|
|
6
|
+
// 为什么需要:accounts.meta.com 这类站点一进登录页就调 navigator.credentials.get({publicKey}),
|
|
7
|
+
// Windows 随即弹出「使用密钥登录 · 请将安全密钥插入 USB 端口」这个**系统模态框**。机器上
|
|
8
|
+
// 根本没有安全密钥,框又是系统级的、页面脚本关不掉,矩阵里正在跑的自动登录就卡死在那儿。
|
|
9
|
+
//
|
|
10
|
+
// 为什么不用 --disable-features=WebAuthentication:那个 feature 名在现在的 Chromium 里
|
|
11
|
+
// 已经不存在了,开关传得进去但完全不起作用(2026-09-21 在 2.1.362 上实测,PublicKeyCredential
|
|
12
|
+
// 照样是 function)。
|
|
13
|
+
//
|
|
14
|
+
// 为什么不直接 delete PublicKeyCredential:整个对象拿掉会让 Facebook 的两步验证页渲染不出来
|
|
15
|
+
// (Comet 挂载了但树是空的,实测白板)。所以对象保留,只回答「没有验证器」,并让 passkey 请求
|
|
16
|
+
// 以标准的 NotAllowedError(等同用户取消)拒绝 —— 站点对这个错误都有正常的密码回退路径。
|
|
17
|
+
(function () {
|
|
18
|
+
try {
|
|
19
|
+
if (window.PublicKeyCredential) {
|
|
20
|
+
const no = () => Promise.resolve(false);
|
|
21
|
+
try { PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = no; } catch (e) {}
|
|
22
|
+
try { PublicKeyCredential.isConditionalMediationAvailable = no; } catch (e) {}
|
|
23
|
+
try {
|
|
24
|
+
if (PublicKeyCredential.getClientCapabilities) {
|
|
25
|
+
PublicKeyCredential.getClientCapabilities = () => Promise.resolve({});
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {}
|
|
28
|
+
}
|
|
29
|
+
if (navigator.credentials) {
|
|
30
|
+
const get = navigator.credentials.get.bind(navigator.credentials);
|
|
31
|
+
const create = navigator.credentials.create ? navigator.credentials.create.bind(navigator.credentials) : null;
|
|
32
|
+
const deny = () => Promise.reject(
|
|
33
|
+
new DOMException("The operation either timed out or was not allowed.", "NotAllowedError"));
|
|
34
|
+
// 只挡 publicKey(通行密钥);密码管理器那套 credentials 不受影响。
|
|
35
|
+
navigator.credentials.get = (o) => (o && o.publicKey ? deny() : get(o));
|
|
36
|
+
if (create) navigator.credentials.create = (o) => (o && o.publicKey ? deny() : create(o));
|
|
37
|
+
}
|
|
38
|
+
} catch (e) { /* 注入失败不能影响页面 */ }
|
|
39
|
+
})();
|
package/src/main.js
CHANGED
|
@@ -24,6 +24,24 @@ const appUpdater = require("./app-updater");
|
|
|
24
24
|
// and guests fell back to the OS-native menu; this unifies them and adds 重新加载
|
|
25
25
|
// + 切换开发者工具 + 检查元素 everywhere (see utils/context-menu-options.js).
|
|
26
26
|
const { attachContextMenu } = require("./utils/context-menu-options");
|
|
27
|
+
// 每个 session 一建出来就挂上 no-passkey:document-start 运行,先于页面脚本,
|
|
28
|
+
// 这样站点探测通行密钥时拿到的就是「没有验证器」,Windows 那个系统模态框不会弹。
|
|
29
|
+
// 放在 session-created 而不是逐个分区手动注册 —— 矩阵有上百个 persist:sandbox-N,
|
|
30
|
+
// 而且新建 profile 随时会多出来,手动注册必然漏。
|
|
31
|
+
const NO_PASSKEY = require("path").join(__dirname, "inject", "no-passkey.js");
|
|
32
|
+
function armNoPasskey(ses) {
|
|
33
|
+
try {
|
|
34
|
+
const has = (ses.getPreloadScripts ? ses.getPreloadScripts() : [])
|
|
35
|
+
.some((x) => String((x && x.filePath) || "").endsWith("no-passkey.js"));
|
|
36
|
+
if (!has) ses.registerPreloadScript({ type: "frame", filePath: NO_PASSKEY });
|
|
37
|
+
} catch (_) {}
|
|
38
|
+
}
|
|
39
|
+
electronApp.on("session-created", armNoPasskey);
|
|
40
|
+
// 默认会话可能在上面这个监听器挂上之前就已经建好了(main.js 里别处也踩过同一个坑,
|
|
41
|
+
// 见启动时那段「默认会话强制直连」的保险),所以 ready 之后再补一次。幂等。
|
|
42
|
+
electronApp.whenReady().then(() => {
|
|
43
|
+
try { armNoPasskey(require("electron").session.defaultSession); } catch (_) {}
|
|
44
|
+
});
|
|
27
45
|
electronApp.on("web-contents-created", (_e, wc) => {
|
|
28
46
|
attachContextMenu(wc);
|
|
29
47
|
// Security backstop for window.open. createWindow (setupWindowHandlers) and the
|
|
@@ -35,8 +53,20 @@ electronApp.on("web-contents-created", (_e, wc) => {
|
|
|
35
53
|
// an external popup there is an RCE/creds-leak surface. External links go to the
|
|
36
54
|
// system browser instead; trusted/local/about:blank popups are still allowed.
|
|
37
55
|
try {
|
|
38
|
-
const { windowOpenDecision } = require("./utils/window-utils");
|
|
56
|
+
const { windowOpenDecision, isWebUrl } = require("./utils/window-utils");
|
|
39
57
|
wc.setWindowOpenHandler(({ url }) => windowOpenDecision(url, { wc }));
|
|
58
|
+
// 同一件事的另一条入口:直接导航(点链接 / location=)到未知协议。
|
|
59
|
+
// 开窗那条被拦了,这条不拦照样会把 bitbrowser:// 这类交给系统去弹框。
|
|
60
|
+
wc.on("will-navigate", (e, url) => {
|
|
61
|
+
if (isWebUrl(url)) return;
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
let from = "";
|
|
64
|
+
try { from = wc.getURL(); } catch (_) {}
|
|
65
|
+
try {
|
|
66
|
+
require("electron-log").warn(
|
|
67
|
+
`[Navigate] 外部协议已拦截: ${String(url).slice(0, 120)} 来自: ${String(from).slice(0, 120)}`);
|
|
68
|
+
} catch (_) {}
|
|
69
|
+
});
|
|
40
70
|
} catch (_) {}
|
|
41
71
|
// Ctrl/Cmd+C inside a <webview> guest: on Windows the application menu's
|
|
42
72
|
// role:"copy" accelerator doesn't reach the focused guest, so a selection
|
|
@@ -89,7 +119,9 @@ electronApp.commandLine.appendSwitch("remote-allow-origins", "*");
|
|
|
89
119
|
// 「使用密钥登录 · 请将安全密钥插入 USB 端口」这个系统模态框 —— 机器上根本没有安全密钥,
|
|
90
120
|
// 框又是系统级的、页面脚本关不掉,自动登录就卡死在那儿(实测 2026-09-21,FB #96)。
|
|
91
121
|
// 关掉这个特性后 navigator.credentials / PublicKeyCredential 直接不存在,站点自动回落到密码。
|
|
92
|
-
|
|
122
|
+
// 注:这里**不要**再写 disable-features=WebAuthentication —— 那个 feature 名在现在的
|
|
123
|
+
// Chromium 里已经没有了,开关传得进去但毫无作用(2.1.362 实测)。真正起作用的是下面
|
|
124
|
+
// session-created 里注册的 no-passkey 注入脚本。
|
|
93
125
|
if (process.platform === "linux") {
|
|
94
126
|
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
|
|
95
127
|
// electronApp.commandLine.appendSwitch("disable-setuid-sandbox");
|
|
@@ -57,8 +57,21 @@ function accountIdxOfWebContents(wc) {
|
|
|
57
57
|
// sandbox profile 1; an opener already in a sandbox profile N opens the link in
|
|
58
58
|
// its OWN profile N. This mirrors the tab-browser policy (profile 0 link →
|
|
59
59
|
// openTab(1); else same profile) — no dialog, no leak into profile 0.
|
|
60
|
+
// 只有这些协议才允许在应用里导航/开窗。其余(bitbrowser:// tg:// whatsapp:// …)
|
|
61
|
+
// 交给 Chromium 就等于交给系统:Windows 弹「获取打开此链接的应用」那个模态框,
|
|
62
|
+
// 机器上没装对应软件也关不掉,自动化直接卡住(实测 2026-09-21,群消息里的
|
|
63
|
+
// bitbrowser:// 推广链接)。这里直接拒掉,并把来源打进日志,便于回溯是谁发起的。
|
|
64
|
+
const WEB_SCHEME = /^(https?|about|blob|data|file|cicyui|devtools|chrome-extension):/i;
|
|
65
|
+
function isWebUrl(u) { return WEB_SCHEME.test(String(u || "")); }
|
|
66
|
+
|
|
60
67
|
function windowOpenDecision(url, { wc } = {}) {
|
|
61
68
|
log.info(`[WindowOpen] Intercepted: ${url}`);
|
|
69
|
+
if (url && !isWebUrl(url)) {
|
|
70
|
+
let from = "";
|
|
71
|
+
try { from = wc ? wc.getURL() : ""; } catch (e) {}
|
|
72
|
+
log.warn(`[WindowOpen] 外部协议已拦截: ${String(url).slice(0, 120)} 来自: ${String(from).slice(0, 120)}`);
|
|
73
|
+
return { action: "deny" };
|
|
74
|
+
}
|
|
62
75
|
if (!url || url === "about:blank" || isTrustedUrl(url)) {
|
|
63
76
|
return {
|
|
64
77
|
action: "allow",
|
|
@@ -567,6 +580,7 @@ if (app) {
|
|
|
567
580
|
}
|
|
568
581
|
|
|
569
582
|
module.exports = {
|
|
583
|
+
isWebUrl,
|
|
570
584
|
createWindow,
|
|
571
585
|
setupWindowHandlers,
|
|
572
586
|
windowOpenDecision,
|