| // 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'; |
| |
| /** Perform enrollment |
| * @param {Array<number>|ArrayBuffer|Uint8Array} challenge Enrollment challenge |
| * @param {Array<number>|ArrayBuffer|Uint8Array} appIdHash Hashed application |
| * id |
| * @param {function(...)} cb Result callback |
| * @param {boolean=} opt_individualAttestation Request the individual |
| * attestation cert rather than the batch one. |
| */ |
| Gnubby.prototype.enroll = function( |
| challenge, appIdHash, cb, opt_individualAttestation) { |
| var p1 = Gnubby.P1_TUP_REQUIRED | Gnubby.P1_TUP_CONSUME; |
| if (opt_individualAttestation) { |
| p1 |= Gnubby.P1_INDIVIDUAL_KEY; |
| } |
| var apdu = new Uint8Array([ |
| 0x00, Gnubby.U2F_ENROLL, p1, 0x00, 0x00, 0x00, |
| challenge.length + appIdHash.length |
| ]); |
| 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 {Array<number>|ArrayBuffer|Uint8Array} challengeHash Hashed |
| * signature challenge |
| * @param {Array<number>|ArrayBuffer|Uint8Array} appIdHash Hashed application |
| * id |
| * @param {Array<number>|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; |
| |
| function gotResponse(rc, data) { |
| if (!rc) { |
| self.version_ = data; |
| } |
| cb(rc, data); |
| } |
| |
| var apdu = |
| new Uint8Array([0x00, Gnubby.U2F_VERSION, 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); |
| return; |
| } |
| if (rc) { |
| // Error. Try with non-ISO 7816-4-conforming layout defined in |
| // earlier U2F drafts. |
| apdu = new Uint8Array( |
| [0x00, Gnubby.U2F_VERSION, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); |
| self.apduReply(apdu.buffer, gotResponse); |
| return; |
| } |
| // Any other response: handle as final result. |
| gotResponse(rc, data); |
| }); |
| }; |