Skip to content

Commit

Permalink
Rewrite parser for osx-storage from js to ts and simplify it
Browse files Browse the repository at this point in the history
In previous implementation each entity was separated on lines but
currently we get chunks with all lines for each entity.
I have to extends Transform this way because of:
microsoft/TypeScript#17032
  • Loading branch information
rugpanov committed Dec 19, 2017
1 parent d1878e7 commit a37e3e3
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 2 deletions.
5 changes: 3 additions & 2 deletions src/bll/credentialsstore/osx/osx-keychain-access.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

import {injectable} from "inversify";
import {OsxSecurityParsingStream2} from "./osx-keychain-parser2";
const childProcess = require("child_process");
const es = require("event-stream");
const parser = require("./osx-keychain-parser");
Expand All @@ -21,13 +22,13 @@ export class OsxKeychain {

public getCredentialsWithoutPasswordsListStream() {
const securityProcess = childProcess.spawn(this.securityPath, ["dump-keychain"]);

const ll = new OsxSecurityParsingStream2();
return securityProcess.stdout
.pipe(es.split())
.pipe(es.mapSync(function (line) {
return line.replace(/\\134/g, "\\");
}))
.pipe(new parser.ParsingStream());
.pipe(ll);
}

public getPasswordForUser(targetName: string): Promise<string> {
Expand Down
123 changes: 123 additions & 0 deletions src/bll/credentialsstore/osx/osx-keychain-parser2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use strict";

import {Transform} from "stream";
const stream = require("readable-stream");
const util = require("util");

//
// Stream based parser for the OSX security(1) program output.
// Implements a simple state machine. States are:
//
// 0 - Waiting for the initial "keychain" string.
// 1 - Waiting for the "attributes" string. adds any properties to the
// current entry object being parsed while waiting.
// 2 - reading attributes. Continues adding the attributes to the
// current entry object until we hit either a non-indented line
// or end. At which point we emit.
//

export class OsxSecurityParsingStream2 extends (Transform as { new(): any; }) {

// Fields at the root - not attributes
private readonly rootFieldRe = /^([^:]+):(?: (?:"([^"]+)")|(.*))?$/;

// Attribute values, this gets a little more complicated
private readonly attrRe = /^ {4}(?:(0x[0-9a-fA-F]+) |"([a-z]{4})")<[^>]+>=(?:(<NULL>)|"([^"]+)"|(0x[0-9a-fA-F]+)(?: "([^"]+)")|(.*)?)/;

public constructor() {
super();
Transform.call(this, {
objectMode: true
});

this.currentEntry = undefined;
this.state = 0;
}

public _transform(chunk, encoding, callback) {
const lines = chunk.toString();
for (let i = 0; i < lines.length; i++) {
try {
this.processNextLine(lines[i]);
} catch (err) {
console.log(err);
callback(err);
}
}

callback();
}

private processNextLine(line: string) {
let match;
let value;
let count = 0;
while (line && line !== "") {
count++;
if (count > 2) {
throw new Error(`Multiple passes attempting to parse line [${line}]. Possible bug in parser and infinite loop`);
}

switch (this.state) {
case 0:
match = this.rootFieldRe.exec(line);
if (match !== null) {
if (match[1] === "keychain") {
this.currentEntry = {
keychain: match[2]
};
this.state = 1;
line = undefined;
} else {
this.currentEntry[match[1]] = match[2] || match[3];
}
}

break;

case 1:
match = this.rootFieldRe.exec(line);
if (match !== null) {
if (match[1] !== "attributes") {
this.currentEntry[match[1]] = match[2];
line = undefined;
} else {
this.state = 2;
line = undefined;
}
}
break;

case 2:
match = this.attrRe.exec(line);
if (match !== null) {
// Did we match a four-char named field? We don't care about hex fields
if (match[2]) {
// We skip nulls, and grab text rather than hex encoded versions of value
value = match[6] || match[4];
if (value) {
this.currentEntry[match[2]] = value;
}
}
line = undefined;
} else {
// Didn't match, so emit current entry, then
// reset to state zero and start processing for the
// next entry.
this.push(this.currentEntry);
this.currentEntry = undefined;
this.state = 0;
}
break;
}
}
}

public _flush(callback) {
if (this.currentEntry) {
this.push(this.currentEntry);
}
callback();
}
}

0 comments on commit a37e3e3

Please sign in to comment.