| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @fileoverview Gnubby methods related to U2F support. |
| */ |
| 'use strict'; |
| |
| // Commands and flags of the Gnubby applet |
| /** Enroll */ |
| Gnubby.U2F_ENROLL = 0x01; |
| /** Request signature */ |
| Gnubby.U2F_SIGN = 0x02; |
| /** Request protocol version */ |
| Gnubby.U2F_VERSION = 0x03; |
| |
| /** Request applet version */ |
| Gnubby.APPLET_VERSION = 0x11; // First 3 bytes are applet version. |
| |
| // APDU.P1 flags |
| /** Test of User Presence required */ |
| Gnubby.P1_TUP_REQUIRED = 0x01; |
| /** Consume a Test of User Presence */ |
| Gnubby.P1_TUP_CONSUME = 0x02; |
| /** Test signature only, no TUP. E.g. to check for existing enrollments. */ |
| Gnubby.P1_TUP_TESTONLY = 0x04; |
| /** Attest with device key */ |
| Gnubby.P1_INDIVIDUAL_KEY = 0x80; |
| |
| // Version values |
| /** V1 of the applet. */ |
| Gnubby.U2F_V1 = 'U2F_V1'; |
| /** V2 of the applet. */ |
| Gnubby.U2F_V2 = 'U2F_V2'; |
| |
| /** |
| * Google corporate appId hash |
| * @private |
| */ |
| Gnubby.GOOGLE_CORP_APP_ID_HASH_ = 'ZEZHL99u7Xvzwzcg8jZnbDbhtF6-BIXbiaPN_dJL1p8'; |
| |
| /** Perform enrollment |
| * @param {ArrayBuffer|Uint8Array} challenge Enrollment challenge |
| * @param {ArrayBuffer|Uint8Array} appIdHash Hashed application id |
| * @param {function(...)} cb Result callback |
| */ |
| Gnubby.prototype.enroll = function(challenge, appIdHash, cb) { |
| var apdu = new Uint8Array( |
| [0x00, |
| Gnubby.U2F_ENROLL, |
| Gnubby.P1_TUP_REQUIRED | Gnubby.P1_TUP_CONSUME, |
| 0x00, 0x00, 0x00, |
| challenge.length + appIdHash.length]); |
| if (B64_encode(appIdHash) == Gnubby.GOOGLE_CORP_APP_ID_HASH_) |
| apdu[2] |= Gnubby.P1_INDIVIDUAL_KEY; |
| var u8 = new Uint8Array(apdu.length + challenge.length + |
| appIdHash.length + 2); |
| for (var i = 0; i < apdu.length; ++i) u8[i] = apdu[i]; |
| for (var i = 0; i < challenge.length; ++i) u8[i + apdu.length] = |
| challenge[i]; |
| for (var i = 0; i < appIdHash.length; ++i) { |
| u8[i + apdu.length + challenge.length] = appIdHash[i]; |
| } |
| this.apduReply(u8.buffer, cb); |
| }; |
| |
| /** Request signature |
| * @param {ArrayBuffer|Uint8Array} challengeHash Hashed signature challenge |
| * @param {ArrayBuffer|Uint8Array} appIdHash Hashed application id |
| * @param {ArrayBuffer|Uint8Array} keyHandle Key handle to use |
| * @param {function(...)} cb Result callback |
| * @param {boolean=} opt_nowink Request signature without winking |
| * (e.g. during enroll) |
| */ |
| Gnubby.prototype.sign = function(challengeHash, appIdHash, keyHandle, cb, |
| opt_nowink) { |
| var self = this; |
| // The sign command's format is ever-so-slightly different between V1 and V2, |
| // so get this gnubby's version prior to sending it. |
| this.version(function(rc, opt_data) { |
| if (rc) { |
| cb(rc); |
| return; |
| } |
| var version = UTIL_BytesToString(new Uint8Array(opt_data || [])); |
| var apduDataLen = |
| challengeHash.length + appIdHash.length + keyHandle.length; |
| if (version != Gnubby.U2F_V1) { |
| // The V2 sign command includes a length byte for the key handle. |
| apduDataLen++; |
| } |
| var apdu = new Uint8Array( |
| [0x00, |
| Gnubby.U2F_SIGN, |
| Gnubby.P1_TUP_REQUIRED | Gnubby.P1_TUP_CONSUME, |
| 0x00, 0x00, 0x00, |
| apduDataLen]); |
| if (opt_nowink) { |
| // A signature request that does not want winking. |
| // These are used during enroll to figure out whether a gnubby was already |
| // enrolled. |
| // Tell applet to not actually produce a signature, even |
| // if already touched. |
| apdu[2] |= Gnubby.P1_TUP_TESTONLY; |
| } |
| var u8 = new Uint8Array(apdu.length + apduDataLen + 2); |
| for (var i = 0; i < apdu.length; ++i) u8[i] = apdu[i]; |
| for (var i = 0; i < challengeHash.length; ++i) u8[i + apdu.length] = |
| challengeHash[i]; |
| for (var i = 0; i < appIdHash.length; ++i) { |
| u8[i + apdu.length + challengeHash.length] = appIdHash[i]; |
| } |
| var keyHandleOffset = apdu.length + challengeHash.length + appIdHash.length; |
| if (version != Gnubby.U2F_V1) { |
| u8[keyHandleOffset++] = keyHandle.length; |
| } |
| for (var i = 0; i < keyHandle.length; ++i) { |
| u8[i + keyHandleOffset] = keyHandle[i]; |
| } |
| self.apduReply(u8.buffer, cb, opt_nowink); |
| }); |
| }; |
| |
| /** Request version information |
| * @param {function(...)} cb Callback |
| */ |
| Gnubby.prototype.version = function(cb) { |
| if (!cb) cb = Gnubby.defaultCallback; |
| if (this.version_) { |
| cb(-GnubbyDevice.OK, this.version_); |
| return; |
| } |
| var self = this; |
| var apdu = new Uint8Array([0x00, Gnubby.U2F_VERSION, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00]); |
| this.apduReply(apdu.buffer, function(rc, data) { |
| if (rc == 0x6d00) { |
| // Command not implemented. Pretend this is v1. |
| var v1 = new Uint8Array(UTIL_StringToBytes(Gnubby.U2F_V1)); |
| self.version_ = v1.buffer; |
| cb(-GnubbyDevice.OK, v1.buffer); |
| } else { |
| if (!rc) { |
| self.version_ = data; |
| } |
| cb(rc, data); |
| } |
| }); |
| }; |