Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for XEP-0484 (FAST) #1042

Merged
merged 13 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ e2e:
cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2 > /dev/null
cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_bind2 > /dev/null
cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_sm > /dev/null
# https://github.com/xmppjs/xmpp.js/pull/1006
# cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_fast > /dev/null
cd server && prosodyctl --config prosody.cfg.lua install mod_sasl2_fast > /dev/null
npm run e2e

clean:
Expand Down
1,018 changes: 279 additions & 739 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion packages/client-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"dependencies": {
"@xmpp/connection": "^0.14.0",
"@xmpp/jid": "^0.14.0",
"@xmpp/xml": "^0.14.0"
"@xmpp/sasl": "^0.14.0",
"@xmpp/xml": "^0.14.0",
"saslmechanisms": "^0.1.1"
},
"engines": {
"node": ">= 20"
Expand Down
36 changes: 36 additions & 0 deletions packages/client-core/src/fast/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# fast

fast for `@xmpp/client`.

Included and enabled in `@xmpp/client`.

## Usage

Resource is optional and will be chosen by the server if omitted.

### string

```js
import { xmpp } from "@xmpp/client";

const client = xmpp({ resource: "laptop" });
```

### function

Instead, you can provide a function that will be called every time resource binding occurs (every (re)connect).

```js
import { xmpp } from "@xmpp/client";

const client = xmpp({ resource: onBind });

async function onBind(bind) {
const resource = await fetchResource();
return resource;
}
```

## References

[XEP-0484: Fast Authentication Streamlining Tokens](https://xmpp.org/extensions/xep-0484.html)
49 changes: 49 additions & 0 deletions packages/client-core/src/fast/fast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { getAvailableMechanisms } from "@xmpp/sasl";
import xml from "@xmpp/xml";
import SASLFactory from "saslmechanisms";

const NS = "urn:xmpp:fast:0";

export default function fast({ sasl2 }) {
const saslFactory = new SASLFactory();

const fast = {
token: null,
expiry: null,
saslFactory,
mechanisms: [],
mechanism: null,
available() {
return !!(this.token && this.mechanism);
},
};

sasl2.use(
NS,
async (element) => {
if (!element.is("fast", NS)) return;
fast.mechanisms = getAvailableMechanisms(element, NS, saslFactory);
fast.mechanism = fast.mechanisms[0];

if (!fast.mechanism) return;

if (!fast.token) {
return xml("request-token", {
xmlns: NS,
mechanism: fast.mechanism,
});
}

return xml("fast", { xmlns: NS });
},
async (element) => {
if (element.is("token", NS)) {
const { token, expiry } = element.attrs;
fast.token = token;
fast.expiry = expiry;
}
},
);

return fast;
}
11 changes: 11 additions & 0 deletions packages/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import _sasl from "@xmpp/sasl";
import _resourceBinding from "@xmpp/resource-binding";
import _streamManagement from "@xmpp/stream-management";
import _bind2 from "@xmpp/client-core/src/bind2/bind2.js";
import _fast from "@xmpp/client-core/src/fast/fast.js";

import SASLFactory from "saslmechanisms";
import scramsha1 from "@xmpp/sasl-scram-sha-1";
import plain from "@xmpp/sasl-plain";
import anonymous from "@xmpp/sasl-anonymous";
import htsha256none from "@xmpp/sasl-ht-sha-256-none";

function client(options = {}) {
const { resource, credentials, username, password, userAgent, ...params } =
Expand Down Expand Up @@ -63,9 +65,17 @@ function client(options = {}) {
createOnAuthenticate(credentials ?? { username, password }, userAgent),
);

const fast = setupIfAvailable(_fast, {
sasl2,
});
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);

// Stream features - order matters and define priority
const sasl = _sasl(
{ streamFeatures, saslFactory },
Expand Down Expand Up @@ -102,6 +112,7 @@ function client(options = {}) {
streamManagement,
mechanisms,
bind2,
fast,
});
}

Expand Down
8 changes: 6 additions & 2 deletions packages/client/lib/createOnAuthenticate.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const ANONYMOUS = "ANONYMOUS";

export default function createOnAuthenticate(credentials, userAgent) {
return async function onAuthenticate(authenticate, mechanisms) {
return async function onAuthenticate(authenticate, mechanisms, fast) {
if (typeof credentials === "function") {
await credentials(authenticate, mechanisms);
await credentials(authenticate, mechanisms, fast);
return;
}

Expand All @@ -16,6 +16,10 @@ export default function createOnAuthenticate(credentials, userAgent) {
return;
}

if (fast?.token) {
credentials.password = fast.token;
}

await authenticate(credentials, mechanisms[0], userAgent);
};
}
6 changes: 4 additions & 2 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"@xmpp/resource-binding": "^0.14.0",
"@xmpp/sasl": "^0.14.0",
"@xmpp/sasl-anonymous": "^0.14.0",
"@xmpp/sasl-ht-sha-256-none": "^0.14.0",
"@xmpp/sasl-plain": "^0.14.0",
"@xmpp/sasl-scram-sha-1": "^0.14.0",
"@xmpp/sasl2": "^0.14.0",
"@xmpp/session-establishment": "^0.14.0",
"@xmpp/starttls": "^0.14.0",
"@xmpp/stream-features": "^0.14.0",
"@xmpp/stream-management": "^0.14.0",
Expand All @@ -32,7 +32,9 @@
"@xmpp/tcp": false,
"@xmpp/tls": false,
"@xmpp/starttls": false,
"@xmpp/sasl-scram-sha-1": false
"@xmpp/sasl-scram-sha-1": false,
"@xmpp/sasl-ht-sha-256-none": false,
"@xmpp/fast": false
},
"engines": {
"node": ">= 20"
Expand Down
27 changes: 27 additions & 0 deletions packages/sasl-ht-sha-256-none/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// https://datatracker.ietf.org/doc/draft-schmaus-kitten-sasl-ht/
import createHmac from "create-hmac";

export function Mechanism() {}

Mechanism.prototype.Mechanism = Mechanism;
Mechanism.prototype.name = "HT-SHA-256-NONE";
Mechanism.prototype.clientFirst = true;

Mechanism.prototype.response = function response(cred) {
this.password = cred.password;
const hmac = createHmac("sha256", this.password);
hmac.update("Initiator");
return cred.username + "\0" + hmac.digest("latin1");
};

Mechanism.prototype.final = function final(data) {
const hmac = createHmac("sha256", this.password);
hmac.update("Responder");
if (hmac.digest("latin1") !== data) {
throw new Error("Responder message from server was wrong");
}
};

export default function saslHashedToken(sasl) {
sasl.use(Mechanism);
}
24 changes: 24 additions & 0 deletions packages/sasl-ht-sha-256-none/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@xmpp/sasl-ht-sha-256-none",
"description": "XMPP SASL HT-SHA-256-NONE for JavaScript",
"repository": "github:xmppjs/xmpp.js",
"homepage": "https://github.com/xmppjs/xmpp.js/tree/main/packages/sasl-ht-sha-256-none",
"bugs": "http://github.com/xmppjs/xmpp.js/issues",
"version": "0.14.0",
"license": "ISC",
"type": "module",
"main": "index.js",
"keywords": [
"XMPP",
"sasl"
],
"dependencies": {
"create-hmac": "^1.1.7"
},
"engines": {
"node": ">= 20"
},
"publishConfig": {
"access": "public"
}
}
17 changes: 9 additions & 8 deletions packages/sasl/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import { procedure } from "@xmpp/events";

const NS = "urn:ietf:params:xml:ns:xmpp-sasl";

function getMechanismNames(element) {
return element.getChildElements().map((el) => el.text());
export function getAvailableMechanisms(element, NS, saslFactory) {
const offered = new Set(
element.getChildren("mechanism", NS).map((m) => m.text()),
);
const supported = saslFactory._mechs.map(({ name }) => name);
return supported.filter((mech) => offered.has(mech));
}

async function authenticate({ saslFactory, entity, mechanism, credentials }) {
Expand Down Expand Up @@ -66,11 +70,8 @@ async function authenticate({ saslFactory, entity, mechanism, credentials }) {

export default function sasl({ streamFeatures, saslFactory }, onAuthenticate) {
streamFeatures.use("mechanisms", NS, async ({ entity }, _next, element) => {
const offered = getMechanismNames(element);
const supported = saslFactory._mechs.map(({ name }) => name);
const intersection = supported.filter((mech) => offered.includes(mech));

if (intersection.length === 0) {
const mechanisms = getAvailableMechanisms(element, NS, saslFactory);
if (mechanisms.length === 0) {
throw new SASLError("SASL: No compatible mechanism available.");
}

Expand All @@ -83,7 +84,7 @@ export default function sasl({ streamFeatures, saslFactory }, onAuthenticate) {
});
}

await onAuthenticate(done, intersection);
await onAuthenticate(done, mechanisms);

await entity.restart();
});
Expand Down
57 changes: 39 additions & 18 deletions packages/sasl2/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import { encode, decode } from "@xmpp/base64";
import SASLError from "@xmpp/sasl/lib/SASLError.js";
import xml from "@xmpp/xml";
import { procedure } from "@xmpp/events";
import { getAvailableMechanisms } from "@xmpp/sasl";

// https://xmpp.org/extensions/xep-0388.html

const NS = "urn:xmpp:sasl:2";

function getMechanismNames(stanza) {
return stanza.getChildren("mechanism", NS).map((m) => m.text());
}

async function authenticate({
saslFactory,
entity,
Expand Down Expand Up @@ -95,45 +92,69 @@ async function authenticate({

export default function sasl2({ streamFeatures, saslFactory }, onAuthenticate) {
const features = new Map();
let fast;

streamFeatures.use(
"authentication",
NS,
async ({ entity }, _next, element) => {
const offered = getMechanismNames(element);
const supported = saslFactory._mechs.map(({ name }) => name);
const intersection = supported.filter((mech) => offered.includes(mech));

if (intersection.length === 0) {
throw new SASLError("SASL: No compatible mechanism available.");
}

const streamFeatures = await getStreamFeatures({ element, features });

const fastStreamFeature = [...streamFeatures].find((el) =>
el?.is("fast", "urn:xmpp:fast:0"),
);
const is_fast = fastStreamFeature && fast;

async function done(credentials, mechanism, userAgent) {
await authenticate({
saslFactory,
const params = {
entity,
mechanism,
credentials,
userAgent,
streamFeatures,
features,
};

if (is_fast) {
try {
await authenticate({
saslFactory: fast.saslFactory,
mechanism: fast.mechanisms[0],
...params,
});
return;
} catch {
// If fast authentication fails, continue and try with sasl
streamFeatures.delete(fastStreamFeature);
}
}

await authenticate({
saslFactory,
mechanism,
...params,
});
}

await onAuthenticate(done, intersection);
const mechanisms = getAvailableMechanisms(element, NS, saslFactory);
if (mechanisms.length === 0) {
throw new SASLError("SASL: No compatible mechanism available.");
}

await onAuthenticate(done, mechanisms, is_fast && fast);
},
);

return {
use(ns, req, res) {
features.set(ns, [req, res]);
},
setup({ fast: _fast }) {
fast = _fast;
},
};
}

function getStreamFeatures({ element, features }) {
async function getStreamFeatures({ element, features }) {
const promises = [];

const inline = element.getChild("inline");
Expand All @@ -146,5 +167,5 @@ function getStreamFeatures({ element, features }) {
promises.push(feature[0](element));
}

return Promise.all(promises);
return new Set(await Promise.all(promises));
}
1 change: 1 addition & 0 deletions packages/xmpp.js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@xmpp/resource-binding": "^0.14.0",
"@xmpp/sasl": "^0.14.0",
"@xmpp/sasl-anonymous": "^0.14.0",
"@xmpp/sasl-ht-sha-256-none": "^0.14.0",
"@xmpp/sasl-plain": "^0.14.0",
"@xmpp/sasl-scram-sha-1": "^0.14.0",
"@xmpp/sasl2": "^0.14.0",
Expand Down
Loading
Loading