UNPKG

node-u2f

Version:

Provides functionality for working with the server side aspects of the U2F protocol.

260 lines (226 loc) 9.01 kB
var crypto = require('crypto'); // Constants var U2F_VERSION = "U2F_V2"; var PUBKEY_LEN = 65; var errors = { registration: { NO_CLIENT_DATA: 101, NO_TYPE: 102, WRONG_TYPE: 103, CHALLENGE_MISMATCH: 104, NO_REGISTRATION_DATA: 105, WRONG_RESERVED_BYTE: 106, PUBLIC_KEY_ERROR: 107, SIGNATURE_INVALID: 108, }, authentication: { NO_KEY_HANDLE: 201, NO_CLIENT_DATA: 202, NO_SIGNATURE_DATA: 203, WRONG_KEY_HANDLE: 204, NO_TYPE: 205, WRONG_TYPE: 206, CHALLENGE_MISMATCH: 207, SIGNATURE_INVALID: 208, } }; exports.errors = errors; /** * Initiates the registration of a device. * * @param appId the U2F AppID. Set this to the Web Origin of the login page, unless you need to * support logging in from multiple Web Origins. * @return a StartedRegistration, which should be sent to the client and temporary saved by the * server. */ exports.startRegistration = function(appId) { var challenge = generateChallenge(); return { version: U2F_VERSION, appId: appId, challenge: base64_to_RFC4648(challenge) }; }; /** * Finishes a previously started registration. * * @param challenge The challenge sent to the client. * @param deviceResponse The response from the device/client. * @return a DeviceRegistration object, holding information about the registered device. Servers should * persist this. */ exports.finishRegistration = function(challenge, deviceResponse){ var registration = {}; // First, check the "clientData" part if (!deviceResponse.clientData) { throw errors.registration.NO_CLIENT_DATA; } // it is a base64-encoded JSON string. var rawClientData = (new Buffer(deviceResponse.clientData, 'base64')).toString(); var clientData = JSON.parse(rawClientData); if (!clientData.typ) { throw errors.registration.NO_TYPE; } if (clientData.typ !== "navigator.id.finishEnrollment") { throw errors.registration.WRONG_TYPE; } if (clientData.challenge !== challenge.challenge) { throw errors.registration.CHALLENGE_MISMATCH; } // Then, the "registrationData" part. if (!deviceResponse.registrationData) { throw errors.registration.NO_REGISTRATION_DATA; } var offset = 0 var registrationData = new Buffer(deviceResponse.registrationData, 'base64'); // Read reserved byte var reservedByte = registrationData.readInt8(offset++); if (reservedByte !== 0x05) { throw errors.registration.WRONG_RESERVED_BYTE; } // Read public key var publicKey = registrationData.slice(offset, offset + PUBKEY_LEN); var tmpKey = pubKeyToPem(publicKey); if (!tmpKey) { throw errors.registration.PUBLIC_KEY_ERROR; } registration.publicKey = publicKey.toString('base64'); offset += PUBKEY_LEN; // Read key handle var keyHandleLength = registrationData.readUInt8(offset++); var keyHandle = registrationData.slice(offset, offset+keyHandleLength); registration.keyHandle = keyHandle.toString('base64'); offset += keyHandleLength; // Read certificate // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes) var certificateLength = 4; certificateLength += (registrationData.readUInt8(offset + 2) << 8); // upper byte certificateLength += (registrationData.readUInt8(offset + 3) << 0); // lower byte var rawCertificate = registrationData.slice(offset, offset+certificateLength); var certificate = certToPem(rawCertificate); registration.certificate = certificate; offset += certificateLength; // Read signature var signature = registrationData.slice(offset); var dataToVerify = "00"; dataToVerify += hash('sha256', challenge.appId); dataToVerify += hash('sha256', rawClientData); dataToVerify += keyHandle.toString('hex'); dataToVerify += publicKey.toString('hex'); dataToVerify = new Buffer(dataToVerify, 'hex') var verifier = crypto.createVerify('sha256'); verifier.update(dataToVerify); var is_valid = verifier.verify(registration.certificate, signature); if(!is_valid) { throw errors.registration.SIGNATURE_INVALID; } return registration; }; /** * Initiates the authentication process. * * @param appId the U2F AppID. Set this to the Web Origin of the login page, unless you need to * support logging in from multiple Web Origins. * @param deviceRegistration the DeviceRegistration for which to initiate authentication. * @return a StartedAuthentication which should be sent to the client and temporary saved by * the server. */ exports.startAuthentication = function(appId, deviceRegistration){ var challenge = generateChallenge(); return { version: U2F_VERSION, appId: appId, // Apprently, the client-side library uses the RFC4648 flavour of base64 keyHandle: base64_to_RFC4648(deviceRegistration.keyHandle), challenge: base64_to_RFC4648(challenge) }; }; /** * Finishes a previously started authentication. * * @param challenge The challenge sent to the client. * @param deviceResponse The response from the device/client. * @param deviceRegistration the DeviceRegistration for which the authentication was initiated. * @param response the response from the device/client. */ exports.finishAuthentication = function(challenge, deviceResponse, deviceRegistration){ // Validation if (!deviceResponse.keyHandle) { throw errors.authentication.NO_KEY_HANDLE; } if (!deviceResponse.clientData) { throw errors.authentication.NO_CLIENT_DATA; } if (!deviceResponse.signatureData) { throw errors.authentication.NO_SIGNATURE_DATA; } if (deviceResponse.keyHandle !== challenge.keyHandle) { throw errors.authentication.WRONG_KEY_HANDLE; } var rawClientData = (new Buffer(deviceResponse.clientData, 'base64')).toString(); var clientData = JSON.parse(rawClientData); if (!clientData.typ) { throw errors.authentication.NO_TYPE; } if (clientData.typ !== "navigator.id.getAssertion") { throw errors.authentication.WRONG_TYPE; } if (clientData.challenge !== challenge.challenge) { throw errors.authentication.CHALLENGE_MISMATCH; } var signatureData = new Buffer(deviceResponse.signatureData, 'base64'); var dataToVerify = ""; dataToVerify += hash('sha256', challenge.appId); dataToVerify += signatureData.slice(0, 5).toString('hex'); dataToVerify += hash('sha256', rawClientData); dataToVerify = new Buffer(dataToVerify, 'hex'); var signature = signatureData.slice(5); var pubKey = new Buffer(deviceRegistration.publicKey, 'base64'); // Transform the public key to PEM format, because that's what crypto wants. var pemPubKey = pubKeyToPem(pubKey); var verifier = crypto.createVerify('sha256'); verifier.update(dataToVerify); var is_valid = verifier.verify(pubKeyToPem(pubKey), signature); if(!is_valid) { throw errors.authentication.SIGNATURE_INVALID; } // Get counter and user presence info var userPresence = signatureData.readUInt8(0); // should equal 1 var counter = signatureData.readUInt32BE(1); // should always be incrementing, and wrap around return { userPresence: userPresence, counter: counter }; }; /**************************************************/ /* UTILITY FUNCTIONS */ /**************************************************/ var base64_to_RFC4648 = function(input) { // RFC 4648 uses '-' instead of '+', and '_' instead of '/'. return input.replace(/\+/g, '-').replace(/\//g, '_'); } var pubKeyToDER = function(key) { if(key.length !== 65 || key[0] !== 0x04) { console.error("Key NOT OK!"); console.error(key.length); console.error(key[0]); return; } /* * Convert the public key to binary DER format * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480 * * SEQUENCE(2 elem) 30 59 * SEQUENCE(2 elem) 30 13 * OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01 * OID1.2.840.10045.3.1.7 (secp256r1) 06 08 2a 86 48 ce 3d 03 01 07 * BIT STRING(520 bit) 03 42 00 ..key.. */ var der = "" der += "3059"; der += "3013"; der += "06072a8648ce3d0201"; der += "06082a8648ce3d030107"; der += "034200" + key.toString("hex"); return der; } var pubKeyToPem = function(key) { var der = pubKeyToDER(key); var der_buf = new Buffer(der, 'hex'); var der_64 = der_buf.toString('base64'); var pem = ""; pem = "-----BEGIN PUBLIC KEY-----\n"; while(der_64.length) { pem += der_64.slice(0, 64) + "\n"; der_64 = der_64.slice(64); } pem += "-----END PUBLIC KEY-----"; return pem; }; var certToPem = function(cert_buf) { var cert_64 = cert_buf.toString('base64'); var pem = ""; pem += "-----BEGIN CERTIFICATE-----\n"; while(cert_64.length) { pem += cert_64.slice(0, 64) + "\n"; cert_64 = cert_64.slice(64); } pem += "-----END CERTIFICATE-----"; return pem; }; var hash = function(algo, data){ var hasher = crypto.createHash(algo); hasher.update(data); return hasher.digest('hex'); }; var generateChallenge = function() { return crypto.randomBytes(32).toString('base64'); };