@ledgerhq/hw-transport-u2f
Version:
Ledger Hardware Wallet Web implementation of the communication layer, using U2F api
173 lines (138 loc) • 4.4 kB
JavaScript
import { sign, isSupported } from "u2f-api";
import Transport from "@ledgerhq/hw-transport";
import { log } from "@ledgerhq/logs";
import { TransportError } from "@ledgerhq/errors";
function wrapU2FTransportError(originalError, message, id) {
const err = new TransportError(message, id); // $FlowFixMe
err.originalError = originalError;
return err;
}
function wrapApdu(apdu, key) {
const result = Buffer.alloc(apdu.length);
for (let i = 0; i < apdu.length; i++) {
result[i] = apdu[i] ^ key[i % key.length];
}
return result;
} // Convert from normal to web-safe, strip trailing "="s
const webSafe64 = base64 => base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); // Convert from web-safe to normal, add trailing "="s
const normal64 = base64 => base64.replace(/-/g, "+").replace(/_/g, "/") + "==".substring(0, 3 * base64.length % 4);
function attemptExchange(apdu, timeoutMillis, scrambleKey, unwrap) {
const keyHandle = wrapApdu(apdu, scrambleKey);
const challenge = Buffer.from("0000000000000000000000000000000000000000000000000000000000000000", "hex");
const signRequest = {
version: "U2F_V2",
keyHandle: webSafe64(keyHandle.toString("base64")),
challenge: webSafe64(challenge.toString("base64")),
appId: location.origin
};
log("apdu", "=> " + apdu.toString("hex"));
return sign(signRequest, timeoutMillis / 1000).then(response => {
const {
signatureData
} = response;
if (typeof signatureData === "string") {
const data = Buffer.from(normal64(signatureData), "base64");
let result;
if (!unwrap) {
result = data;
} else {
result = data.slice(5);
}
log("apdu", "<= " + result.toString("hex"));
return result;
} else {
throw response;
}
});
}
let transportInstances = [];
function emitDisconnect() {
transportInstances.forEach(t => t.emit("disconnect"));
transportInstances = [];
}
function isTimeoutU2FError(u2fError) {
return u2fError.metaData.code === 5;
}
/**
* U2F web Transport implementation
* @example
* import TransportU2F from "@ledgerhq/hw-transport-u2f";
* ...
* TransportU2F.create().then(transport => ...)
*/
export default class TransportU2F extends Transport {
/*
*/
/*
*/
/**
* static function to create a new Transport from a connected Ledger device discoverable via U2F (browser support)
*/
static async open(_, _openTimeout = 5000) {
return new TransportU2F();
}
constructor() {
super();
this.scrambleKey = void 0;
this.unwrap = true;
transportInstances.push(this);
}
/**
* Exchange with the device using APDU protocol.
* @param apdu
* @returns a promise of apdu response
*/
async exchange(apdu) {
try {
return await attemptExchange(apdu, this.exchangeTimeout, this.scrambleKey, this.unwrap);
} catch (e) {
const isU2FError = typeof e.metaData === "object";
if (isU2FError) {
if (isTimeoutU2FError(e)) {
emitDisconnect();
} // the wrapping make error more usable and "printable" to the end user.
throw wrapU2FTransportError(e, "Failed to sign with Ledger device: U2F " + e.metaData.type, "U2F_" + e.metaData.code);
} else {
throw e;
}
}
}
/**
*/
setScrambleKey(scrambleKey) {
this.scrambleKey = Buffer.from(scrambleKey, "ascii");
}
/**
*/
setUnwrap(unwrap) {
this.unwrap = unwrap;
}
close() {
// u2f have no way to clean things up
return Promise.resolve();
}
}
TransportU2F.isSupported = isSupported;
TransportU2F.list = () => // this transport is not discoverable but we are going to guess if it is here with isSupported()
isSupported().then(supported => supported ? [null] : []);
TransportU2F.listen = observer => {
let unsubscribed = false;
isSupported().then(supported => {
if (unsubscribed) return;
if (supported) {
observer.next({
type: "add",
descriptor: null
});
observer.complete();
} else {
observer.error(new TransportError("U2F browser support is needed for Ledger. " + "Please use Chrome, Opera or Firefox with a U2F extension. " + "Also make sure you're on an HTTPS connection", "U2FNotSupported"));
}
});
return {
unsubscribe: () => {
unsubscribed = true;
}
};
};
//# sourceMappingURL=TransportU2F.js.map