From 61f4c70423b0124328619b9335e4a9981f9a66c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 20:26:47 +0000 Subject: [PATCH 1/2] Drop the DERIVE_*_REQ_PRESS opcodes; press_required is now ignored Firmware removed opcodes 3 and 4 and burned the numbers, so sending either now returns CTAP2_ERR_EXTENSION_NOT_SUPPORTED. Always send 1 or 2. press_required is kept in the public API signature so existing callers still parse, but it no longer does anything. Presence is decided by the device from what is being asked for: a public-key derivation never prompts, a shared-secret derivation always does, with no setting to disable it. The suffix had also become a second key domain - the firmware set additional_data[0] = 1 for it, which changes the HKDF salt - so the same label produced two different keys depending on an opcode named after touches. That is how vault.js came to fetch its public key with press_required=false and run its ECDH with press_required=true: its comment explains the choice purely in terms of when a touch is needed, so whoever wrote it did not know the two calls were using different keys. It worked only because both sides were deterministic. One label now means one key. Derived secrets change as a result. Vault entries and anything else derived through the REQ_PRESS path will not reproduce. Pre-release, no migration. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011sMEydE1HrJTCPkJ25RLRY --- .../onlykey/onlykey-3rd-party.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/onlykey-fido2/onlykey/onlykey-3rd-party.js b/src/onlykey-fido2/onlykey/onlykey-3rd-party.js index a82be5b8..163b0547 100644 --- a/src/onlykey-fido2/onlykey/onlykey-3rd-party.js +++ b/src/onlykey-fido2/onlykey/onlykey-3rd-party.js @@ -47,11 +47,21 @@ module.exports = function(imports, onlykeyApi) { CURVE25519: 3 }; + // 3 and 4 (DERIVE_*_REQ_PRESS) were removed from the firmware and the + // numbers are burned, not reused - sending either now gets + // CTAP2_ERR_EXTENSION_NOT_SUPPORTED rather than being reinterpreted. + // + // The `press_required` argument these mapped to is now IGNORED, and kept + // only so existing callers still parse. Presence is decided by the device + // from what is being asked for: deriving a public key never prompts, + // deriving a shared secret always does, with no setting to turn it off. + // The suffix had also quietly become a second key domain (the firmware set + // additional_data[0] = 1 for it, changing the HKDF salt), which is how + // vault.js ended up fetching its public key in one domain and doing its + // ECDH in the other. One label now means one key. var KEYACTION = { DERIVE_PUBLIC_KEY: 1, - DERIVE_SHARED_SECRET: 2, - DERIVE_PUBLIC_KEY_REQ_PRESS: 3, - DERIVE_SHARED_SECRET_REQ_PRESS: 4 + DERIVE_SHARED_SECRET: 2 }; // Uint8Array.from() is NOT a string encoder. Given a string it treats it as @@ -312,7 +322,7 @@ module.exports = function(imports, onlykeyApi) { } Array.prototype.push.apply(message, dataHash); - var keyAction = press_required ? KEYACTION.DERIVE_PUBLIC_KEY_REQ_PRESS : KEYACTION.DERIVE_PUBLIC_KEY; + var keyAction = KEYACTION.DERIVE_PUBLIC_KEY; // press_required ignored, see KEYACTION var enc_resp = 1; await onlykeyApi.ctaphid_via_webauthn(cmd, keyAction, keytype, enc_resp, message, 60000).then(async(response) => { @@ -413,7 +423,7 @@ module.exports = function(imports, onlykeyApi) { //msg("input pubkey -> " + pubkey) //msg("full message -> " + message) - var keyAction = press_required ? KEYACTION.DERIVE_SHARED_SECRET_REQ_PRESS : KEYACTION.DERIVE_SHARED_SECRET; + var keyAction = KEYACTION.DERIVE_SHARED_SECRET; // press_required ignored; the device always prompts var enc_resp = 1; await onlykeyApi.ctaphid_via_webauthn(cmd, keyAction, keytype, enc_resp, message, 60000).then(async(response) => { @@ -523,9 +533,7 @@ module.exports = function(imports, onlykeyApi) { Array.prototype.push.apply(message, labelHash); if (ctX) Array.prototype.push.apply(message, Array.from(ctX)); - var keyAction = ctX - ? (press_required ? KEYACTION.DERIVE_SHARED_SECRET_REQ_PRESS : KEYACTION.DERIVE_SHARED_SECRET) - : (press_required ? KEYACTION.DERIVE_PUBLIC_KEY_REQ_PRESS : KEYACTION.DERIVE_PUBLIC_KEY); + var keyAction = ctX ? KEYACTION.DERIVE_SHARED_SECRET : KEYACTION.DERIVE_PUBLIC_KEY; // If the OnlyKey is set to "Challenge Code" for web derived keys // (webderivemode 0), a shared-secret derive makes the device wait From 740f1c9dcc3a0a40ede4571de120e5e8445b22a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 20:32:40 +0000 Subject: [PATCH 2/2] Remove the dead history module src/lib/history.js could never have run. It calls ok.derive_public_key("onlykey-gun", function (error, historyPubkey) {...}) with two arguments against a four-argument signature (additional_d, keytype, press_required, cb), so `keytype` receives the callback function, `press_required` and `cb` are undefined, and nothing is ever called back. Its only reference was plugins-devel.js - it is not in plugins.js, so it never shipped in a production bundle either. Found while auditing callers of the derive API for the REQ_PRESS opcode removal: it was the only other caller, and it turned out not to be one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011sMEydE1HrJTCPkJ25RLRY --- src/lib/history.js | 114 ------------------------------------------- src/plugins-devel.js | 1 - 2 files changed, 115 deletions(-) delete mode 100644 src/lib/history.js diff --git a/src/lib/history.js b/src/lib/history.js deleted file mode 100644 index f5ca36a3..00000000 --- a/src/lib/history.js +++ /dev/null @@ -1,114 +0,0 @@ -//change _template_ to your plugin name -module.exports = { - consumes: ["app", "onlykey3rd", "newGun", "forge", "SEA"], - provides: ["history"], - - setup: async function(options, imports, register) { - var historyAPI = {}; - - var onlykey3rd = imports.onlykey3rd; - var ok = onlykey3rd(1, 0); - var newGun = imports.newGun; - var SEA = imports.SEA; - var forge = imports.forge; - - var gun = newGun(); - - /**/ - - var disconnected_PK = window.localStorage.onlykey_has_history; - if (!disconnected_PK) { - disconnected_PK = JSON.stringify(await SEA.pair()); - window.localStorage.onlykey_has_history = disconnected_PK; - } - - disconnected_PK = JSON.parse(disconnected_PK); - - // var disconnected_PUBKEY = disconnected_PK.epub; - var disconnected_SECRET = await SEA.secret(disconnected_PK, disconnected_PK); - - historyAPI.historyEnabled = false; - - var historyPUBKEY = false; - var historySECRET = false; - - historyAPI.ready = false; - - historyAPI.init = function() { - - } - - historyAPI.setup = function() { - - } - - function doGunAuth(finished) { - var gunUID = forge.sha256.create().update(historyPUBKEY).digest().toHex(); - var gunPASS = forge.sha256.create().update(historySECRET).digest().toHex(); - gun.user().auth(gunUID, gunPASS, async function(err, res) { - if (err.err) { - gun.user().create(gunUID, gunPASS, finished); - } - else - finished(); - }); - } - - var encrypt = function(message) { - if (!historySECRET) - return ok.encrypt(message, disconnected_SECRET); - return ok.encrypt(message, historySECRET); - }; - var decrypt = function(message) { - if (!historySECRET) - return ok.decrypt(message, disconnected_SECRET); - return ok.decrypt(message, historySECRET); - }; - - - historyAPI.history = { - get: async function(key) { - var hist = gun.user().get("history"); - return decrypt(await hist.get(key)); - }, - set: async function(key, message) { - var hist = gun.user().get("history"); - return hist.get(key).put(await encrypt(message)); - } - }; - /* - - if (ok.history) { - $("#pgpkeyurl2").val(await ok.history.get("pgpkeyurl2")); - $("#pgpkeyurl2").change(function() { - ok.history.set("pgpkeyurl2", $("#pgpkeyurl2").val()); - }); - - $("#pgpkeyurl").val(await ok.history.get("pgpkeyurl")); - $("#pgpkeyurl").change(function() { - ok.history.set("pgpkeyurl", $("#pgpkeyurl").val()); - }); - }*/ - - function doConnect() { - return new Promise(async function(resolve) { - ok.connect(function() { - if (ok.derive_public_key) { - // disable_onlykey = false; - ok.derive_public_key("onlykey-gun", function(error, historyPubkey) { - ok.derive_shared_secret("onlykey-gun", historyPubkey, async function(error, historySecret) { - - }); - }); - } - }); - }); - } - - register(null, { - history: historyAPI - }); - - } - -}; \ No newline at end of file diff --git a/src/plugins-devel.js b/src/plugins-devel.js index f14643ee..d4ee81d2 100644 --- a/src/plugins-devel.js +++ b/src/plugins-devel.js @@ -18,7 +18,6 @@ module.exports.push(require("./plugins/console/console_debug.js")); module.exports.push(require("./plugins/chat/chat.js")); /* for encrypted data to for onlykey devices */ -module.exports.push(require("./lib/history.js")); module.exports.push(require("./plugins/password-generator/password-generator.js"));