blob: 2203bec5fe7c04b3073ef2636fad1da2ccd5e2e7 [file] [log] [blame]
// Copyright 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview An SSH agent backend that supports private keys stored on smart
* cards, using the Google Smart Card Connector app.
*
* Note: A single API context with the Google Smart Card Connector Client
* library is retained on file scope level and shared among all instances of
* the backend.
*/
/**
* An SSH agent backend that uses the Google Smart Card Connector library to
* perform SSH authentication using private keys stored on smart cards.
*
* @param {!nassh.agent.Agent.UserIO} userIO Reference to object with terminal
* IO functions.
* @constructor
* @implements nassh.agent.Backend
*/
nassh.agent.backends.GSC = function(userIO) {
nassh.agent.Backend.apply(this, [userIO]);
/**
* Map a string representation of an identity's key blob to the reader that
* provides it.
*
* @member {Object<!string, !string>}
* @private
*/
this.keyBlobToReader_ = {};
};
nassh.agent.backends.GSC.prototype =
Object.create(nassh.agent.Backend.prototype);
nassh.agent.backends.GSC.constructor = nassh.agent.backends.GSC;
/**
* The unique ID of the backend.
*
* @readonly
* @const {!string}
*/
nassh.agent.backends.GSC.prototype.BACKEND_ID = 'gsc';
// Register the backend for use by nassh.agent.Agent.
nassh.agent.registerBackend(nassh.agent.backends.GSC);
/**
* The title of the app (used for logging purposes by the GSC library).
*
* @readonly
* @const {!string}
*/
nassh.agent.backends.GSC.CLIENT_TITLE = chrome.runtime.getManifest().name;
/**
* The ID of the official Google Smart Card Connector app.
*
* @readonly
* @const {!string}
*/
nassh.agent.backends.GSC.SERVER_APP_ID =
GoogleSmartCard.PcscLiteCommon.Constants.SERVER_OFFICIAL_APP_ID;
/**
* The PC/SC-Lite compatible API used in the current GSC context.
*
* @type {?GoogleSmartCard.PcscLiteClient.API}
*/
nassh.agent.backends.GSC.API = null;
/**
* The context for communication with the Google Smart Card Connector app.
*
* @type {?GoogleSmartCard.PcscLiteClient.Context}
*/
nassh.agent.backends.GSC.APIContext = null;
// clang-format off
/**
* Constants for the hash functions as used in the EMSA-PKCS1-v1_5 encoding and
* the SSH agent protocol.
* @see https://tools.ietf.org/html/rfc4880#section-5.2.2
* @see https://tools.ietf.org/html/draft-ietf-curdle-rsa-sha2-00#section-2
*
* @readonly
* @enum {!{name: !string, identifier: !Uint8Array,
* signaturePrefix: !Uint8Array}}
*/
nassh.agent.backends.GSC.HashAlgorithms = {
SHA1: {
name: 'SHA-1',
identifier: new Uint8Array([
0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0E, 0x03, 0x02,
0x1A, 0x05, 0x00, 0x04, 0x14,
]),
signaturePrefix: new Uint8Array(/* 'ssh-rsa' */[
0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61,
]),
},
SHA256: {
name: 'SHA-256',
identifier: new Uint8Array([
0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20,
]),
signaturePrefix: new Uint8Array(/* 'rsa-sha2-256' */[
0x72, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x32,
0x35, 0x36,
]),
},
SHA512: {
name: 'SHA-512',
identifier: new Uint8Array([
0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01,
0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40,
]),
signaturePrefix: new Uint8Array(/* 'rsa-sha2-512' */[
0x72, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, 0x35,
0x31, 0x32,
])
},
};
// clang-format on
/**
* Byte representation of the string 'ssh-rsa'.
*
* @readonly
* @const {!Uint8Array}
*/
nassh.agent.backends.GSC.BYTES_SSH_RSA =
new Uint8Array([0x73, 0x73, 0x68, 0x2d, 0x72, 0x73, 0x61]);
/**
* Initialize the Google Smart Card Connector library context on first use.
*
* @returns {!Promise<void>|!Promise<Error>} A resolving Promise if the
* initialization succeeded; a rejecting Promise otherwise.
*/
nassh.agent.backends.GSC.prototype.ping = async function() {
try {
await nassh.agent.backends.GSC.initializeAPIContext();
} catch (e) {
this.showMessage(nassh.msg(
'SMART_CARD_CONNECTOR_NOT_INSTALLED',
'https://chrome.google.com/webstore/detail/khpfeaanjngmcnplbdlpegiifgpfgdco'));
throw e;
}
};
/**
* Retrieve a list of SSH identities from a connected reader.
*
* If the backend fails to retrieve the identities from the reader, this reader
* will be skipped without interrupting the identity retrieval from other
* readers. Blocked devices will also be skipped. The backend remembers which
* key blobs were obtained from which reader.
*
* @param {!Object<!string, {reader: !string, readerKeyId: !Uint8Array}>}
* keyBlobToReader Maps SSH identities to the readers they have been
* retrieved from for later use by signRequest.
* @param {!string} reader The name of the reader to connect to.
* @returns {!Promise<!Array<!Identity>>} A Promise resolving to a list of SSH
* identities.
*/
nassh.agent.backends.GSC.prototype.requestReaderIdentities_ =
async function(keyBlobToReader, reader) {
const manager = new nassh.agent.backends.GSC.SmartCardManager();
try {
await manager.establishContext();
await manager.connect(reader);
// TODO: Loop over applets.
await manager.selectApplet(
nassh.agent.backends.GSC.SmartCardManager.CardApplets.OPENPGP);
// Exclude blocked readers.
if (await manager.fetchPINVerificationTriesRemaining() === 0) {
console.error(`GSC.requestIdentities: skipping blocked reader ${reader}`);
return [];
}
const readerKeyBlob = await manager.fetchPublicKeyBlob();
const readerKeyId = await manager.fetchAuthenticationPublicKeyId();
const readerKeyBlobStr = new TextDecoder('utf-8').decode(readerKeyBlob);
keyBlobToReader[readerKeyBlobStr] = {reader, readerKeyId};
return [{
keyBlob: readerKeyBlob,
comment: new Uint8Array([]),
}];
} catch (e) {
console.error(e);
console.error(
`GSC.requestIdentities: failed to get public key ID from ` +
`reader ${reader}, skipping`);
return [];
} finally {
await manager.disconnect();
await manager.releaseContext();
}
};
/**
* Retrieve a list of SSH identities from all connected readers and all applets.
*
* If the backend fails to retrieve the identities from a reader, this reader
* will be skipped without interrupting the identity retrieval from other
* readers. Blocked devices will also be skipped. The backend remembers which
* key blobs were obtained from which reader.
*
* @returns {!Promise<!Array<!Identity>>|!Promise<!Error>} A Promise
* resolving to a list of SSH identities; a rejecting Promise if the
* connected readers could not be listed or some other error occurred.
*/
nassh.agent.backends.GSC.prototype.requestIdentities = async function() {
// Written to this.keyBlobToReader_ in the end to prevent asynchronous
// overwrites from leaving it in an inconsistent state.
let keyBlobToReader = {};
const manager = new nassh.agent.backends.GSC.SmartCardManager();
let readers;
try {
await manager.establishContext();
readers = await manager.listReaders();
} finally {
await manager.releaseContext();
}
const identityArrays = await Promise.all(
readers.map(this.requestReaderIdentities_.bind(this, keyBlobToReader)));
this.keyBlobToReader_ = keyBlobToReader;
return [].concat(...identityArrays);
};
/**
* Request a smart card PIN from the user.
*
* This uses hterm's subcommand support to temporarily take over control of the
* terminal.
*
* @param {!string} reader The name of the reader for which the user will be
* asked to provide the PIN.
* @param {!Uint8Array} readerKeyId The ID of the key for which the user will
* be asked to provide the PIN.
* @param {!number} numTries The number of PIN attempts the user has left.
* @returns {!Promise<!string>|!Promise<void>} A promise resolving to the PIN
* entered by the user; a rejecting promise if the user cancelled the PIN
* entry.
*/
nassh.agent.backends.GSC.prototype.requestPIN =
async function(reader, readerKeyId, numTries) {
// Show 8 hex character (4 byte) fingerprint to the user.
const shortFingerprint =
nassh.agent.backends.GSC.arrayToHexString(readerKeyId.slice(-4));
return this.promptUser(
nassh.msg('REQUEST_PIN_PROMPT', [shortFingerprint, reader, numTries]));
};
/**
* Unlock a key on a connected smart card reader and request the PIN from
* the user.
*
* @param {!nassh.agent.backends.GSC.SmartCardManager} manager A
* SmartCardManager object connected to the reader on which the specified
* key resides.
* @param {!Uint8Array} keyId The fingerprint of the key to unlock.
* @returns {!Promise.<void>} A resolving promise if the key has been unlocked;
* a rejecting promise if an error occurred.
* @private
*/
nassh.agent.backends.GSC.prototype.unlockKey_ = async function(manager, keyId) {
let pin;
do {
const currentKeyId = await manager.fetchAuthenticationPublicKeyId();
if (!lib.array.compare(keyId, currentKeyId)) {
throw new Error(
`GSC.unlockKey_: key ID changed for reader ${manager.reader}`);
}
const numTries = await manager.fetchPINVerificationTriesRemaining();
try {
pin = await this.requestPIN(manager.readerShort(), keyId, numTries);
} catch (e) {
throw new Error('GSC.signRequest: authentication canceled by user');
}
} while (!await manager.verifyPIN(pin));
};
/**
* Compute a signature of a challenge on the smart card.
*
* Before the private key operation can be performed, the smart card PIN is
* requested from the user.
*
* @param {!Uint8Array} keyBlob The blob of the key which should be used to
* sign the challenge.
* @param {!Uint8Array} data The raw challenge data.
* @param {!number} flags The signature flags.
* @returns {!Promise<!Uint8Array>|!Promise<!Error>} A Promise resolving
* to the computed signature; a rejecting promise if unsupported signature
* flags are provided, there is no reader corresponding to the requested
* key, the key on the reader has changed since requestIdentities has been
* called or the user cancels the PIN entry.
*/
nassh.agent.backends.GSC.prototype.signRequest =
async function(keyBlob, data, flags) {
let hashConstants;
if (flags === 0) {
hashConstants = nassh.agent.backends.GSC.HashAlgorithms.SHA1;
} else if (flags & 0b100) {
hashConstants = nassh.agent.backends.GSC.HashAlgorithms.SHA512;
} else if (flags & 0b10) {
hashConstants = nassh.agent.backends.GSC.HashAlgorithms.SHA256;
} else {
throw new Error(
`GSC.signRequest: unsupported signature flags (` +
`${flags.toString(2)})`);
}
const keyBlobStr = new TextDecoder('utf-8').decode(keyBlob);
if (!this.keyBlobToReader_.hasOwnProperty(keyBlobStr)) {
throw new Error(`GSC.signRequest: no reader found for key "${keyBlobStr}"`);
}
const {reader, readerKeyId} = this.keyBlobToReader_[keyBlobStr];
const hash = await window.crypto.subtle.digest(hashConstants.name, data);
const dataToSign =
lib.array.concatTyped(hashConstants.identifier, new Uint8Array(hash));
const manager = new nassh.agent.backends.GSC.SmartCardManager();
try {
await manager.establishContext();
await manager.connect(reader);
// TODO: Support PIV applet.
await manager.selectApplet(
nassh.agent.backends.GSC.SmartCardManager.CardApplets.OPENPGP);
await this.unlockKey_(manager, readerKeyId);
const rawSignature = await manager.authenticate(dataToSign);
return lib.array.concatTyped(
new Uint8Array(lib.array.uint32ToArrayBigEndian(
hashConstants.signaturePrefix.length)),
hashConstants.signaturePrefix,
new Uint8Array(lib.array.uint32ToArrayBigEndian(rawSignature.length)),
rawSignature);
} finally {
await manager.disconnect();
await manager.releaseContext();
}
};
/**
* Handler for the apiContextDisposed event.
*/
nassh.agent.backends.GSC.apiContextDisposedListener = function() {
console.debug('GSC: API context disposed');
nassh.agent.backends.GSC.APIContext = null;
nassh.agent.backends.GSC.API = null;
};
/**
* Initialize the Google Smart Card Connector API context.
*
* @returns {!Promise<void>|!Promise<!Error>} A resolving Promise if the
* initialization succeeded; a rejecting Promise if the Smart Card Connector
* app is not installed or disabled.
*/
nassh.agent.backends.GSC.initializeAPIContext = async function() {
if (!nassh.agent.backends.GSC.API || !nassh.agent.backends.GSC.APIContext) {
nassh.agent.backends.GSC.APIContext =
new GoogleSmartCard.PcscLiteClient.Context(
nassh.agent.backends.GSC.CLIENT_TITLE,
nassh.agent.backends.GSC.SERVER_APP_ID);
nassh.agent.backends.GSC.API = await new Promise((resolve) => {
nassh.agent.backends.GSC.APIContext.addOnInitializedCallback((api) => {
nassh.agent.backends.GSC.APIContext.addOnDisposeCallback(
nassh.agent.backends.GSC.apiContextDisposedListener);
resolve(api);
});
nassh.agent.backends.GSC.APIContext.addOnDisposeCallback(
() => resolve(null));
nassh.agent.backends.GSC.APIContext.initialize();
});
}
if (!nassh.agent.backends.GSC.API || !nassh.agent.backends.GSC.APIContext) {
throw new Error(
'GSC.initializeAPIContext: Smart Card Connector app not ' +
'installed or disabled.');
}
};
/**
* Expand a PC/SC-Lite numerical error code into a more detailed error message.
*
* If the error is not recognized to be a PC/SC-Lite error code or is not
* a number, the original error is returned.
*
* @param {?number|?Error} error A numerical PC/SC-Lite error code or an Error
* object, which should be transformed into its textual representation.
* @param {?string} stack Information about the call stack at the time the
* error occurred.
* @returns {?Error}
*/
nassh.agent.backends.GSC.decodePcscError = async function(error, stack) {
stack = stack || '';
// Numeric error codes signify PC/SC-Lite errors.
if (Number.isInteger(error)) {
try {
const errorText =
await nassh.agent.backends.GSC.API.pcsc_stringify_error(error);
return new Error(`${errorText} (${error})\n${stack}`);
} catch (e) {
return new Error(`unknown PC/SC-Lite error (${error})\n${stack}`);
}
} else {
return error;
}
};
/**
* Convert an array of bytes into a hex string.
*
* @param {!Uint8Array} array
* @returns {!string}
*/
nassh.agent.backends.GSC.arrayToHexString = function(array) {
// Always include leading zeros.
return array.reduce(
(str, byte) => str + lib.f.zpad(byte.toString(16).toUpperCase(), 2), '');
};
/**
* A command APDU as defined in ISO/IEC 7816-4, consisting of a header and
* optional command data.
*
* @param {!number} cla The CLA byte.
* @param {!number} ins The INS byte.
* @param {!number} p1 The P1 byte.
* @param {!number} p2 The P2 byte.
* @param {!Uint8Array=} [data]
* @param {!boolean} [expectResponse=true] If true, expect a response from the
* smart card.
* @constructor
*/
nassh.agent.backends.GSC.CommandAPDU =
function(
cla, ins, p1, p2, data = new Uint8Array([]), expectResponse = true) {
/**
* The header of an APDU, consisting of the CLA, INS, P1 and P2 byte in order.
*
* @member {!Uint8Array}
* @private
*/
this.header_ = new Uint8Array([cla, ins, p1, p2]);
/**
* The data to be sent in the body of the APDU.
*
* @member {!Uint8Array}
* @private
*/
this.data_ = data;
/**
* If true, a response from the smart card will be expected.
*
* @member {!boolean}
* @private
*/
this.expectResponse_ = expectResponse;
};
/**
* Get the raw commands.
*
* In order to simplify the command logic, we always expect the maximum amount
* of bytes in the response (256 for normal length, 65536 for extended length).
*
* @param {!boolean} supportsChaining Set to true if command chaining can be
* used with the card.
* @param {!boolean} supportsExtendedLength Set to true if extended lengths
* (Lc and Le) can be used with the card.
* @returns {!Array<!Uint8Array>} The raw response.
*/
nassh.agent.backends.GSC.CommandAPDU.prototype.commands = function(
supportsChaining, supportsExtendedLength) {
const MAX_LC = 255;
const MAX_EXTENDED_LC = 65535;
if (this.data_.length === 0 && supportsExtendedLength) {
const extendedLe = this.expectResponse_ ?
new Uint8Array([0x00, 0x00, 0x00]) : new Uint8Array([]);
return [lib.array.concatTyped(this.header_, extendedLe)];
}
if (this.data_.length === 0) {
const le = this.expectResponse_ ?
new Uint8Array([0x00]) : new Uint8Array([]);
return [lib.array.concatTyped(this.header_, le)];
}
if (this.data_.length <= MAX_EXTENDED_LC && supportsExtendedLength) {
const extendedLc = new Uint8Array(
[0x00, this.data_.length >> 8, this.data_.length & 0xFF]);
const extendedLe = this.expectResponse_ ?
new Uint8Array([0x00, 0x00]) : new Uint8Array([]);
return [
lib.array.concatTyped(this.header_, extendedLc, this.data_, extendedLe),
];
}
if (this.data_.length <= MAX_LC || supportsChaining) {
let commands = [];
let remainingBytes = this.data_.length;
while (remainingBytes > MAX_LC) {
let header = new Uint8Array(this.header_);
// Set continuation bit in CLA byte.
header[0] |= 1 << 4;
const lc = new Uint8Array([MAX_LC]);
const data = this.data_.subarray(
this.data_.length - remainingBytes,
this.data_.length - remainingBytes + MAX_LC);
const le =
this.expectResponse_ ? new Uint8Array([0x00]) : new Uint8Array([]);
commands.push(lib.array.concatTyped(header, lc, data, le));
remainingBytes -= MAX_LC;
}
const lc = new Uint8Array([remainingBytes]);
const data = this.data_.subarray(this.data_.length - remainingBytes);
const le =
this.expectResponse_ ? new Uint8Array([0x00]) : new Uint8Array([]);
commands.push(lib.array.concatTyped(this.header_, lc, data, le));
return commands;
}
throw new Error(
`CommandAPDU.commands: data field too long (${this.data_.length} ` +
` > ${MAX_LC}) and no support for chaining`);
};
/**
* Human-readable descriptions of common data object tags for OpenPGP cards.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @enum {!string}
*/
nassh.agent.backends.GSC.DATA_OBJECT_TAG = {
0x5E: 'Login data',
0x5F50: 'URL to public keys',
0x65: 'Cardholder Related Data',
0x5B: 'Name',
0x5F2D: 'Language preference',
0x5F35: 'Sex',
0x6E: 'Application Related Data',
0x4F: 'Application Identifier',
0x5F52: 'Historical bytes',
0x73: 'Discretionary data objects',
0xC0: 'Extended capabilities',
0xC1: 'Algorithm attributes: signature',
0xC2: 'Algorithm attributes: decryption',
0xC3: 'Algorithm attributes: authentication',
0xC4: 'PW Status Bytes',
0xC5: 'Fingerprints',
0xC6: 'CA Fingerprints',
0xCD: 'Generation Timestamps',
0x7A: 'Security support template',
0x93: 'Digital signature counter',
0x7F49: 'Public key template',
0x81: 'Modulus',
0x82: 'Public exponent',
};
/**
* Human-readable descriptions of common data object tag classes for OpenPGP
* cards.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @enum {!string}
*/
nassh.agent.backends.GSC.DATA_OBJECT_TAG_CLASS = {
0: 'universal',
1: 'application',
2: 'context-specific',
3: 'private',
};
/**
* A TLV-encoded data object following ISO 7816-4: Annex D.
* @see http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-d.aspx
*
* @constructor
*/
nassh.agent.backends.GSC.DataObject = function() {};
/**
* Recursively parse (a range of) the byte representation of a TLV-encoded data
* object into a DataObject object.
* @see http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-d.aspx
*
* @constructs nassh.agent.backends.GSC.DataObject
* @param {!Uint8Array} bytes The raw bytes of the data object.
* @param {!number} [start=0] The position in bytes at which the parsing should
* start.
* @param {!number} [end=bytes.length] The position in bytes until which to
* parse.
* @throws Will throw if the raw data does not follow the specification for
* TLV-encoded data objects.
* @returns {[?nassh.agent.backends.GSC.DataObject, !number]} A pair of
* a DataObject object that is the result of the parsing and an index into
* the input byte array which points to the end of the part consumed so
* far.
*/
nassh.agent.backends.GSC.DataObject.fromBytesInRange = function(
bytes, start = 0, end = bytes.length) {
let pos = start;
// Skip 0x00 and 0xFF bytes before and between tags.
while (pos < end && (bytes[pos] === 0x00 || bytes[pos] === 0xFF)) {
++pos;
}
if (pos >= end) {
return [null, start];
}
const dataObject = new nassh.agent.backends.GSC.DataObject();
const tagByte = bytes[pos++];
dataObject.tagClass = tagByte >>> 6;
dataObject.tagClassDescription =
nassh.agent.backends.GSC.DATA_OBJECT_TAG_CLASS[dataObject.tagClass];
const isConstructed = !!(tagByte & (1 << 5));
dataObject.isConstructed = isConstructed;
let tagNumber = tagByte & 0b00011111;
let numTagNumberBytes = 1;
if (tagNumber === 0b00011111) {
if (!(bytes[pos] & 0b01111111)) {
throw new Error(
'DataObject.fromBytesWithStart: first byte of the tag number is 0');
}
tagNumber = 0;
do {
tagNumber = (tagNumber << 7) + (bytes[pos] & 0b01111111);
++numTagNumberBytes;
} while (bytes[pos++] & (1 << 7));
}
dataObject.tagNumber = tagNumber;
dataObject.tag = bytes.slice(pos - numTagNumberBytes, pos)
.reverse()
.reduce((acc, val) => (acc << 8) + val, 0);
dataObject.tagDescription =
nassh.agent.backends.GSC.DATA_OBJECT_TAG[dataObject.tag] ||
`<unimplemented tag: ${dataObject.tag}>`;
const lengthByte = bytes[pos++];
let valueLength = 0;
if (lengthByte <= 0x7F) {
valueLength = lengthByte;
} else {
const numLengthBytes = lengthByte & 0b01111111;
for (let i = 0; i < numLengthBytes; ++i) {
valueLength = (valueLength * 0x100) + bytes[pos++];
}
}
dataObject.valueLength = valueLength;
const valueStart = pos;
const valueEnd = pos + valueLength;
const value = bytes.slice(valueStart, valueEnd);
if (isConstructed) {
dataObject.children = [];
let child;
do {
[child, pos] = nassh.agent.backends.GSC.DataObject.fromBytesInRange(
bytes, pos, valueEnd);
if (child) {
dataObject.children.push(child);
}
} while (child);
} else {
dataObject.value = value;
}
return [dataObject, valueEnd];
};
/**
* Parse the byte representation of one or multiple TLV-encoded data objects
* into a DataObject.
*
* Some smart cards return constructed data objects with their tags and
* lengths, other cards return a list of subtags. In the latter case, this
* function creates an artificial root object which contains all the subtags
* in the list as children.
* @see http://www.cardwerk.com/smartcards/smartcard_standard_ISO7816-4_annex-d.aspx
*
* @constructs nassh.agent.backends.GSC.DataObject
* @param {!Uint8Array} bytes The raw bytes of the data object.
* @throws Will throw if the raw data does not follow the specification for
* TLV-encoded data objects.
* @returns {?nassh.agent.backends.GSC.DataObject} A DataObject that is the
* result of the parsing.
*/
nassh.agent.backends.GSC.DataObject.fromBytes = function(bytes) {
let dataObjects = [];
let pos = 0;
let dataObject;
do {
[dataObject, pos] = nassh.agent.backends.GSC.DataObject.fromBytesInRange(
bytes, pos);
if (dataObject) {
dataObjects.push(dataObject);
}
} while(dataObject);
if (dataObjects.length === 0) {
return null;
}
if (dataObjects.length === 1) {
return dataObjects[0];
}
// Create an artificial root object under which all tags of a top-level
// tag list are subsumed. This ensures a consistent structure of replies
// to GET DATA command among different smart card brands.
const artificialRootObject = new nassh.agent.backends.GSC.DataObject();
artificialRootObject.isConstructed = true;
artificialRootObject.children = dataObjects;
return artificialRootObject;
};
/**
* Return the value of a tag that is a leaf in the data object.
*
* @param {!number} tag
* @returns {?Array<?DataObject>|?Uint8Array} The value of the requested tag if
* present; null otherwise.
*/
nassh.agent.backends.GSC.DataObject.prototype.lookup = function(tag) {
if (this.tag === tag) {
if (this.isConstructed) {
return this.children;
} else {
return this.value;
}
} else {
if (this.isConstructed) {
for (let child of this.children) {
let result = child.lookup(tag);
if (result !== null) {
return result;
}
}
}
return null;
}
};
/**
* Representation of status bytes as returned by smart card commands.
*
* @param {!Uint8Array} bytes The raw status bytes.
* @constructor
*/
nassh.agent.backends.GSC.StatusBytes = function(bytes) {
/**
* The raw status bytes.
*
* @member {!Uint8Array}
* @readonly
*/
this.bytes = bytes;
};
/**
* Calculates the 16-bit value represented by the status bytes.
*
* @returns {!number} The 16-bit value represented by the status bytes.
*/
nassh.agent.backends.GSC.StatusBytes.prototype.value = function() {
return (this.bytes[0] << 8) + this.bytes[1];
};
nassh.agent.backends.GSC.StatusBytes.prototype.toString = function() {
return `(0x${this.bytes[0].toString(16)} 0x${this.bytes[1].toString(16)})`;
};
/**
* A lifecycle and communication manager for smart cards with convenience
* functions for commands commonly used for SSH authentication.
*
* @constructor
*/
nassh.agent.backends.GSC.SmartCardManager = function() {
/**
* Whether the manager is connected to a reader.
*
* @member {!boolean}
* @private
*/
this.connected_ = false;
/**
* The current PC/SC-Lite context.
*
* @member {?number}
* @private
*/
this.context_ = null;
/**
* The name of the reader the manager is connected to.
*
* @member {?string}
* @private
*/
this.reader_ = null;
/**
* The handle of the card the manager is currently communicating with.
*
* @member {?number}
* @private
*/
this.cardHandle_ = null;
/**
* The transmission protocol currently in use by the agent.
* @member {?number}
* @private
*/
this.activeProtocol_ = null;
/**
* The smart card applet that has been selected by the manager.
*
* @member {!nassh.agent.backends.GSC.SmartCardManager.CardApplets}
* @private
*/
this.appletSelected_ =
nassh.agent.backends.GSC.SmartCardManager.CardApplets.NONE;
/**
* True if the card is known to support command chaining.
*
* @member {!boolean}
* @private
*/
this.supportsChaining_ = false;
/**
* True if the card is known to support extended lengths (Lc and Le).
* @member {!boolean}
* @private
*/
this.supportsExtendedLength_ = false;
};
/**
* Smart card applets used for SSH authentication.
*
* @enum {!number}
*/
nassh.agent.backends.GSC.SmartCardManager.CardApplets = {
NONE: 0,
OPENPGP: 1,
PIV: 2,
};
/**
* A list of the most descriptive parts of the names for popular smart card
* readers and hardware tokens.
*
* If a reader name contains a string from the list, it is replaced by this
* string. The strings are processed in order, thus substrings of other strings
* should come last.
*
* @readonly
* @const {!Array<!string>}
*/
nassh.agent.backends.GSC.SmartCardManager.READER_SHORT_NAMES = [
'Yubikey NEO-N',
'Yubikey NEO',
'Yubikey 4-N',
'Yubikey 4',
'Nitrokey Start',
'Nitrokey Pro',
'Nitrokey Storage',
'Gemalto PC Twin Reader',
'Gemalto USB Shell Token',
];
/**
* Common status values (or individual bytes) returned by smart card applets.
*
* @readonly
* @enum {!number}
*/
nassh.agent.backends.GSC.SmartCardManager.StatusValues = {
COMMAND_CORRECT: 0x9000,
COMMAND_CORRECT_MORE_DATA_1: 0x6100,
COMMAND_INCORRECT_PARAMETERS: 0x6A80,
COMMAND_WRONG_PIN: 0x6982,
COMMAND_BLOCKED_PIN: 0x6983,
};
/**
* Command APDU for the 'GET RESPONSE' command.
*
* Used to retrieve the continuation of a long response.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @const {!nassh.agent.backends.GSC.CommandAPDU}
*/
nassh.agent.backends.GSC.SmartCardManager.GET_RESPONSE_APDU =
new nassh.agent.backends.GSC.CommandAPDU(0x00, 0xC0, 0x00, 0x00);
/**
* Command APDU for the 'SELECT APPLET' command with the OpenPGP Application
* Identifier (AID) as data.
*
* Used to select the OpenPGP applet on a smart card.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @const {!nassh.agent.backends.GSC.CommandAPDU}
*/
nassh.agent.backends.GSC.SmartCardManager.SELECT_APPLET_OPENPGP_APDU =
new nassh.agent.backends.GSC.CommandAPDU(
0x00, 0xA4, 0x04, 0x00,
new Uint8Array([0xD2, 0x76, 0x00, 0x01, 0x24, 0x01]));
/**
* Command APDU for the 'GET DATA' command with the identifier of the
* 'Application Related Data' data object as data.
*
* Used to retrieve the 'Application Related Data'.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @const {!nassh.agent.backends.GSC.CommandAPDU}
*/
nassh.agent.backends.GSC.SmartCardManager.FETCH_APPLICATION_RELATED_DATA_APDU =
new nassh.agent.backends.GSC.CommandAPDU(0x00, 0xCA, 0x00, 0x6E);
/**
* Command APDU for the 'GET DATA' command with the identifier of the
* 'Historical Bytes' data object as data.
*
* Used to retrieve the 'Historical Bytes", which contain information on the
* communication capabilities of the card.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @const {!nassh.agent.backends.GSC.CommandAPDU}
*/
nassh.agent.backends.GSC.SmartCardManager.FETCH_HISTORICAL_BYTES_APDU =
new nassh.agent.backends.GSC.CommandAPDU(0x00, 0xCA, 0x5F, 0x52);
/**
* Command APDU for the 'GENERATE ASYMMETRIC KEY PAIR' command in 'reading' mode
* with the identifier of the authentication subkey as data.
*
* Used to retrieve information on the public part of the authentication subkey.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @const {!nassh.agent.backends.GSC.CommandAPDU}
*/
nassh.agent.backends.GSC.SmartCardManager.READ_AUTHENTICATION_PUBLIC_KEY_APDU =
new nassh.agent.backends.GSC.CommandAPDU(
0x00, 0x47, 0x81, 0x00, new Uint8Array([0xA4, 0x00]));
/**
* Header bytes of the command APDU for the 'VERIFY PIN' command.
*
* Used to unlock private key operations on the smart card.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @const {!Array<!number>}
*/
nassh.agent.backends.GSC.SmartCardManager.VERIFY_PIN_APDU_HEADER =
[0x00, 0x20, 0x00, 0x82];
/**
* Header bytes of the command APDU for the 'GENERAL AUTHENTICATE' command.
*
* Used to perform a signature operation using the authentication subkey on the
* smart card.
* @see https://g10code.com/docs/openpgp-card-2.0.pdf
*
* @readonly
* @const {!Array<!number>}
*/
nassh.agent.backends.GSC.SmartCardManager.INTERNAL_AUTHENTICATE_APDU_HEADER =
[0x00, 0x88, 0x00, 0x00];
/**
* Get the name of the reader the manager is connected to.
*
* @returns {?string}
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.reader = function() {
return this.reader_;
};
/**
* Get the shortened name of the reader the manager is connected to.
*
* Uses a list of name parts of popular "smart cards".
*
* @returns {?string}
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.readerShort = function() {
if (!this.reader_) {
return null;
}
for (const shortName of
nassh.agent.backends.GSC.SmartCardManager.READER_SHORT_NAMES) {
if (this.reader_.includes(shortName)) {
return shortName;
}
}
return this.reader_;
};
/**
* Wrap a GSC-internal thenable into a vanilla Promise for execution.
*
* Packs the return values of the thenable into an array if there is not just a
* single one.
*
* @param sCardPromise
* @returns {!Promise<...args>|!Promise<Error>} A promise resolving to the
* return values of the GSC thenable; a rejecting promise containing an
* Error object if an error occurred.
* @private
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.execute_ = function(
sCardPromise) {
// Retain call stack for logging purposes.
const stack = lib.f.getStack();
return sCardPromise.then(
(result) =>
new Promise(function(resolve, reject) {
result.get(
(...args) => args.length > 1 ? resolve(args) : resolve(args[0]),
reject);
})
.catch(
(e) =>
nassh.agent.backends.GSC.decodePcscError(e, stack).then(
(e) => Promise.reject(e))));
};
/**
* Establish a PC/SC-lite context if the current context is not valid.
*
* @returns {!Promise<void>}
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.establishContext =
async function() {
if (!await this.hasValidContext()) {
this.context_ =
await this.execute_(nassh.agent.backends.GSC.API.SCardEstablishContext(
GoogleSmartCard.PcscLiteClient.API.SCARD_SCOPE_SYSTEM, null, null));
}
};
/**
* Check whether the current PC/SC-lite context is valid.
*
* @returns {!Promise<!boolean>}
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.hasValidContext =
async function() {
if (!this.context_) {
return false;
}
try {
await this.execute_(
nassh.agent.backends.GSC.API.SCardIsValidContext(this.context_));
} catch (_) {
return false;
}
return true;
};
/**
* Retrieve a list of names of connected readers known to the Smart Card
* Connector app.
*
* @returns {!Promise<!Array<!string>>|!Promise<!Error>} A Promise resolving
* to a list of readers; a rejecting Promise if the context is invalid.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.listReaders =
async function() {
if (await this.hasValidContext()) {
return this.execute_(
nassh.agent.backends.GSC.API.SCardListReaders(this.context_, null));
} else {
throw new Error('SmartCardManager.listReaders: invalid context');
}
};
/**
* Connect to the reader with the given name.
*
* Requests exclusive access to the reader and uses the T1 protocol.
*
* @param {!string} reader
* @returns {!Promise<void>|!Promise<?Error>} A resolving Promise if the
* initiation of the connection was successful; a rejecting Promise if the
* context is invalid or the connection failed.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.connect =
async function(reader) {
if (!await this.hasValidContext()) {
throw new Error('SmartCardManager.connect: invalid context');
}
if (this.connected_) {
await this.disconnect();
}
if (await this.hasValidContext() && !this.connected_) {
[this.cardHandle_, this.activeProtocol_] =
await this.execute_(nassh.agent.backends.GSC.API.SCardConnect(
this.context_, reader,
GoogleSmartCard.PcscLiteClient.API.SCARD_SHARE_EXCLUSIVE,
GoogleSmartCard.PcscLiteClient.API.SCARD_PROTOCOL_T1));
this.reader_ = reader;
this.connected_ = true;
}
};
/**
* Transmit a command APDU to the card and retrieve the result.
*
* Supports command chaining and continued responses.
*
* @param {!nassh.agent.backends.GSC.CommandAPDU} commandAPDU
* @returns {!Promise<!Uint8Array>|
* !Promise<!nassh.agent.backends.GSC.StatusBytes>} A Promise resolving to
* the response; a rejecting Promise containing the status bytes if they
* signal an error.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.transmit =
async function(commandAPDU) {
if (!this.connected_) {
throw new Error('SmartCardManager.transmit: not connected');
}
let data;
for (const command of commandAPDU.commands(
this.supportsChaining_, this.supportsExtendedLength_)) {
const result =
await this.execute_(nassh.agent.backends.GSC.API.SCardTransmit(
this.cardHandle_, GoogleSmartCard.PcscLiteClient.API.SCARD_PCI_T1,
Array.from(command)));
data = await this.getData_(result);
}
return data;
};
/**
* Parse the raw result of a command APDU received from the smart card and
* handle the status bytes.
*
* Supports continued responses.
*
* @param rawResult - A result array formed using execute_ on the result
* returned asynchronously by SCardTransmit.
* @returns {!Promise<!Uint8Array>|
* !Promise<!nassh.agent.backends.GSC.StatusBytes>} A Promise resolving to
* the response; a rejecting Promise containing the status bytes if they
* signal an error.
* @private
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.getData_ =
async function(rawResult) {
const result = new Uint8Array(rawResult[1]);
let data = result.slice(0, -2);
const statusBytes =
new nassh.agent.backends.GSC.StatusBytes(result.slice(-2));
if ((statusBytes.value() & 0xFF00) ===
nassh.agent.backends.GSC.SmartCardManager.StatusValues
.COMMAND_CORRECT_MORE_DATA_1) {
// transmit recursively calls getData_ to assemble the complete response.
const dataContinued = await this.transmit(
nassh.agent.backends.GSC.SmartCardManager.GET_RESPONSE_APDU);
data = lib.array.concatTyped(data, dataContinued);
} else if (
statusBytes.value() !==
nassh.agent.backends.GSC.SmartCardManager.StatusValues.COMMAND_CORRECT) {
console.error(
'SmartCardManager.getData_: operation returned specific status bytes ' +
statusBytes);
throw statusBytes;
}
return data;
};
/**
* Select a specific applet on the smart card.
*
* @param {!nassh.agent.backends.GSC.SmartCardManager.CardApplets} applet
* @returns {!Promise<void>|!Promise<!Error>} A Promise resolving to the key
* blob; a rejecting Promise if the manager is not connected to a smart
* card, an applet has already been selected or the selected applet is not
* supported.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.selectApplet =
async function(applet) {
if (!this.connected_) {
throw new Error('SmartCardManager.selectApplet: not connected');
}
if (this.appletSelected_ !==
nassh.agent.backends.GSC.SmartCardManager.CardApplets.NONE)
throw new Error('SmartCardManager.selectApplet: applet already selected');
switch (applet) {
case nassh.agent.backends.GSC.SmartCardManager.CardApplets.OPENPGP:
await this.transmit(
nassh.agent.backends.GSC.SmartCardManager.SELECT_APPLET_OPENPGP_APDU);
await this.determineOpenPGPCardCapabilities();
break;
default:
throw new Error(
`SmartCardManager.selectApplet: applet ID ${applet} not supported`);
}
if (this.appletSelected_ !==
nassh.agent.backends.GSC.SmartCardManager.CardApplets.NONE)
throw new Error(
'SmartCardManager.selectApplet: applet already selected (race)');
this.appletSelected_ = applet;
};
/**
* Encode an unsigned integer as an mpint.
* @see https://tools.ietf.org/html/rfc4251#section-5
*
* @param {!Uint8Array} bytes Raw bytes of an unsigned integer.
* @returns {!Uint8Array} Wire encoding of an mpint
*/
nassh.agent.backends.GSC.SmartCardManager.encodeUnsignedMpint =
function(bytes) {
let mpint = new Uint8Array(bytes);
let pos = 0;
// Strip leading zeros.
while (pos < mpint.length && !mpint[pos]) {
++pos;
}
mpint = mpint.slice(pos);
// Add a leading zero if the positive result would otherwise be treated as a
// signed mpint.
if (mpint.length && (mpint[0] & (1 << 7))) {
mpint = lib.array.concatTyped(new Uint8Array([0]), mpint);
}
return mpint;
};
/**
* Fetch the public key blob of the authentication subkey on the smart card.
*
* For OpenPGP, see RFC 4253, Section 6.6 and RFC 4251, Section 5.
*
* @returns {!Promise<!Uint8Array>|!Promise<!Error>} A Promise resolving to
* the key blob; a rejecting Promise if the selected applet is not
* supported.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.fetchPublicKeyBlob =
async function() {
switch (this.appletSelected_) {
case nassh.agent.backends.GSC.SmartCardManager.CardApplets.OPENPGP:
const publicKeyTemplate = nassh.agent.backends.GSC.DataObject.fromBytes(
await this.transmit(nassh.agent.backends.GSC.SmartCardManager
.READ_AUTHENTICATION_PUBLIC_KEY_APDU));
let modulus =
nassh.agent.backends.GSC.SmartCardManager.encodeUnsignedMpint(
publicKeyTemplate.lookup(0x81));
const exponent =
nassh.agent.backends.GSC.SmartCardManager.encodeUnsignedMpint(
publicKeyTemplate.lookup(0x82));
return lib.array.concatTyped(
new Uint8Array(lib.array.uint32ToArrayBigEndian(7)),
// 'ssh-rsa'
nassh.agent.backends.GSC.BYTES_SSH_RSA,
new Uint8Array(lib.array.uint32ToArrayBigEndian(exponent.length)),
exponent,
new Uint8Array(lib.array.uint32ToArrayBigEndian(modulus.length)),
modulus,
);
default:
throw new Error(
'SmartCardManager.fetchPublicKeyBlob: no or unsupported applet ' +
'selected');
}
};
/**
* Fetch the fingerprint of the public key the authentication subkey on the
* smart card.
*
* @returns {!Promise<!Uint8Array>|!Promise<!Error>} A Promise resolving
* to the fingerprint; a rejecting Promise if the selected applet is not
* supported.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype
.fetchAuthenticationPublicKeyId =
async function() {
switch (this.appletSelected_) {
case nassh.agent.backends.GSC.SmartCardManager.CardApplets.OPENPGP:
const appRelatedData = nassh.agent.backends.GSC.DataObject.fromBytes(
await this.transmit(nassh.agent.backends.GSC.SmartCardManager
.FETCH_APPLICATION_RELATED_DATA_APDU));
return appRelatedData.lookup(0xC5).subarray(40, 60);
default:
throw new Error(
'SmartCardManager.fetchAuthenticationPublicKeyId: no or ' +
'unsupported applet selected');
}
};
/**
* Fetch the number of PIN verification attempts that remain.
*
* @returns {!Promise<!number>|!Promise<!Error>} A Promise resolving to the
* number of PIN verification attempts; a rejecting Promise if the selected
* applet is not supported.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype
.fetchPINVerificationTriesRemaining =
async function() {
switch (this.appletSelected_) {
case nassh.agent.backends.GSC.SmartCardManager.CardApplets.OPENPGP:
const appRelatedData = nassh.agent.backends.GSC.DataObject.fromBytes(
await this.transmit(nassh.agent.backends.GSC.SmartCardManager
.FETCH_APPLICATION_RELATED_DATA_APDU));
return appRelatedData.lookup(0xC4)[4];
default:
throw new Error(
'SmartCardManager.fetchPINVerificationTriesRemaining: no or ' +
'unsupported applet selected');
}
};
/**
* Determine the card capabilities of an OpenPGP card. This includes support for
* command chaining and extended lengths.
*
* @returns {!Promise.<void>}
*/
nassh.agent.backends.GSC.SmartCardManager.prototype
.determineOpenPGPCardCapabilities =
async function() {
const historicalBytes = await this.transmit(
nassh.agent.backends.GSC.SmartCardManager.FETCH_HISTORICAL_BYTES_APDU);
// Parse data objects in COMPACT-TLV.
// First byte is assumed to be 0x00, last three bytes are status bytes.
const compactTLVData = historicalBytes.slice(1, -3);
let pos = 0;
let capabilitiesBytes = null;
while (pos < compactTLVData.length) {
const tag = compactTLVData[pos];
if (tag === 0x73) {
capabilitiesBytes = compactTLVData.slice(pos + 1, pos + 4);
break;
} else {
// The length of the tag is encoded in the second nibble.
pos += 1 + (tag & 0x0F);
}
}
if (capabilitiesBytes) {
this.supportsChaining_ = capabilitiesBytes[2] & (1 << 7);
this.supportsExtendedLength_ = capabilitiesBytes[2] & (1 << 6);
} else {
console.error(
'SmartCardManager.determineOpenPGPCardCapabilities: ' +
'capabilities tag not found');
}
};
/**
* Verify the smart card PIN to unlock private key operations.
*
* @param {!string} pin A UTF-8 string.
* @returns {!Promise<!boolean>|!Promise<!Error>} A Promise resolving to true
* if the supplied PIN was correct; a Promise resolving to false if the
* supplied PIN was incorrect; a rejecting Promise if the device is
* blocked or unrecognized status bytes were returned or the selected applet
* is not supported.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.verifyPIN =
async function(pin) {
const pinBytes = new TextEncoder('utf-8').encode(pin);
switch (this.appletSelected_) {
case nassh.agent.backends.GSC.SmartCardManager.CardApplets.OPENPGP:
try {
await this.transmit(new nassh.agent.backends.GSC.CommandAPDU(
...nassh.agent.backends.GSC.SmartCardManager.VERIFY_PIN_APDU_HEADER,
pinBytes,
false /* expectResponse */));
return true;
} catch (error) {
if (error instanceof nassh.agent.backends.GSC.StatusBytes) {
switch (error.value()) {
case nassh.agent.backends.GSC.SmartCardManager.StatusValues
.COMMAND_INCORRECT_PARAMETERS:
// This happens if the PIN entered by the user is too short,
// e.g. less than six characters long for OpenPGP.
case nassh.agent.backends.GSC.SmartCardManager.StatusValues
.COMMAND_WRONG_PIN:
return false;
case nassh.agent.backends.GSC.SmartCardManager.StatusValues
.COMMAND_BLOCKED_PIN:
throw new Error('SmartCardManager.verifyPIN: device is blocked');
default:
throw new Error(
`SmartCardManager.verifyPIN: failed (${error.toString()})`);
}
} else {
throw error;
}
}
default:
throw new Error(
'SmartCardManager.verifyPIN: no or unsupported applet selected');
}
};
/**
* Sign a challenge with the authentication subkey.
*
* Has to be used after a successful verifyPIN command.
*
* @param {!Uint8Array} data The raw challenge to be signed.
* @returns {!Promise<!Uint8Array>|!Promise<!Error>} A Promise resolving to
* the computed signature; a rejecting Promise if the selected applet is not
* supported.
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.authenticate =
async function(data) {
switch (this.appletSelected_) {
case nassh.agent.backends.GSC.SmartCardManager.CardApplets.OPENPGP:
return this.transmit(new nassh.agent.backends.GSC.CommandAPDU(
...nassh.agent.backends.GSC.SmartCardManager
.INTERNAL_AUTHENTICATE_APDU_HEADER,
data));
default:
throw new Error(
'SmartCardManager.authenticate: no or unsupported applet selected');
}
};
/**
* Disconnect from the currently connected reader.
*
* @returns {!Promise<void>}
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.disconnect =
async function() {
if (this.connected_) {
await this.execute_(nassh.agent.backends.GSC.API.SCardDisconnect(
this.cardHandle_, GoogleSmartCard.PcscLiteClient.API.SCARD_LEAVE_CARD));
this.connected_ = false;
this.reader_ = null;
this.cardHandle_ = null;
this.activeProtocol_ = null;
this.appletSelected_ =
nassh.agent.backends.GSC.SmartCardManager.CardApplets.NONE;
}
};
/**
* Release the current PC/SC-Lite context.
*
* @returns {!Promise<void>}
*/
nassh.agent.backends.GSC.SmartCardManager.prototype.releaseContext =
async function() {
if (!await this.hasValidContext()) {
this.context_ = null;
return;
}
if (this.connected_) {
await this.disconnect();
}
await this.execute_(
nassh.agent.backends.GSC.API.SCardReleaseContext(this.context_));
this.context_ = null;
};