1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5/*
6 * This implements SASL for IRC.
7 *   https://raw.github.com/atheme/atheme/master/doc/SASL
8 *   https://ircv3.net/specs/extensions/sasl-3.2
9 */
10
11const EXPORTED_SYMBOLS = ["ircSASL", "capSASL"];
12
13const { ircHandlers } = ChromeUtils.import(
14  "resource:///modules/ircHandlers.jsm"
15);
16
17var ircSASL = {
18  name: "SASL AUTHENTICATE",
19  priority: ircHandlers.DEFAULT_PRIORITY,
20  isEnabled() {
21    return this._activeCAPs.has("sasl");
22  },
23
24  commands: {
25    AUTHENTICATE(aMessage) {
26      // Expect an empty response, if something different is received abort.
27      if (aMessage.params[0] != "+") {
28        this.sendMessage("AUTHENTICATE", "*");
29        this.WARN(
30          "Aborting SASL authentication, unexpected message " +
31            "received:\n" +
32            aMessage.rawMessage
33        );
34        return true;
35      }
36
37      // An authentication identity, authorization identity and password are
38      // used, separated by null.
39      let data = [
40        this._requestedNickname,
41        this._requestedNickname,
42        this.imAccount.password,
43      ].join("\0");
44      // btoa for Unicode, see https://developer.mozilla.org/en-US/docs/DOM/window.btoa
45      let base64Data = btoa(unescape(encodeURIComponent(data)));
46      this.sendMessage(
47        "AUTHENTICATE",
48        base64Data,
49        "AUTHENTICATE <base64 encoded nick, user and password not logged>"
50      );
51      return true;
52    },
53
54    "900": function(aMessage) {
55      // RPL_LOGGEDIN
56      // <nick>!<ident>@<host> <account> :You are now logged in as <user>
57      // Now logged in ("whether by SASL or otherwise").
58      this.isAuthenticated = true;
59      return true;
60    },
61
62    "901": function(aMessage) {
63      // RPL_LOGGEDOUT
64      // The user's account name is unset (whether by SASL or otherwise).
65      this.isAuthenticated = false;
66      return true;
67    },
68
69    "902": function(aMessage) {
70      // ERR_NICKLOCKED
71      // Authentication failed because the account is currently locked out,
72      // held, or otherwise administratively made unavailable.
73      this.WARN(
74        "You must use a nick assigned to you. SASL authentication failed."
75      );
76      this.removeCAP("sasl");
77      return true;
78    },
79
80    "903": function(aMessage) {
81      // RPL_SASLSUCCESS
82      // Authentication was successful.
83      this.isAuthenticated = true;
84      this.LOG("SASL authentication successful.");
85      // We may receive this again while already connected if the user manually
86      // identifies with Nickserv.
87      if (!this.connected) {
88        this.removeCAP("sasl");
89      }
90      return true;
91    },
92
93    "904": function(aMessage) {
94      // ERR_SASLFAIL
95      // Sent when the SASL authentication fails because of invalid credentials
96      // or other errors not explicitly mentioned by other numerics.
97      this.WARN("Authentication with SASL failed.");
98      this.removeCAP("sasl");
99      return true;
100    },
101
102    "905": function(aMessage) {
103      // ERR_SASLTOOLONG
104      // Sent when credentials are valid, but the SASL authentication fails
105      // because the client-sent `AUTHENTICATE` command was too long.
106      this.ERROR("SASL: AUTHENTICATE command was too long.");
107      this.removeCAP("sasl");
108      return true;
109    },
110
111    "906": function(aMessage) {
112      // ERR_SASLABORTED
113      // The client completed registration before SASL authentication completed,
114      // or because we sent `AUTHENTICATE` with `*` as the parameter.
115      //
116      // Freenode sends 906 in addition to 904, ignore 906 in this case.
117      if (this._requestedCAPs.has("sasl")) {
118        this.ERROR(
119          "Registration completed before SASL authentication completed."
120        );
121        this.removeCAP("sasl");
122      }
123      return true;
124    },
125
126    "907": function(aMessage) {
127      // ERR_SASLALREADY
128      // Response if client attempts to AUTHENTICATE after successful
129      // authentication.
130      this.ERROR("Attempting SASL authentication twice?!");
131      this.removeCAP("sasl");
132      return true;
133    },
134
135    "908": function(aMessage) {
136      // RPL_SASLMECHS
137      // <nick> <mechanisms> :are available SASL mechanisms
138      // List of SASL mechanisms supported by the server (or network, services).
139      // The numeric contains a comma-separated list of mechanisms.
140      return false;
141    },
142  },
143};
144
145var capSASL = {
146  name: "SASL CAP",
147  priority: ircHandlers.DEFAULT_PRIORITY,
148  isEnabled: () => true,
149
150  commands: {
151    sasl(aMessage) {
152      // Return early if we are already authenticated (can happen due to cap-notify)
153      if (this.isAuthenticated) {
154        return true;
155      }
156
157      if (
158        (aMessage.cap.subcommand === "LS" ||
159          aMessage.cap.subcommand === "NEW") &&
160        this.imAccount.password
161      ) {
162        if (aMessage.cap.value) {
163          const mechanisms = aMessage.cap.value.split(",");
164          // We only support the plain authentication mechanism for now, abort if it's not available.
165          if (!mechanisms.includes("PLAIN")) {
166            return true;
167          }
168        }
169        // If it supports SASL, let the server know we're requiring SASL.
170        this.addCAP("sasl");
171        this.sendMessage("CAP", ["REQ", "sasl"]);
172      } else if (aMessage.cap.subcommand === "ACK") {
173        // The server acknowledges our choice to use SASL, send the first
174        // message.
175        this.sendMessage("AUTHENTICATE", "PLAIN");
176      } else if (aMessage.cap.subcommand === "NAK") {
177        this.removeCAP("sasl");
178      }
179
180      return true;
181    },
182  },
183};
184