UNPKG

appcenter-cli

Version:

Command line tool for Visual Studio App Center

147 lines (127 loc) 4.64 kB
// Token store implementation over OSX keychain // // Access to the OSX keychain - list, add, get password, remove // import * as _ from "lodash"; import * as rx from "rxjs"; import * as childProcess from "child_process"; import * as from from "from2"; import * as split from "split2"; import * as through from "through2"; import * as stream from "stream"; import { TokenStore, TokenEntry, TokenKeyType, TokenValueType } from "../token-store"; import { createOsxSecurityParsingStream, OsxSecurityParsingStream } from "./osx-keychain-parser"; const debug = require("debug")("appcenter-cli:util:token-store:osx:osx-token-store"); import { inspect } from "util"; const securityPath = "/usr/bin/security"; const serviceName = "appcenter-cli"; const oldServiceName = "mobile-center-cli"; export class OsxTokenStore implements TokenStore { list(): rx.Observable<TokenEntry> { return rx.Observable.create((observer: rx.Observer<TokenEntry>) => { const securityProcess = childProcess.spawn(securityPath, ["dump-keychain"]); const securityStream = securityProcess.stdout .pipe(split()) .pipe(through(function (line: Buffer, enc: any, done: Function) { done(null, line.toString().replace(/\\134/g, "\\")); })) .pipe(new OsxSecurityParsingStream()); securityStream.on("data", (data: any) => { debug(`listing, got data ${inspect(data)}`); if (data.svce !== serviceName) { debug(`service does not match, skipping`); return; } const key: TokenKeyType = data.acct; // Have to get specific token to get tokens, but we have ids const accessToken: TokenValueType = { id: data.gena, token: null }; debug(`Outputting ${inspect({ key, accessToken })}`); observer.next({ key, accessToken }); }); securityStream.on("end", (err: Error) => { debug(`output from security program complete`); if (err) { observer.error(err); } else { observer.complete(); } }); }); } get(key: TokenKeyType, useOldName: boolean = false): Promise<TokenEntry> { const args = [ "find-generic-password", "-a", key, "-s", useOldName ? oldServiceName : serviceName, "-g" ]; return new Promise<TokenEntry>((resolve, reject) => { resolve = _.once(resolve); reject = _.once(reject); childProcess.execFile(securityPath, args, (err: Error, stdout: string, stderr: string) => { if (err) { return reject(err); } const match = /^password: (?:0x[0-9A-F]+. )?"(.*)"$/m.exec(stderr); if (match) { const accessToken = match[1].replace(/\\134/g, "\\"); debug(`stdout for security program = "${stdout}"`); debug(`parsing stdout`); // Parse the rest of the information from stdout to get user & token ID const parsed = from([stdout]) .pipe(createOsxSecurityParsingStream()); parsed.on("data", (data: any) => { debug(`got data on key lookup: ${inspect(data)}`); resolve({ key: data.acct, accessToken: { id: data.gena, token: accessToken } }); }); parsed.on("error", (err: Error) => { debug(`parsed string failed`); reject(err); }); } else { reject(new Error("Password in incorrect format")); } }); }); } set(key: TokenKeyType, value: TokenValueType): Promise<void> { const args = [ "add-generic-password", "-a", key, "-D", "appcenter cli password", "-s", serviceName, "-w", value.token, "-U" ]; if (value.id) { args.push("-G", value.id); } return new Promise<void>((resolve, reject) => { childProcess.execFile(securityPath, args, function (err, stdout, stderr) { if (err) { return reject(new Error("Could not add password to keychain: " + stderr)); } return resolve(); }); }); } remove(key: TokenKeyType): Promise<void> { const args = [ "delete-generic-password", "-a", key, "-s", serviceName ]; return new Promise<void>((resolve, reject) => { childProcess.execFile(securityPath, args, function (err, stdout, stderr) { if (err) { return reject(new Error("Could not remove account from keychain, " + stderr)); } return resolve(); }); }); } } export function createOsxTokenStore(): TokenStore { return new OsxTokenStore(); }