yubico-node
Version:
A NodeJS implementation of the Yubico OTP API
187 lines (186 loc) • 7.93 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
var crypto = require("crypto");
var ResponseStatus;
(function (ResponseStatus) {
ResponseStatus["OK"] = "OK";
ResponseStatus["BAD_OTP"] = "BAD_OTP";
ResponseStatus["REPLAYED_OTP"] = "REPLAYED_OTP";
ResponseStatus["BAD_SIGNATURE"] = "BAD_SIGNATURE";
ResponseStatus["MISSING_PARAMETER"] = "MISSING_PARAMETER";
ResponseStatus["NO_SUCH_CLIENT"] = "NO_SUCH_CLIENT";
ResponseStatus["OPERATION_NOT_ALLOWED"] = "OPERATION_NOT_ALLOWED";
ResponseStatus["BACKEND_ERROR"] = "BACKEND_ERROR";
ResponseStatus["NOT_ENOUGH_ANSWERS"] = "NOT_ENOUGH_ANSWERS";
ResponseStatus["REPLAYED_REQUEST"] = "REPLAYED_REQUEST";
})(ResponseStatus = exports.ResponseStatus || (exports.ResponseStatus = {}));
var Response = /** @class */ (function () {
function Response(otp, nonce, h, t, status, timestamp, sessioncounter, sessionuse, sl) {
this.otp = otp;
this.nonce = nonce;
this.h = h;
this.t = t;
this.status = status;
this.timestamp = timestamp;
this.sessioncounter = sessioncounter;
this.sessionuse = sessionuse;
this.sl = sl;
}
/**
* Creates a response from a string body
* @param body {string} Body from the Yubico server
* @returns {Response} Newly generated Response instance from the input
*/
Response.fromRawBody = function (body) {
// Grab all the key balue pairs from the response and
// split them into an array. We relace the \r\n's with \n's to
// sanitize the response. Slice off the last 2 elements as they
// are empty.
var keyValuePairs = body
.replace(/\r\n/g, "\n")
.split("\n")
.slice(0, -2);
// Reduce the array in to an object that contains the key
// value pairs from the response
var values = keyValuePairs.reduce(function (accumulated, current) {
var _a;
// Split the key and the value apart by the '='. Limit the value
// in case the value contains an '=' in the base64 padding
var _b = current.split(/=(.+)/), key = _b[0], value = _b[1];
// Add this key value pair to the object
return Object.assign(accumulated, (_a = {},
_a[key] = value,
_a));
}, {});
// Return a Response instance from the parsed values
return new Response(values.otp, values.nonce, values.h, values.t, values.status, values.timestamp, values.sessioncounter, values.sessionuse, values.sl);
};
/**
* Validate the request against the nonce, secret, and otp
* @param nonce {string} Nonce used during the request
* @param secret {string} Secret used to verify against
* @param otp {string} OTP from when the key was pressed
*/
Response.prototype.validate = function (nonce, secret, otp) {
var _this = this;
var _a;
if (this.status !== ResponseStatus.OK) {
var errorMessages = (_a = {},
_a[ResponseStatus.BAD_OTP] = "The OTP is invalid format",
_a[ResponseStatus.REPLAYED_OTP] = "The OTP has already been seen by the service.",
_a[ResponseStatus.BAD_SIGNATURE] = "The HMAC signature verification failed.",
_a[ResponseStatus.MISSING_PARAMETER] = "The request lacks a parameter.",
_a[ResponseStatus.NO_SUCH_CLIENT] = "The client id does not exist. If you just registered for one, please give it 10 minutes to propagate",
_a[ResponseStatus.OPERATION_NOT_ALLOWED] = "The client id is not allowed to verify OTPs.",
_a[ResponseStatus.BACKEND_ERROR] = "Unexpected error in our server. Please contact Yubico if you see this error.",
_a[ResponseStatus.NOT_ENOUGH_ANSWERS] = "Server could not get requested number of syncs during before timeout.",
_a[ResponseStatus.REPLAYED_REQUEST] = "Server has seen the OTP/Nonce combination before",
_a);
var errorMessage = errorMessages[this.status];
if (!errorMessage) {
throw new Error("Unknown status " + this.status);
}
throw new Error(errorMessage);
}
if (this.nonce !== nonce) {
throw new Error("Nonces do not equal");
}
// Define the keys used in the hash
var keys = ["nonce", "otp", "sessioncounter", "sessionuse", "sl", "status", "t", "timestamp"];
// Concatenate all the keys as they would be used in a HTTP request
var body = keys
.filter(function (key) { return _this[key] !== undefined; })
.sort()
.map(function (key) { return key + "=" + _this[key]; })
.join("&");
// Hash them to compare against the server's assertion
var hash = crypto
.createHmac("sha1", Buffer.from(secret, "base64"))
.update(body)
.digest("base64");
// If the hashes diverge, the response should not be trusted and we throw an error
if (hash !== this.h) {
throw new Error("Hash provided from server and client hash do not match");
}
// If the OTPs don't match, throw an error as the response was tampered with
// The hash should pick this up, but you can never be too sure
if (this.otp !== otp) {
throw new Error("OTPs do not match");
}
};
/**
* @returns {string} the one time password used in the request
*/
Response.prototype.getOneTimePassword = function () {
return this.otp;
};
/**
* @returns {Date} Timestamp of the request in UTC
*/
Response.prototype.getTimestampUTC = function () {
return new Date(this.t * 1000);
};
/**
* @returns {Date} YubiKey internal timestamp value when key was pressed
*/
Response.prototype.getTimestamp = function () {
return new Date(this.timestamp);
};
/**
* @returns {number} YubiKey internal usage counter when key was pressed
*/
Response.prototype.getSessionCounter = function () {
return parseInt(this.sessioncounter, 10);
};
/**
* @returns {number} YubiKey internal session usage counter when key was pressed
*/
Response.prototype.getSessionUse = function () {
return parseInt(this.sessionuse, 10);
};
/**
* @returns {ResponseStatus} The status of the request. This will only show OK as all others will throw
*/
Response.prototype.getStatus = function () {
return this.status;
};
/**
* @returns {string} The public ID is the first 12 bytes of the OTP, this does not change between each request and can be used to identify users
*/
Response.prototype.getPublicId = function () {
return this.otp.substr(0, 12);
};
/**
* @returns {number} The serial number of the key described as a 48 bit number
*/
Response.prototype.getSerialNumber = function () {
var publicId = this.getPublicId();
// Convert the modHex to a UIntBE as described here
// https://developers.yubico.com/yubico-c/Manuals/modhex.1.html
var modHexConversion = {
b: "1",
c: "0",
d: "2",
e: "3",
f: "4",
g: "5",
h: "6",
i: "7",
j: "8",
k: "9",
l: "A",
n: "B",
r: "C",
t: "D",
u: "E",
v: "F",
};
// Do the conversion and return the 48 bit integer
return Buffer.from(publicId
.split("")
.map(function (char) { return modHexConversion[char]; })
.join(""), "hex").readUIntBE(0, 6);
};
return Response;
}());
exports.Response = Response;