summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/fakeserver/Auth.jsm
blob: 4bd240b50942de79dfd9e601c456ca85e51801c0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This file implements the authentication mechanisms
 * - AUTH LOGIN
 * - AUTH PLAIN
 * - AUTH CRAM-MD5
 * for all the server implementations, i.e. in a generic way.
 * In fact, you could use this to implement a real server in JS :-) .
 *
 * @author Ben Bucksch <ben.bucksch beonex.com>
 */

var EXPORTED_SYMBOLS = ["AuthPLAIN", "AuthLOGIN", "AuthCRAM"];

/**
 * Implements AUTH PLAIN
 *
 * @see RFC 4616
 */
var AuthPLAIN = {
  /**
   * Takes full PLAIN auth line, and decodes it.
   *
   * @param line {string}
   * @returns {Object { username : value, password : value } }
   * @throws {string}   error to return to client
   */
  decodeLine(line) {
    dump("AUTH PLAIN line -" + line + "-\n");
    line = atob(line); // base64 decode
    let aap = line.split("\u0000"); // 0-charater is delimiter
    if (aap.length != 3) {
      throw new Error("Expected three parts");
    }
    /* aap is: authorize-id, authenticate-id, password.
       Generally, authorize-id = authenticate-id = username.
       authorize-id may thus be empty and then defaults to authenticate-id. */
    var result = {};
    var authzid = aap[0];
    result.username = aap[1];
    result.password = aap[2];
    dump(
      "authorize-id: -" +
        authzid +
        "-, username: -" +
        result.username +
        "-, password: -" +
        result.password +
        "-\n"
    );
    if (authzid && authzid != result.username) {
      throw new Error(
        "Expecting a authorize-id that's either the same as authenticate-id or empty"
      );
    }
    return result;
  },

  /**
   * Create an AUTH PLAIN line, to allow a client to authenticate to a server.
   * Useful for tests.
   */
  encodeLine(username, password) {
    username = username.substring(0, 255);
    password = password.substring(0, 255);
    return btoa("\u0000" + username + "\u0000" + password); // base64 encode
  },
};

var AuthLOGIN = {
  /**
   * Takes full LOGIN auth line, and decodes it.
   * It may contain either username or password,
   * depending on state/step (first username, then pw).
   *
   * @param line {string}
   * @returns {string} username or password
   * @throws {string}   error to return to client
   */
  decodeLine(line) {
    dump("AUTH LOGIN -" + atob(line) + "-\n");
    return atob(line); // base64 decode
  },
};

/**
 * Implements AUTH CRAM-MD5
 *
 * @see RFC 2195, RFC 2104
 */
var AuthCRAM = {
  /**
   * First part of CRAM exchange is that the server sends
   * a challenge to the client. The client response depends on
   * the challenge. (This prevents replay attacks, I think.)
   * This function generates the challenge.
   *
   * You need to store it, you'll need it to check the client response.
   *
   * @param domain {string} - Your hostname or domain,
   *    e.g. "example.com", "mx.example.com" or just "localhost".
   * @returns {string} The challenge.
   *   It's already base64-encoded. Send it as-is to the client.
   */
  createChallenge(domain) {
    var timestamp = new Date().getTime(); // unixtime
    var challenge = "<" + timestamp + "@" + domain + ">";
    dump("CRAM challenge unencoded: " + challenge + "\n");
    return btoa(challenge);
  },
  /**
   * Takes full CRAM-MD5 auth line, and decodes it.
   *
   * Compare the returned |digest| to the result of
   * encodeCRAMMD5(). If they match, the |username|
   * returned here is authenticated.
   *
   * @param line {string}
   * @returns {Object { username : value, digest : value } }
   * @throws {string}   error to return to client
   */
  decodeLine(line) {
    dump("AUTH CRAM-MD5 line -" + line + "-\n");
    line = atob(line);
    dump("base64 decoded -" + line + "-\n");
    var sp = line.split(" ");
    if (sp.length != 2) {
      throw new Error("Expected one space");
    }
    var result = {};
    result.username = sp[0];
    result.digest = sp[1];
    return result;
  },
  /**
   * @param text {string} - server challenge (base64-encoded)
   * @param key {string} - user's password
   * @returns {string} digest as hex string
   */
  encodeCRAMMD5(text, key) {
    text = atob(text); // createChallenge() returns it already encoded
    dump("encodeCRAMMD5(text: -" + text + "-, key: -" + key + "-)\n");
    const kInputLen = 64;
    // const kHashLen = 16;
    const kInnerPad = 0x36; // per spec
    const kOuterPad = 0x5c;

    key = this.textToNumberArray(key);
    text = this.textToNumberArray(text);
    // Make sure key is exactly kDigestLen bytes long. Algo per spec.
    if (key.length > kInputLen) {
      // (results in kHashLen)
      key = this.md5(key);
    }
    while (key.length < kInputLen) {
      // Fill up with zeros.
      key.push(0);
    }

    // MD5((key XOR outerpad) + MD5((key XOR innerpad) + text)) , per spec
    var digest = this.md5(
      this.xor(key, kOuterPad).concat(
        this.md5(this.xor(key, kInnerPad).concat(text))
      )
    );
    return this.arrayToHexString(digest);
  },
  // Utils
  xor(binary, value) {
    var result = [];
    for (var i = 0; i < binary.length; i++) {
      result.push(binary[i] ^ value);
    }
    return result;
  },
  md5(binary) {
    var md5 = Cc["@mozilla.org/security/hash;1"].createInstance(
      Ci.nsICryptoHash
    );
    md5.init(Ci.nsICryptoHash.MD5);
    md5.update(binary, binary.length);
    return this.textToNumberArray(md5.finish(false));
  },
  textToNumberArray(text) {
    var array = [];
    for (var i = 0; i < text.length; i++) {
      // Convert string (only lower byte) to array.
      array.push(text.charCodeAt(i) & 0xff);
    }
    return array;
  },
  arrayToHexString(binary) {
    var result = "";
    for (var i = 0; i < binary.length; i++) {
      if (binary[i] > 255) {
        throw new Error("unexpected that value > 255");
      }
      let hex = binary[i].toString(16);
      if (hex.length < 2) {
        hex = "0" + hex;
      }
      result += hex;
    }
    return result;
  },
};