1
Fork 0

convert to ESM

This commit is contained in:
Conduitry 2021-04-21 08:09:10 -04:00
parent 219ca6c083
commit fd086972a3
3 changed files with 96 additions and 80 deletions

157
crypt.js
View File

@ -1,4 +1,4 @@
const CACHE_PATH = __dirname + '/cache'; const CACHE_PATH = dirname(fileURLToPath(import.meta.url)) + '/cache';
const DEFAULT_CIPHER_ALGORITHM = 'aes-256-cbc'; const DEFAULT_CIPHER_ALGORITHM = 'aes-256-cbc';
const DEFAULT_HASH_ALGORITHM = 'sha512'; const DEFAULT_HASH_ALGORITHM = 'sha512';
const DEFAULT_SPLIT_SIZE = 33554432; const DEFAULT_SPLIT_SIZE = 33554432;
@ -6,20 +6,43 @@ const HMAC_KEY_LENGTH = 32;
const RSA_KEY_BITS = 2048; const RSA_KEY_BITS = 2048;
const STREAM_CONCURRENCY = 8; const STREAM_CONCURRENCY = 8;
const crypto = require('crypto'); import {
const fs = require('fs'); createCipheriv,
const path_ = require('path'); createDecipheriv,
const v8 = require('v8'); createHash,
createHmac,
createPrivateKey,
createPublicKey,
generateKeyPairSync,
getCipherInfo,
privateDecrypt,
publicEncrypt,
randomBytes,
} from 'crypto';
import {
accessSync,
createReadStream,
createWriteStream,
mkdirSync,
readdirSync,
readFileSync,
statSync,
unlinkSync,
writeFileSync,
} from 'fs';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { deserialize, serialize } from 'v8';
function init({ export function init({
crypt: crypt_dir, crypt: crypt_dir,
cipher: cipher_algorithm = DEFAULT_CIPHER_ALGORITHM, cipher: cipher_algorithm = DEFAULT_CIPHER_ALGORITHM,
hash: hash_algorithm = DEFAULT_HASH_ALGORITHM, hash: hash_algorithm = DEFAULT_HASH_ALGORITHM,
split: split_size = DEFAULT_SPLIT_SIZE, split: split_size = DEFAULT_SPLIT_SIZE,
passphrase, passphrase,
}) { }) {
fs.mkdirSync(crypt_dir, { recursive: true }); mkdirSync(crypt_dir, { recursive: true });
fs.writeFileSync( writeFileSync(
crypt_dir + '/info', crypt_dir + '/info',
cipher_algorithm + cipher_algorithm +
'\n' + '\n' +
@ -27,30 +50,30 @@ function init({
'\n' + '\n' +
split_size + split_size +
'\n' + '\n' +
crypto.randomBytes(HMAC_KEY_LENGTH).toString('base64url') + randomBytes(HMAC_KEY_LENGTH).toString('base64url') +
'\n', '\n',
); );
const pair = crypto.generateKeyPairSync('rsa', { modulusLength: RSA_KEY_BITS }); const pair = generateKeyPairSync('rsa', { modulusLength: RSA_KEY_BITS });
fs.writeFileSync(crypt_dir + '/public', pair.publicKey.export({ type: 'spki', format: 'pem' })); writeFileSync(crypt_dir + '/public', pair.publicKey.export({ type: 'spki', format: 'pem' }));
fs.writeFileSync( writeFileSync(
crypt_dir + '/private', crypt_dir + '/private',
pair.privateKey.export({ type: 'pkcs8', format: 'pem', cipher: cipher_algorithm, passphrase }), pair.privateKey.export({ type: 'pkcs8', format: 'pem', cipher: cipher_algorithm, passphrase }),
); );
} }
function get_info(crypt_dir, passphrase) { function get_info(crypt_dir, passphrase) {
const s = fs.readFileSync(crypt_dir + '/info', 'ascii').match(/\S+/g); const s = readFileSync(crypt_dir + '/info', 'ascii').match(/\S+/g);
const info = { const info = {
cipher_algorithm: s[0], cipher_algorithm: s[0],
hash_algorithm: s[1], hash_algorithm: s[1],
split_size: +s[2], split_size: +s[2],
hmac_key: Buffer.from(s[3], 'base64url'), hmac_key: Buffer.from(s[3], 'base64url'),
public_key: crypto.createPublicKey(fs.readFileSync(crypt_dir + '/public')), public_key: createPublicKey(readFileSync(crypt_dir + '/public')),
index: new Map(), index: new Map(),
}; };
for (const dirent of fs.readdirSync(crypt_dir, { withFileTypes: true })) { for (const dirent of readdirSync(crypt_dir, { withFileTypes: true })) {
if (dirent.isFile() && dirent.name.endsWith('-index')) { if (dirent.isFile() && dirent.name.endsWith('-index')) {
const s = fs.readFileSync(crypt_dir + '/' + dirent.name, 'ascii').match(/\S+/g); const s = readFileSync(crypt_dir + '/' + dirent.name, 'ascii').match(/\S+/g);
info.index.set(dirent.name.slice(0, -6), { info.index.set(dirent.name.slice(0, -6), {
hash_hmac: Buffer.from(s[0], 'base64url'), hash_hmac: Buffer.from(s[0], 'base64url'),
key: Buffer.from(s[1], 'base64url'), key: Buffer.from(s[1], 'base64url'),
@ -60,8 +83,8 @@ function get_info(crypt_dir, passphrase) {
} }
} }
if (passphrase != null) { if (passphrase != null) {
info.private_key = crypto.createPrivateKey({ info.private_key = createPrivateKey({
key: fs.readFileSync(crypt_dir + '/private'), key: readFileSync(crypt_dir + '/private'),
passphrase, passphrase,
}); });
} }
@ -71,7 +94,7 @@ function get_info(crypt_dir, passphrase) {
async function get_plain_index(plain_dir, hash_algorithm, filter) { async function get_plain_index(plain_dir, hash_algorithm, filter) {
let cache; let cache;
try { try {
cache = v8.deserialize(fs.readFileSync(CACHE_PATH)); cache = deserialize(readFileSync(CACHE_PATH));
} catch { } catch {
cache = new Map(); cache = new Map();
} }
@ -80,9 +103,9 @@ async function get_plain_index(plain_dir, hash_algorithm, filter) {
const stream_queue = make_stream_queue(); const stream_queue = make_stream_queue();
while (pending.length) { while (pending.length) {
const dir = pending.shift(); const dir = pending.shift();
for (const name of fs.readdirSync(dir)) { for (const name of readdirSync(dir)) {
const path = dir + '/' + name; const path = dir + '/' + name;
const stats = fs.statSync(path); const stats = statSync(path);
if (stats.isFile()) { if (stats.isFile()) {
if (!filter || filter(path.slice(plain_dir.length + 1))) { if (!filter || filter(path.slice(plain_dir.length + 1))) {
const key = path + ':' + hash_algorithm; const key = path + ':' + hash_algorithm;
@ -97,9 +120,8 @@ async function get_plain_index(plain_dir, hash_algorithm, filter) {
}); });
} else { } else {
stream_queue.add(() => stream_queue.add(() =>
fs createReadStream(path)
.createReadStream(path) .pipe(createHash(hash_algorithm))
.pipe(crypto.createHash(hash_algorithm))
.once('readable', function () { .once('readable', function () {
const hash = this.read(); const hash = this.read();
cache.set(key, { size: stats.size, mtimeMs: stats.mtimeMs, hash }); cache.set(key, { size: stats.size, mtimeMs: stats.mtimeMs, hash });
@ -114,14 +136,13 @@ async function get_plain_index(plain_dir, hash_algorithm, filter) {
} }
} }
await stream_queue.done(); await stream_queue.done();
fs.writeFileSync(CACHE_PATH, v8.serialize(cache)); writeFileSync(CACHE_PATH, serialize(cache));
return plain_index; return plain_index;
} }
function get_crypt_filename(info, path, start) { function get_crypt_filename(info, path, start) {
return ( return (
crypto createHmac(info.hash_algorithm, info.hmac_key)
.createHmac(info.hash_algorithm, info.hmac_key)
.update(path + '@' + start) .update(path + '@' + start)
.digest('base64url') + '-data' .digest('base64url') + '-data'
); );
@ -158,20 +179,20 @@ function make_stream_queue() {
}; };
} }
async function encrypt({ plain: plain_dir, crypt: crypt_dir, filter }) { export async function encrypt({ plain: plain_dir, crypt: crypt_dir, filter }) {
const added = new Set(); const added = new Set();
const deleted = new Set(); const deleted = new Set();
const updated = new Set(); const updated = new Set();
// READ CRYPT INDEX // READ CRYPT INDEX
const info = get_info(crypt_dir); const info = get_info(crypt_dir);
const { keyLength, ivLength } = crypto.getCipherInfo(info.cipher_algorithm); const { keyLength, ivLength } = getCipherInfo(info.cipher_algorithm);
// CONSTRUCT PLAIN INDEX // CONSTRUCT PLAIN INDEX
const plain_index = await get_plain_index(plain_dir, info.hash_algorithm, filter); const plain_index = await get_plain_index(plain_dir, info.hash_algorithm, filter);
// CREATE INDEX OF PLAIN FILES AS THEY WILL APPEAR IN THE CRYPT INDEX // CREATE INDEX OF PLAIN FILES AS THEY WILL APPEAR IN THE CRYPT INDEX
const path_hmac_lookup = new Map(); const path_hmac_lookup = new Map();
for (const path of plain_index.keys()) { for (const path of plain_index.keys()) {
path_hmac_lookup.set( path_hmac_lookup.set(
crypto.createHmac(info.hash_algorithm, info.hmac_key).update(path).digest('base64url'), createHmac(info.hash_algorithm, info.hmac_key).update(path).digest('base64url'),
path, path,
); );
} }
@ -179,14 +200,14 @@ async function encrypt({ plain: plain_dir, crypt: crypt_dir, filter }) {
for (const path_hmac of info.index.keys()) { for (const path_hmac of info.index.keys()) {
if (!path_hmac_lookup.has(path_hmac)) { if (!path_hmac_lookup.has(path_hmac)) {
deleted.add(path_hmac + '-index'); deleted.add(path_hmac + '-index');
fs.unlinkSync(crypt_dir + '/' + path_hmac + '-index'); unlinkSync(crypt_dir + '/' + path_hmac + '-index');
} }
} }
// UPDATE/ADD FILES // UPDATE/ADD FILES
const stream_queue = make_stream_queue(); const stream_queue = make_stream_queue();
for (const [path_hmac, path] of path_hmac_lookup) { for (const [path_hmac, path] of path_hmac_lookup) {
const { size, hash } = plain_index.get(path); const { size, hash } = plain_index.get(path);
const hash_hmac = crypto.createHmac(info.hash_algorithm, info.hmac_key).update(hash).digest(); const hash_hmac = createHmac(info.hash_algorithm, info.hmac_key).update(hash).digest();
if (!info.index.has(path_hmac)) { if (!info.index.has(path_hmac)) {
added.add(path); added.add(path);
} else if (Buffer.compare(info.index.get(path_hmac).hash_hmac, hash_hmac)) { } else if (Buffer.compare(info.index.get(path_hmac).hash_hmac, hash_hmac)) {
@ -194,16 +215,16 @@ async function encrypt({ plain: plain_dir, crypt: crypt_dir, filter }) {
} else { } else {
continue; continue;
} }
const key = crypto.randomBytes(keyLength); const key = randomBytes(keyLength);
const iv = crypto.randomBytes(ivLength); const iv = randomBytes(ivLength);
const cipher = crypto.createCipheriv(info.cipher_algorithm, key, iv); const cipher = createCipheriv(info.cipher_algorithm, key, iv);
fs.writeFileSync( writeFileSync(
crypt_dir + '/' + path_hmac + '-index', crypt_dir + '/' + path_hmac + '-index',
hash_hmac.toString('base64url') + hash_hmac.toString('base64url') +
'\n' + '\n' +
crypto.publicEncrypt(info.public_key, key).toString('base64url') + publicEncrypt(info.public_key, key).toString('base64url') +
'\n' + '\n' +
crypto.publicEncrypt(info.public_key, iv).toString('base64url') + publicEncrypt(info.public_key, iv).toString('base64url') +
'\n' + '\n' +
Buffer.concat([cipher.update(path), cipher.final()]).toString('base64url') + Buffer.concat([cipher.update(path), cipher.final()]).toString('base64url') +
'\n', '\n',
@ -211,17 +232,16 @@ async function encrypt({ plain: plain_dir, crypt: crypt_dir, filter }) {
for (let start = 0; ; start += info.split_size) { for (let start = 0; ; start += info.split_size) {
if (start < size) { if (start < size) {
stream_queue.add(() => stream_queue.add(() =>
fs createReadStream(plain_dir + '/' + path, {
.createReadStream(plain_dir + '/' + path, { start,
start, end: Math.min(start + info.split_size - 1, size - 1),
end: Math.min(start + info.split_size - 1, size - 1), })
}) .pipe(createCipheriv(info.cipher_algorithm, key, iv))
.pipe(crypto.createCipheriv(info.cipher_algorithm, key, iv)) .pipe(createWriteStream(crypt_dir + '/' + get_crypt_filename(info, path, start))),
.pipe(fs.createWriteStream(crypt_dir + '/' + get_crypt_filename(info, path, start))),
); );
} else { } else {
try { try {
fs.unlinkSync(crypt_dir + '/' + get_crypt_filename(info, path, start)); unlinkSync(crypt_dir + '/' + get_crypt_filename(info, path, start));
} catch { } catch {
break; break;
} }
@ -232,21 +252,21 @@ async function encrypt({ plain: plain_dir, crypt: crypt_dir, filter }) {
return { added, deleted, updated }; return { added, deleted, updated };
} }
function clean({ crypt: crypt_dir, passphrase }) { export function clean({ crypt: crypt_dir, passphrase }) {
// READ CRYPT INDEX // READ CRYPT INDEX
const info = get_info(crypt_dir, passphrase); const info = get_info(crypt_dir, passphrase);
// GET CRYPT FILES // GET CRYPT FILES
const crypt_filenames = new Set(); const crypt_filenames = new Set();
for (const dirent of fs.readdirSync(crypt_dir, { withFileTypes: true })) { for (const dirent of readdirSync(crypt_dir, { withFileTypes: true })) {
if (dirent.isFile() && dirent.name.endsWith('-data')) { if (dirent.isFile() && dirent.name.endsWith('-data')) {
crypt_filenames.add(dirent.name); crypt_filenames.add(dirent.name);
} }
} }
// SKIP ALL FILES REFERRED TO BY AN INDEX // SKIP ALL FILES REFERRED TO BY AN INDEX
for (const item of info.index.values()) { for (const item of info.index.values()) {
const key = crypto.privateDecrypt(info.private_key, item.key); const key = privateDecrypt(info.private_key, item.key);
const iv = crypto.privateDecrypt(info.private_key, item.iv); const iv = privateDecrypt(info.private_key, item.iv);
const decipher = crypto.createDecipheriv(info.cipher_algorithm, key, iv); const decipher = createDecipheriv(info.cipher_algorithm, key, iv);
const path = Buffer.concat([decipher.update(item.path), decipher.final()]).toString(); const path = Buffer.concat([decipher.update(item.path), decipher.final()]).toString();
for (let start = 0; ; start += info.split_size) { for (let start = 0; ; start += info.split_size) {
const crypt_filename = get_crypt_filename(info, path, start); const crypt_filename = get_crypt_filename(info, path, start);
@ -259,16 +279,16 @@ function clean({ crypt: crypt_dir, passphrase }) {
} }
// DELETE UNUSED CRYPT FILES // DELETE UNUSED CRYPT FILES
for (const crypt_filename of crypt_filenames) { for (const crypt_filename of crypt_filenames) {
fs.unlinkSync(crypt_dir + '/' + crypt_filename); unlinkSync(crypt_dir + '/' + crypt_filename);
} }
return { deleted: crypt_filenames }; return { deleted: crypt_filenames };
} }
async function decrypt({ plain: plain_dir, crypt: crypt_dir, filter, passphrase }) { export async function decrypt({ plain: plain_dir, crypt: crypt_dir, filter, passphrase }) {
const added = new Set(); const added = new Set();
const deleted = new Set(); const deleted = new Set();
const updated = new Set(); const updated = new Set();
fs.mkdirSync(plain_dir, { recursive: true }); mkdirSync(plain_dir, { recursive: true });
// READ CRYPT INDEX // READ CRYPT INDEX
const info = get_info(crypt_dir, passphrase); const info = get_info(crypt_dir, passphrase);
// CONSTRUCT PLAIN INDEX // CONSTRUCT PLAIN INDEX
@ -277,15 +297,14 @@ async function decrypt({ plain: plain_dir, crypt: crypt_dir, filter, passphrase
// DELETE MISSING FILES // DELETE MISSING FILES
const path_hmac_lookup = new Map(); const path_hmac_lookup = new Map();
for (const path of plain_index.keys()) { for (const path of plain_index.keys()) {
const path_hmac = crypto const path_hmac = createHmac(info.hash_algorithm, info.hmac_key)
.createHmac(info.hash_algorithm, info.hmac_key)
.update(path) .update(path)
.digest('base64url'); .digest('base64url');
if (info.index.has(path_hmac)) { if (info.index.has(path_hmac)) {
path_hmac_lookup.set(path_hmac, path); path_hmac_lookup.set(path_hmac, path);
} else { } else {
deleted.add(path); deleted.add(path);
fs.unlinkSync(plain_dir + '/' + path); unlinkSync(plain_dir + '/' + path);
} }
} }
// UPDATE/ADD FILES // UPDATE/ADD FILES
@ -296,8 +315,7 @@ async function decrypt({ plain: plain_dir, crypt: crypt_dir, filter, passphrase
target = added; target = added;
} else if ( } else if (
Buffer.compare( Buffer.compare(
crypto createHmac(info.hash_algorithm, info.hmac_key)
.createHmac(info.hash_algorithm, info.hmac_key)
.update(plain_index.get(path_hmac_lookup.get(path_hmac)).hash) .update(plain_index.get(path_hmac_lookup.get(path_hmac)).hash)
.digest(), .digest(),
item.hash_hmac, item.hash_hmac,
@ -307,30 +325,27 @@ async function decrypt({ plain: plain_dir, crypt: crypt_dir, filter, passphrase
} else { } else {
continue; continue;
} }
const key = crypto.privateDecrypt(info.private_key, item.key); const key = privateDecrypt(info.private_key, item.key);
const iv = crypto.privateDecrypt(info.private_key, item.iv); const iv = privateDecrypt(info.private_key, item.iv);
const decipher = crypto.createDecipheriv(info.cipher_algorithm, key, iv); const decipher = createDecipheriv(info.cipher_algorithm, key, iv);
const path = Buffer.concat([decipher.update(item.path), decipher.final()]).toString(); const path = Buffer.concat([decipher.update(item.path), decipher.final()]).toString();
target.add(path); target.add(path);
fs.mkdirSync(plain_dir + '/' + path_.dirname(path), { recursive: true }); mkdirSync(plain_dir + '/' + dirname(path), { recursive: true });
fs.writeFileSync(plain_dir + '/' + path, Buffer.alloc(0)); writeFileSync(plain_dir + '/' + path, Buffer.alloc(0));
for (let start = 0; ; start += info.split_size) { for (let start = 0; ; start += info.split_size) {
const file = crypt_dir + '/' + get_crypt_filename(info, path, start); const file = crypt_dir + '/' + get_crypt_filename(info, path, start);
try { try {
fs.accessSync(file); accessSync(file);
} catch { } catch {
break; break;
} }
stream_queue.add(() => stream_queue.add(() =>
fs createReadStream(file)
.createReadStream(file) .pipe(createDecipheriv(info.cipher_algorithm, key, iv))
.pipe(crypto.createDecipheriv(info.cipher_algorithm, key, iv)) .pipe(createWriteStream(plain_dir + '/' + path, { flags: 'r+', start })),
.pipe(fs.createWriteStream(plain_dir + '/' + path, { flags: 'r+', start })),
); );
} }
} }
await stream_queue.done(); await stream_queue.done();
return { added, deleted, updated }; return { added, deleted, updated };
} }
module.exports = { init, encrypt, clean, decrypt };

3
package.json Normal file
View File

@ -0,0 +1,3 @@
{
"type": "module"
}

16
pass.js
View File

@ -1,12 +1,12 @@
const readline = require('readline'); import { createInterface } from 'readline';
const stream = require('stream'); import { Writable } from 'stream';
const devnull = new stream.Writable({ write: (chunk, encoding, cb) => cb() });
function get_pass(prompt) { const devnull = new Writable({ write: (chunk, encoding, cb) => cb() });
export function get_pass(prompt) {
process.stdout.write(prompt); process.stdout.write(prompt);
return new Promise((res, rej) => { return new Promise((res, rej) => {
const rl = readline const rl = createInterface({ input: process.stdin, output: devnull, terminal: true })
.createInterface({ input: process.stdin, output: devnull, terminal: true })
.once('line', (line) => { .once('line', (line) => {
res(line); res(line);
rl.close(); rl.close();
@ -18,7 +18,7 @@ function get_pass(prompt) {
}); });
} }
async function confirm_pass(prompt1, prompt2, error) { export async function confirm_pass(prompt1, prompt2, error) {
for (;;) { for (;;) {
const pass1 = await get_pass(prompt1); const pass1 = await get_pass(prompt1);
const pass2 = await get_pass(prompt2); const pass2 = await get_pass(prompt2);
@ -28,5 +28,3 @@ async function confirm_pass(prompt1, prompt2, error) {
process.stdout.write(error + '\n'); process.stdout.write(error + '\n');
} }
} }
module.exports = { get_pass, confirm_pass };