appcenter-cli
Version:
Command line tool for Visual Studio App Center
203 lines (175 loc) • 6.76 kB
text/typescript
//
// Implementation of token store that reads and writes to the Windows credential store.
// Uses included "creds.exe" program to access the credential store.
//
import * as childProcess from "child_process";
import { Observable, Observer } from "rxjs";
import * as stream from "stream";
import * as split from "split2";
import * as through from "through2";
import * as path from "path";
import * as parser from "./win-credstore-parser";
import { TokenStore, TokenEntry, TokenKeyType, TokenValueType } from "../token-store";
const debug = require("debug")("appcenter-cli:util:token-store:win32:win-token-store");
import { inspect } from "util";
const credExePath = path.join(__dirname, "../../../../bin/windows/creds.exe");
const targetNamePrefix = "AppCenterCli:target=";
const oldTargetNamePrefix = "MobileCenterCli:target=";
class Prefixer {
private prefix: string;
constructor(useOldName: boolean) {
this.prefix = useOldName ? oldTargetNamePrefix : targetNamePrefix;
}
ensurePrefix(targetName: string): string {
if (targetName.slice(this.prefix.length) !== this.prefix) {
targetName = this.prefix + targetName;
}
return targetName;
}
removePrefix(targetName: string): string {
return targetName.slice(this.prefix.length);
}
removePrefixFromCred(cred: any): any {
if (cred.targetName) {
cred.targetName = this.removePrefix(cred.targetName);
}
return cred;
}
}
function encodeTokenValueAsHex(token: TokenValueType): string {
const tokenValueAsString = JSON.stringify(token);
return Buffer.from(tokenValueAsString, "utf8").toString("hex");
}
function decodeTokenValueFromHex(token: string): TokenValueType {
return JSON.parse(Buffer.from(token, "hex").toString("utf8"));
}
function credToTokenEntry(cred: any): TokenEntry {
// Assumes credential comes in with prefixes on target skipped, and
// Credential object in hexidecimal
debug(`Converting credential ${inspect(cred)} to TokenEntry`);
return {
key: cred.targetName,
accessToken: decodeTokenValueFromHex(cred.credential)
};
}
export class WinTokenStore implements TokenStore {
/**
* 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(): Observable<TokenEntry> {
const prefixer = new Prefixer(false);
return Observable.create((observer: Observer<TokenEntry>) => {
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: Buffer, enc: any, done: Function) {
done(null, prefixer.removePrefixFromCred(chunk));
}));
credStream.on("data", (cred: any) => {
debug(`Got data from creds: ${cred}`);
observer.next(credToTokenEntry(cred));
});
credStream.on("end", () => {
debug(`output list completed`);
observer.complete();
});
credStream.on("error", (err: Error) => 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: TokenKeyType, useOldName: boolean = false): Promise<TokenEntry> {
const prefixer = new Prefixer(useOldName);
const args = [ "-s", "-t", prefixer.ensurePrefix(key) ];
const credsProcess = childProcess.spawn(credExePath, args);
let result: any = null;
const errors: string[] = [];
debug(`Getting key with args ${inspect(args)}`);
return new Promise<TokenEntry>((resolve, reject) => {
credsProcess.stdout.pipe(parser.createParsingStream())
.pipe(through.obj(function (chunk: Buffer, enc: any, done: Function) {
done(null, prefixer.removePrefixFromCred(chunk));
}))
.on("data", (credential: any) => {
result = credential;
result.targetName = prefixer.removePrefix(result.targetName);
});
credsProcess.stderr.pipe(split())
.on("data", (line: string) => {
errors.push(line);
});
credsProcess.on("exit", (code: number) => {
if (code === 0) {
debug(`Completed getting token, result = ${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: TokenKeyType, credential: TokenValueType): Promise<void> {
const prefixer = new Prefixer(false);
const args = [ "-a", "-t", prefixer.ensurePrefix(key), "-p", encodeTokenValueAsHex(credential) ];
debug(`Saving token with args ${inspect(args)}`);
return new Promise<void>((resolve, reject) => {
childProcess.execFile(credExePath, args,
function (err) {
if (err) {
debug(`Token store failed, ${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: TokenKeyType): Promise<void> {
const prefixer = new Prefixer(false);
const args = [ "-d", "-t", prefixer.ensurePrefix(key) ];
if (key.slice(-1) === "*") {
args.push("-g");
}
debug(`Deleting token with args ${inspect(args)}`);
return new Promise<void>((resolve, reject) => {
childProcess.execFile(credExePath, args,
function (err) {
if (err) { return reject(err); }
resolve();
});
});
}
}
export function createWinTokenStore(): TokenStore {
debug(`Creating WinTokenStore`);
return new WinTokenStore();
}