208 строки
6.0 KiB
JavaScript
208 строки
6.0 KiB
JavaScript
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* 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
|
|
* @return {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;
|
|
},
|
|
};
|