appcenter-cli
Version:
Command line tool for Visual Studio App Center
176 lines (175 loc) • 6.98 kB
JavaScript
;
//
// Implementation of token store that reads and writes to the Windows credential store.
// Uses included "creds.exe" program to access the credential store.
//
Object.defineProperty(exports, "__esModule", { value: true });
exports.createWinTokenStore = exports.WinTokenStore = void 0;
const childProcess = require("child_process");
const rxjs_1 = require("rxjs");
const split = require("split2");
const through = require("through2");
const path = require("path");
const parser = require("./win-credstore-parser");
const debug = require("debug")("appcenter-cli:util:token-store:win32:win-token-store");
const util_1 = require("util");
const credExePath = path.join(__dirname, "../../../../bin/windows/creds.exe");
const targetNamePrefix = "AppCenterCli:target=";
const oldTargetNamePrefix = "MobileCenterCli:target=";
class Prefixer {
constructor(useOldName) {
this.prefix = useOldName ? oldTargetNamePrefix : targetNamePrefix;
}
ensurePrefix(targetName) {
if (targetName.slice(this.prefix.length) !== this.prefix) {
targetName = this.prefix + targetName;
}
return targetName;
}
removePrefix(targetName) {
return targetName.slice(this.prefix.length);
}
removePrefixFromCred(cred) {
if (cred.targetName) {
cred.targetName = this.removePrefix(cred.targetName);
}
return cred;
}
}
function encodeTokenValueAsHex(token) {
const tokenValueAsString = JSON.stringify(token);
return Buffer.from(tokenValueAsString, "utf8").toString("hex");
}
function decodeTokenValueFromHex(token) {
return JSON.parse(Buffer.from(token, "hex").toString("utf8"));
}
function credToTokenEntry(cred) {
// Assumes credential comes in with prefixes on target skipped, and
// Credential object in hexidecimal
debug(`Converting credential ${util_1.inspect(cred)} to TokenEntry`);
return {
key: cred.targetName,
accessToken: decodeTokenValueFromHex(cred.credential),
};
}
class WinTokenStore {
/**
* list the contents of the credential store, parsing each value.
*
* We ignore everything that wasn't put there by us, we look
* for target names starting with the target name prefix.
*
*
* @return {Observable<TokenEntry>} stream of credentials.
*/
list() {
const prefixer = new Prefixer(false);
return rxjs_1.Observable.create((observer) => {
const credsProcess = childProcess.spawn(credExePath, ["-s", "-g", "-t", `${targetNamePrefix}*`]);
debug("Creds process started for list, monitoring output");
const credStream = credsProcess.stdout.pipe(parser.createParsingStream()).pipe(through.obj(function (chunk, enc, done) {
done(null, prefixer.removePrefixFromCred(chunk));
}));
credStream.on("data", (cred) => {
debug(`Got data from creds: ${cred}`);
observer.next(credToTokenEntry(cred));
});
credStream.on("end", () => {
debug(`output list completed`);
observer.complete();
});
credStream.on("error", (err) => observer.error(err));
});
}
/**
* Get details for a specific credential. Assumes generic credential.
*
* @param {tokenKeyType} key target name for credential
* @return {Promise<TokenEntry>} Returned credential or null if not found.
*/
get(key, useOldName = false) {
const prefixer = new Prefixer(useOldName);
const args = ["-s", "-t", prefixer.ensurePrefix(key)];
const credsProcess = childProcess.spawn(credExePath, args);
let result = null;
const errors = [];
debug(`Getting key with args ${util_1.inspect(args)}`);
return new Promise((resolve, reject) => {
credsProcess.stdout
.pipe(parser.createParsingStream())
.pipe(through.obj(function (chunk, enc, done) {
done(null, prefixer.removePrefixFromCred(chunk));
}))
.on("data", (credential) => {
result = credential;
result.targetName = prefixer.removePrefix(result.targetName);
});
credsProcess.stderr.pipe(split()).on("data", (line) => {
errors.push(line);
});
credsProcess.on("exit", (code) => {
if (code === 0) {
debug(`Completed getting token, result = ${util_1.inspect(result)}`);
return resolve(credToTokenEntry(result));
}
return reject(new Error(`Getting credential failed, exit code ${code}: ${errors.join(", ")}`));
});
});
}
/**
* Set the credential for a given key in the credential store.
* Creates or updates, assumes generic credential.
*
* @param {TokenKeyType} key key for entry (string user name for now)
* @param {TokenValueType} credential the credential to be encrypted
*
* @return {Promise<void>} Promise that completes when update has finished
* @param {Function(err)} callback completion callback
*/
set(key, credential) {
const prefixer = new Prefixer(false);
const args = ["-a", "-t", prefixer.ensurePrefix(key), "-p", encodeTokenValueAsHex(credential)];
debug(`Saving token with args ${util_1.inspect(args)}`);
return new Promise((resolve, reject) => {
childProcess.execFile(credExePath, args, function (err) {
if (err) {
debug(`Token store failed, ${util_1.inspect(err)}`);
return reject(err);
}
debug(`Token successfully stored`);
return resolve();
});
});
}
/**
* Remove the given key from the credential store.
*
* @param {TokenKeyType} key target name to remove.
* if ends with "*" character,
* will delete all targets
* starting with that prefix
* @param {Function(err)} callback completion callback
*/
remove(key) {
const prefixer = new Prefixer(false);
const args = ["-d", "-t", prefixer.ensurePrefix(key)];
if (key.slice(-1) === "*") {
args.push("-g");
}
debug(`Deleting token with args ${util_1.inspect(args)}`);
return new Promise((resolve, reject) => {
childProcess.execFile(credExePath, args, function (err) {
if (err) {
return reject(err);
}
resolve();
});
});
}
}
exports.WinTokenStore = WinTokenStore;
function createWinTokenStore() {
debug(`Creating WinTokenStore`);
return new WinTokenStore();
}
exports.createWinTokenStore = createWinTokenStore;