From 8932a84613a9df0147d5139eab29738d0db49036 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 6 Jan 2025 17:12:13 -0500 Subject: [PATCH] fast: Use Web crypto (#1045) --- package-lock.json | 3 -- packages/client/index.js | 6 +-- packages/client/package.json | 4 +- packages/sasl-ht-sha-256-none/index.js | 35 +++++++++++++----- packages/sasl-ht-sha-256-none/package.json | 3 -- packages/sasl/index.js | 6 +-- packages/sasl2/index.js | 15 ++++---- packages/sasl2/test.js | 43 ++++++++++++++++++++++ 8 files changed, 84 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 260773a6..13d18a4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14326,9 +14326,6 @@ "name": "@xmpp/sasl-ht-sha-256-none", "version": "0.14.0", "license": "ISC", - "dependencies": { - "create-hmac": "^1.1.7" - }, "engines": { "node": ">= 20" } diff --git a/packages/client/index.js b/packages/client/index.js index 81e18b62..9888f5af 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -68,16 +68,16 @@ function client(options = {}) { createOnAuthenticate(credentials ?? { username, password }, userAgent), ); - const fast = setupIfAvailable(_fast, { + const fast = _fast({ sasl2, }); - fast && sasl2.setup({ fast }); + sasl2.setup({ fast }); // SASL2 inline features const bind2 = _bind2({ sasl2, entity }, resource); // FAST mechanisms - order matters and define priority - fast && setupIfAvailable(htsha256none, fast.saslFactory); + htsha256none(fast.saslFactory); // Stream features - order matters and define priority const sasl = _sasl( diff --git a/packages/client/package.json b/packages/client/package.json index 78efcbe7..f23b77b7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -32,9 +32,7 @@ "@xmpp/tcp": false, "@xmpp/tls": false, "@xmpp/starttls": false, - "@xmpp/sasl-scram-sha-1": false, - "@xmpp/sasl-ht-sha-256-none": false, - "@xmpp/fast": false + "@xmpp/sasl-scram-sha-1": false }, "engines": { "node": ">= 20" diff --git a/packages/sasl-ht-sha-256-none/index.js b/packages/sasl-ht-sha-256-none/index.js index 195c4f2f..591fa32f 100644 --- a/packages/sasl-ht-sha-256-none/index.js +++ b/packages/sasl-ht-sha-256-none/index.js @@ -1,5 +1,4 @@ // https://datatracker.ietf.org/doc/draft-schmaus-kitten-sasl-ht/ -import createHmac from "create-hmac"; export function Mechanism() {} @@ -7,17 +6,35 @@ Mechanism.prototype.Mechanism = Mechanism; Mechanism.prototype.name = "HT-SHA-256-NONE"; Mechanism.prototype.clientFirst = true; -Mechanism.prototype.response = function response(cred) { +Mechanism.prototype.response = async function response(cred) { this.password = cred.password; - const hmac = createHmac("sha256", this.password); - hmac.update("Initiator"); - return cred.username + "\0" + hmac.digest("latin1"); + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const hmac = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(this.password), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"] + ); + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const digest = await crypto.subtle.sign("HMAC", hmac, new TextEncoder().encode("Initiator")); + const digestS = String.fromCharCode.apply(null, new Uint8Array(digest)); + return cred.username + "\0" + digestS; }; -Mechanism.prototype.final = function final(data) { - const hmac = createHmac("sha256", this.password); - hmac.update("Responder"); - if (hmac.digest("latin1") !== data) { +Mechanism.prototype.final = async function final(data) { + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const hmac = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(this.password), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"] + ); + // eslint-disable-next-line n/no-unsupported-features/node-builtins + const digest = await crypto.subtle.sign("HMAC", hmac, new TextEncoder().encode("Responder")); + const digestS = String.fromCharCode.apply(null, new Uint8Array(digest)); + if (digestS !== data) { throw new Error("Responder message from server was wrong"); } }; diff --git a/packages/sasl-ht-sha-256-none/package.json b/packages/sasl-ht-sha-256-none/package.json index 84b07016..6af011c2 100644 --- a/packages/sasl-ht-sha-256-none/package.json +++ b/packages/sasl-ht-sha-256-none/package.json @@ -12,9 +12,6 @@ "XMPP", "sasl" ], - "dependencies": { - "create-hmac": "^1.1.7" - }, "engines": { "node": ">= 20" }, diff --git a/packages/sasl/index.js b/packages/sasl/index.js index 2ae4ae4e..7c47312a 100644 --- a/packages/sasl/index.js +++ b/packages/sasl/index.js @@ -39,14 +39,14 @@ async function authenticate({ saslFactory, entity, mechanism, credentials }) { xml( "auth", { xmlns: NS, mechanism: mech.name }, - encode(mech.response(creds)), + encode(await mech.response(creds)), ), async (element, done) => { if (element.getNS() !== NS) return; if (element.name === "challenge") { - mech.challenge(decode(element.text())); - const resp = mech.response(creds); + await mech.challenge(decode(element.text())); + const resp = await mech.response(creds); await entity.send( xml( "response", diff --git a/packages/sasl2/index.js b/packages/sasl2/index.js index a00d83a2..d81ddfed 100644 --- a/packages/sasl2/index.js +++ b/packages/sasl2/index.js @@ -38,7 +38,7 @@ async function authenticate({ entity, xml("authenticate", { xmlns: NS, mechanism: mech.name }, [ mech.clientFirst && - xml("initial-response", {}, encode(mech.response(creds))), + xml("initial-response", {}, encode(await mech.response(creds))), userAgent, ...streamFeatures, ]), @@ -46,8 +46,8 @@ async function authenticate({ if (element.getNS() !== NS) return; if (element.name === "challenge") { - mech.challenge(decode(element.text())); - const resp = mech.response(creds); + await mech.challenge(decode(element.text())); + const resp = await mech.response(creds); await entity.send( xml( "response", @@ -69,7 +69,7 @@ async function authenticate({ if (element.name === "success") { const additionalData = element.getChild("additional-data")?.text(); if (additionalData && mech.final) { - mech.final(decode(additionalData)); + await mech.final(decode(additionalData)); } // https://xmpp.org/extensions/xep-0388.html#success @@ -99,12 +99,13 @@ export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) { NS, async ({ entity }, _next, element) => { const mechanisms = getAvailableMechanisms(element, NS, saslFactory); - if (mechanisms.length === 0) { + const streamFeatures = await getStreamFeatures({ element, features }); + const fast_available = !!fast?.mechanism; + + if (mechanisms.length === 0 && !fast_available) { throw new SASLError("SASL: No compatible mechanism available."); } - const streamFeatures = await getStreamFeatures({ element, features }); - const fast_available = !!fast?.mechanism; await onAuthenticate(done, mechanisms, fast_available && fast); async function done(credentials, mechanism, userAgent) { diff --git a/packages/sasl2/test.js b/packages/sasl2/test.js index 547de79c..e311e264 100644 --- a/packages/sasl2/test.js +++ b/packages/sasl2/test.js @@ -102,6 +102,49 @@ test("with function credentials", async () => { expect(entity.jid.toString()).toBe(jid); }); +test("with FAST token", async () => { + const mech = "HT-SHA-256-NONE"; + function onAuthenticate(authenticate, mechanisms, fast) { + expect(mechanisms).toEqual([]); + expect(fast.mechanism).toEqual(mech); + return authenticate({ token: { token: "hai", mechanism: fast.mechanism } }, null, userAgent); + } + + const { entity } = mockClient({ credentials: onAuthenticate }); + + entity.mockInput( + + + + + {mech} + + + + , + ); + + expect(await promise(entity, "send")).toEqual( + + bnVsbACNMNimsTBnxS04m8x7wgKjBHdDUL/nXPU4J4vqxqjBIg== + {userAgent} + + , + ); + + const jid = "username@localhost/example~Ln8YSSzsyK-b_3-vIFvOJNnE"; + + expect(entity.jid?.toString()).not.toBe(jid); + + entity.mockInput( + + {jid} + , + ); + + expect(entity.jid.toString()).toBe(jid); +}); + test("failure", async () => { const { entity } = mockClient({ credentials, userAgent });