-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.js
executable file
·263 lines (241 loc) · 7.68 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#! /usr/bin/env node
const fs = require("fs");
const readline = require("readline");
const util = require('util');
const exec = util.promisify(require("child_process").exec);
const mkdir = util.promisify(fs.mkdir);
const path = require("path");
const crypto = require("crypto");
const ALGO = "aes-256-ctr";
function exit(msg) {
if (msg) {
process.stderr.write(`\x1b[31m${msg}\n\x1b[0m`);
}
process.exit(!!msg);
}
function encryptBlock(val, key, ivSecret) {
let hmac = crypto.createHmac("sha1", ivSecret);
hmac.update(val);
const iv = hmac.digest("hex").slice(0,16);
const cipher = crypto.createCipheriv(ALGO, key, iv);
let encrypted = cipher.update(val, 'utf8', 'base64');
encrypted += cipher.final('base64');
return [iv, encrypted].join('');
}
function decryptBlock(val, key, _ivSecret) {
const iv = val.slice(0,16); // First 16 bytes stored are the IV
const encrypted = val.slice(16) // ...remaining are the contents to decrypt
const decipher = crypto.createDecipheriv(ALGO, key, iv);
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
return (decrypted + decipher.final('utf8'));
}
const funcMap = {
clean: encryptBlock,
smudge: decryptBlock,
}
async function getConfigPath() {
try {
const { stdout } = await exec("git rev-parse --git-dir");
return stdout.replace('\n', '');
} catch(e) {
return exit(e.message)
}
}
function getDirPath(gitConfigPath) {
return `${gitConfigPath}/privatize`;
}
// NOTE: if we've already initialized privatize, don't overwrite
// our key
async function assertNotInitialized(gitConfigPath) {
try {
const initialized = fs.existsSync(getDirPath(gitConfigPath));
if (initialized) {
return exit("Error: this repo has already initialized privatize")
}
} catch(e) {
return exit(e.message)
}
}
async function createKeyAndIV({ gitConfigPath, path }) {
try {
let keyPath;
// NOTE: aes-256 should have 256B key length. we use the first 32
// bytes as the key and the next 16 as the initialization vector
const keyAndIV = crypto.randomBytes(48);
if (path) {
keyPath = path;
} else {
await mkdir(getDirPath(gitConfigPath));
keyPath = `${getDirPath(gitConfigPath)}/key`;
}
fs.writeFileSync(keyPath, keyAndIV);
} catch(e) {
exit(e.message);
}
}
async function copyKey(keyPath, gitConfigPath) {
try {
const gitPrivatizePath = getDirPath(gitConfigPath);
await mkdir(gitPrivatizePath);
fs.copyFileSync(keyPath, `${gitPrivatizePath}/key`);
} catch(e) {
exit(e.message);
}
}
async function resetHeadHard() {
try {
await exec("git reset --hard");
} catch(e) {
exit(e.message);
}
}
async function unlock(keyFile) {
try {
const gitConfigPath = await getConfigPath();
await assertNotInitialized(gitConfigPath);
if (!keyFile) {
return exit("Error: provide the key to decrypt encrypted files");
}
const keyExists = fs.existsSync(keyFile);
if (!keyExists) {
return exit("Error: provide the key to decrypt encrypted files");
}
await addGitFilters();
await copyKey(keyFile, gitConfigPath);
await resetHeadHard();
} catch(e) {
exit(e.message);
}
}
function privatizeStream({ cmd, readStream, keyFile }) {
return new Promise(async (resolve) => {
let keyAndIV;
if (keyFile) {
keyAndIV = fs.readFileSync(keyFile);
} else {
const gitConfigPath = await getConfigPath();
keyAndIV = fs.readFileSync(`${getDirPath(gitConfigPath)}/key`);
}
const key = keyAndIV.slice(0,32); // first 32B are the key
const ivSecret = keyAndIV.slice(32,48); // next 16B are the IV secret
let currentBlockIsProtected = false;
let heredocIndex = 0;
let lineNumber = 0;
let outputLines = {}; // { 0: 'some text', ... [x]: "some more text. <<PRIVATE", [x+1]: "" }
let privatizeQueue = [];
const rl = readline.createInterface({
input: readStream || process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', async function(line){
if (line.endsWith("<<PRIVATE")) {
outputLines[lineNumber] = line;
currentBlockIsProtected = true;
privatizeQueue.push({ raw: [], lineNumber: lineNumber + 1 });
} else if (currentBlockIsProtected) {
if (line.startsWith("PRIVATE")) {
outputLines[lineNumber] = `${line}`;
currentBlockIsProtected = false;
heredocIndex++;
} else {
privatizeQueue[heredocIndex].raw.push(line);
}
} else {
outputLines[lineNumber] = line;
}
lineNumber++;
})
rl.on('close', async function() {
if (currentBlockIsProtected) {
exit("Error: privatize's HEREDOC (<<PRIVATE) was opened but not closed.");
} else {
const cmdFunc = funcMap[cmd];
for (data of privatizeQueue) {
if (!cmdFunc) { // diff
outputLines[data.lineNumber] = data.raw.join('\n');
} else {
outputLines[data.lineNumber] = await cmdFunc(data.raw.join('\n'), key, ivSecret);
}
}
for (let i = 0; i <= lineNumber; i++) {
if (outputLines.hasOwnProperty(i)) {
process.stdout.write(`${outputLines[i]}\n`);
}
}
resolve();
}
});
});
}
async function addGitFilters() {
await exec("git config filter.privatize.smudge '\"privatize\" smudge'");
await exec("git config filter.privatize.clean '\"privatize\" clean'");
await exec("git config diff.privatize.textconv '\"privatize\" diff'");
await exec("git config filter.privatize.required true");
}
async function init() {
const gitConfigPath = await getConfigPath();
await assertNotInitialized(gitConfigPath);
await createKeyAndIV({ gitConfigPath });
await addGitFilters()
}
function help() {
console.log(`
Usage: privatize COMMAND [ARGS ...]
Commands:
git-init generate a key and prepare repo to use privatize
git-unlock KEYFILE decrypt this repo using the given symmetric key
export-git-key FILENAME export this repo's symmetric key to the given file
create-key FILENAME create a symmetric key for standalone encryption
encrypt KEYFILE encrypt a file from stdin and pipe to stdout
decrypt KEYFILE decrypt a file from stdin and pipe to stdout
help prints this message
`);
}
(async () => {
const cmd = process.argv[2];
if (['clean', 'smudge'].indexOf(cmd) > -1) { // internal, for filter
await privatizeStream({ cmd });
exit();
} else if (cmd === 'git-init') {
await init();
exit();
} else if (cmd === 'git-unlock') {
const keyFile = process.argv[3];
await unlock(keyFile);
exit();
} else if (cmd === 'encrypt') {
const keyFile = process.argv[3];
await privatizeStream({ cmd: "clean", keyFile });
exit();
} else if (cmd === 'decrypt') {
const keyFile = process.argv[3];
await privatizeStream({ cmd: "smudge", keyFile });
exit();
} else if (cmd === 'diff') { // internal, for git-diff
const diffFile = process.argv[3];
const newFile = diffFile.startsWith('/');
const readStream = fs.createReadStream(diffFile);
await privatizeStream({ cmd: "diff", readStream });
exit();
} else if (cmd == 'create-key') {
const fileName = process.argv[3];
if (!fileName) {
exit("Error: create-key filename not provided");
}
await createKeyAndIV({ path: fileName });
exit()
} else if (cmd == 'export-git-key') {
const fileName = process.argv[3];
if (!fileName) {
exit("Error: export-git-key filename not provided");
}
const gitConfigPath = await getConfigPath();
fs.copyFileSync(`${getDirPath(gitConfigPath)}/key`, fileName);
exit()
} else {
help()
exit();
}
})();