UNPKG

node-file-encrypt

Version:

a file encrypt & decrypt package based on TEA

430 lines (407 loc) 19.9 kB
/* - - - - - - - - - - - - - - - - file encrypt by fiefdx- - - - - - - - - - - - - - - - - */ "use strict"; import path from 'path'; import fs from 'fs'; import util from 'util'; import tea from 'node-tea'; import sha1 from 'sha1'; let encrypt_block_size = 1024 * 8; const fsRead = util.promisify(fs.read); const fsOpen = util.promisify(fs.open); const fsWrite = util.promisify(fs.write); const fsWriteFile = util.promisify(fs.writeFile); const fsClose = util.promisify(fs.close); function getEncryptLength(length) { let fillN = (8 - (length + 2)) % 8; if (fillN < 0) { fillN += 8; } fillN += 2; return 1 + length + fillN + 7; } function intToBytes(i) { let b = new Array(4); b[3] = i & 0x000000ff; b[2] = (i >>> 8) & 0x000000ff; b[1] = (i >>> 16) & 0x000000ff; b[0] = (i >>> 24) & 0x000000ff; return b; } function bytesToInt(b) { let i; i = (((b[0] & 0x000000ff) << 24)| ((b[1] & 0x000000ff) << 16)| ((b[2] & 0x000000ff) << 8)| (b[3] & 0x000000ff)); return i; } function longToBytes(i) { let b = new Array(8); b[7] = i & 0x00000000000000ff; b[6] = (i >>> 8) & 0x00000000000000ff; b[5] = (i >>> 16) & 0x00000000000000ff; b[4] = (i >>> 24) & 0x00000000000000ff; b[3] = (i >>> 32) & 0x00000000000000ff; b[2] = (i >>> 40) & 0x00000000000000ff; b[1] = (i >>> 48) & 0x00000000000000ff; b[0] = (i >>> 56) & 0x00000000000000ff; return b; } function bytesToLong(b) { let i; i = (((b[0] & 0x00000000000000ff) << 56)| ((b[1] & 0x00000000000000ff) << 48)| ((b[2] & 0x00000000000000ff) << 40)| ((b[3] & 0x00000000000000ff) << 32)| ((b[4] & 0x00000000000000ff) << 24)| ((b[5] & 0x00000000000000ff) << 16)| ((b[6] & 0x00000000000000ff) << 8)| (b[7] & 0x00000000000000ff)); return i; } export function FileEncrypt(inPath, outPath, fileType, cryptFileName = true) { this.inPath = inPath; this.outPath = outPath || path.dirname(this.inPath); // if not specified outPath, then outPath will be inPath's base path this.fileName = path.basename(inPath); this.fileSize = 0; this.fileHeader = 'crypt'; this.fileType = fileType || '.crypt'; this.cryptFileName = cryptFileName; this.fileNamePos = 0; this.fileNameLen = 0; this.filePos = 0; this.fileLen = 0; this.encryptFileName = ''; this.encryptFilePath = ''; this.decryptFilePath = ''; this.fp = null; this.progressCallInterval = 100; // 100 milliseconds } FileEncrypt.prototype.openSourceFile = function() { if (fs.existsSync(this.inPath) && fs.lstatSync(this.inPath).isFile()) { const stats = fs.statSync(this.inPath); this.fileSize = stats.size; this.fp = fs.openSync(this.inPath, 'r'); } else { throw new Error(util.format("File [%s] doesn't exists!", this.inPath)); } } FileEncrypt.prototype.encrypt = function(key, progressCallback) { // progressCallback(percent, startAt) let startAt = new Date(); let lastCallAt = new Date(); if (this.cryptFileName) { let fileNameHash = sha1(this.fileName); let timestamp = util.format("%d", Date.now()); this.encryptFileName = sha1(util.format("%s%s%s", fileNameHash, this.fileSize, timestamp)) + this.fileType; this.encryptFilePath = path.join(this.outPath, this.encryptFileName); } else { let fileName = this.fileName.replace(/\.[^.]*$/, '') + this.fileType; this.encryptFilePath = path.join(this.outPath, fileName); } let cryptFp = null; if (!fs.existsSync(this.encryptFilePath)) { cryptFp = fs.openSync(this.encryptFilePath, 'w'); } else { throw new Error(util.format("Encrypt file [%s] exists already!", this.encryptFilePath)); } if (this.fp && cryptFp) { if (key != '') { if (progressCallback && typeof progressCallback === 'function') { progressCallback(0, startAt); } let headerFileName = tea.strToBytes(tea.encrypt(this.fileName, key)); this.fileNamePos = 25; this.fileNameLen = headerFileName.length; this.filePos = this.fileNamePos + this.fileNameLen; fs.writeFileSync(cryptFp, new Buffer.from(tea.strToBytes(this.fileHeader)), {encoding: 'binary', flag: 'a'}); fs.writeFileSync(cryptFp, new Buffer.from(intToBytes(this.fileNamePos)), {encoding: 'binary', flag: 'a'}); fs.writeFileSync(cryptFp, new Buffer.from(intToBytes(this.fileNameLen)), {encoding: 'binary', flag: 'a'}); fs.writeFileSync(cryptFp, new Buffer.from(intToBytes(this.filePos)), {encoding: 'binary', flag: 'a'}); fs.writeFileSync(cryptFp, new Buffer.from(longToBytes(this.fileLen)), {encoding: 'binary', flag: 'a'}); fs.writeFileSync(cryptFp, new Buffer.from(headerFileName), {encoding: 'binary', flag: 'a'}); let currentPos = 0; while (true) { let buf = new Buffer.alloc(encrypt_block_size); let size = fs.readSync(this.fp, buf, 0, encrypt_block_size, currentPos); if (size == 0) { fs.closeSync(this.fp); break; } let cryptBuf = new Buffer.from(tea.encryptBytes(Array.from(buf.slice(0, size)), key)); this.fileLen += cryptBuf.length; fs.writeFileSync(cryptFp, cryptBuf, {encoding: 'binary', flag: 'a'}); currentPos += encrypt_block_size; let now = Date.now(); if (progressCallback && typeof progressCallback === 'function' && currentPos < this.fileSize && now - lastCallAt.getTime() >= this.progressCallInterval) { let percent = Math.floor(currentPos * 100 / this.fileSize); if (percent > 100) { percent = 100; } progressCallback(percent, startAt); lastCallAt = new Date(); } } fs.writeSync(cryptFp, new Buffer.from(longToBytes(this.fileLen)), 0, 8, 17); if (progressCallback && typeof progressCallback === 'function') { progressCallback(100, startAt); } } else { throw new Error("Password is empty!"); } fs.closeSync(cryptFp); } } FileEncrypt.prototype.decrypt = function(key, progressCallback) { // progressCallback(percent, startAt) let startAt = new Date(); let lastCallAt = new Date(); let cryptFp = null; if (this.fp) { let fileHeader = new Buffer.alloc(this.fileHeader.length); fs.readSync(this.fp, fileHeader, 0, 5); if (tea.bytesToStr(Array.from(fileHeader)) == this.fileHeader) { if (key != '') { if (progressCallback && typeof progressCallback === 'function') { progressCallback(0, startAt); } let fileNamePosBuf = new Buffer.alloc(4); let fileNameLenBuf = new Buffer.alloc(4); let filePosBuf = new Buffer.alloc(4); let fileLenBuf = new Buffer.alloc(8); fs.readSync(this.fp, fileNamePosBuf, 0, 4); fs.readSync(this.fp, fileNameLenBuf, 0, 4); fs.readSync(this.fp, filePosBuf, 0, 4); fs.readSync(this.fp, fileLenBuf, 0, 8); this.fileNamePos = bytesToInt(Array.from(fileNamePosBuf)); this.fileNameLen = bytesToInt(Array.from(fileNameLenBuf)); this.filePos = bytesToInt(Array.from(filePosBuf)); this.fileLen = bytesToLong(Array.from(fileLenBuf)); let fileNameBuf = new Buffer.alloc(this.fileNameLen); fs.readSync(this.fp, fileNameBuf, 0, this.fileNameLen, this.fileNamePos); let fileName = tea.bytesToStr(tea.decryptBytes(Array.from(fileNameBuf), key)); this.decryptFilePath = path.join(this.outPath, fileName); if (!fs.existsSync(this.decryptFilePath)) { cryptFp = fs.openSync(this.decryptFilePath, 'w'); } else { throw new Error(util.format("Decrypt file [%s] exists already!", this.decryptFilePath)); } let cryptLength = getEncryptLength(encrypt_block_size); let currentPos = this.filePos; while (true) { let size = 0; let buf = new Buffer.alloc(cryptLength); if (this.fileLen < cryptLength) { size = fs.readSync(this.fp, buf, 0, this.fileLen, currentPos); } else { size = fs.readSync(this.fp, buf, 0, cryptLength, currentPos); } if (size == 0) { fs.closeSync(this.fp); break; } this.fileLen -= size; let decryptBytes = tea.decryptBytes(Array.from(buf.slice(0, size)), key); fs.writeFileSync(cryptFp, new Buffer.from(decryptBytes), {encoding: 'binary', flag: 'a'}); currentPos += size; let now = Date.now(); if (progressCallback && typeof progressCallback === 'function' && currentPos < this.fileSize && now - lastCallAt.getTime() >= this.progressCallInterval) { let percent = Math.floor(currentPos * 100 / this.fileSize); if (percent > 100) { percent = 100; } progressCallback(percent, startAt); lastCallAt = new Date(); } } fs.closeSync(cryptFp); if (progressCallback && typeof progressCallback === 'function') { progressCallback(100, startAt); } } else { throw new Error("Password is empty!"); } } else { throw new Error(util.format("This file [%s] is not a encrypt file!", this.inPath)); } } } FileEncrypt.prototype.encryptAsync = async function(key, progressCallback, callback) { let err = null; let startAt = new Date(); let lastCallAt = new Date(); if (this.cryptFileName) { let fileNameHash = sha1(this.fileName); let timestamp = util.format("%d", Date.now()); this.encryptFileName = sha1(util.format("%s%s%s", fileNameHash, this.fileSize, timestamp)) + this.fileType; this.encryptFilePath = path.join(this.outPath, this.encryptFileName); } else { let fileName = this.fileName.replace(/\.[^.]*$/, '') + this.fileType; this.encryptFilePath = path.join(this.outPath, fileName); } let cryptFp = null; if (!fs.existsSync(this.encryptFilePath)) { cryptFp = await fsOpen(this.encryptFilePath, 'w'); if (this.fp && cryptFp) { if (key != '') { if (progressCallback && typeof progressCallback === 'function') { progressCallback(0, startAt); } let headerFileName = tea.strToBytes(tea.encrypt(this.fileName, key)); this.fileNamePos = 25; this.fileNameLen = headerFileName.length; this.filePos = this.fileNamePos + this.fileNameLen; await fsWriteFile(cryptFp, new Buffer.from(tea.strToBytes(this.fileHeader)), {encoding: 'binary', flag: 'a'}); await fsWriteFile(cryptFp, new Buffer.from(intToBytes(this.fileNamePos)), {encoding: 'binary', flag: 'a'}); await fsWriteFile(cryptFp, new Buffer.from(intToBytes(this.fileNameLen)), {encoding: 'binary', flag: 'a'}); await fsWriteFile(cryptFp, new Buffer.from(intToBytes(this.filePos)), {encoding: 'binary', flag: 'a'}); await fsWriteFile(cryptFp, new Buffer.from(longToBytes(this.fileLen)), {encoding: 'binary', flag: 'a'}); await fsWriteFile(cryptFp, new Buffer.from(headerFileName), {encoding: 'binary', flag: 'a'}); let currentPos = 0; while (true) { let buf = new Buffer.alloc(encrypt_block_size); let r = await fsRead(this.fp, buf, 0, encrypt_block_size, currentPos); let size = r.bytesRead; if (size == 0) { await fsClose(this.fp); break; } let cryptBuf = new Buffer.from(tea.encryptBytes(Array.from(buf.slice(0, size)), key)); this.fileLen += cryptBuf.length; await fsWriteFile(cryptFp, cryptBuf, {encoding: 'binary', flag: 'a'}); currentPos += encrypt_block_size; let now = Date.now(); if (progressCallback && typeof progressCallback === 'function' && currentPos < this.fileSize && now - lastCallAt.getTime() >= this.progressCallInterval) { let percent = Math.floor(currentPos * 100 / this.fileSize); if (percent > 100) { percent = 100; } progressCallback(percent, startAt); lastCallAt = new Date(); } } await fsWrite(cryptFp, new Buffer.from(longToBytes(this.fileLen)), 0, 8, 17); if (progressCallback && typeof progressCallback === 'function') { progressCallback(100, startAt); } } else { err = new Error("Password is empty!"); } await fsClose(cryptFp); } } else { err = new Error(util.format("Encrypt file [%s] exists already!", this.encryptFilePath)); } if (callback) { callback(err); } } FileEncrypt.prototype.decryptAsync = async function(key, progressCallback, callback) { // progressCallback(percent, startAt) let err = null; let startAt = new Date(); let lastCallAt = new Date(); let cryptFp = null; if (this.fp) { let fileHeader = new Buffer.alloc(this.fileHeader.length); await fsRead(this.fp, fileHeader, 0, 5, 0); if (tea.bytesToStr(Array.from(fileHeader)) == this.fileHeader) { if (key != '') { if (progressCallback && typeof progressCallback === 'function') { progressCallback(0, startAt); } let fileNamePosBuf = new Buffer.alloc(4); let fileNameLenBuf = new Buffer.alloc(4); let filePosBuf = new Buffer.alloc(4); let fileLenBuf = new Buffer.alloc(8); await fsRead(this.fp, fileNamePosBuf, 0, 4, 5); await fsRead(this.fp, fileNameLenBuf, 0, 4, 9); await fsRead(this.fp, filePosBuf, 0, 4, 13); await fsRead(this.fp, fileLenBuf, 0, 8, 17); this.fileNamePos = bytesToInt(Array.from(fileNamePosBuf)); this.fileNameLen = bytesToInt(Array.from(fileNameLenBuf)); this.filePos = bytesToInt(Array.from(filePosBuf)); this.fileLen = bytesToLong(Array.from(fileLenBuf)); let fileNameBuf = new Buffer.alloc(this.fileNameLen); await fsRead(this.fp, fileNameBuf, 0, this.fileNameLen, this.fileNamePos); let fileName = tea.bytesToStr(tea.decryptBytes(Array.from(fileNameBuf), key)); this.decryptFilePath = path.join(this.outPath, fileName); if (!fs.existsSync(this.decryptFilePath)) { cryptFp = await fsOpen(this.decryptFilePath, 'w'); let cryptLength = getEncryptLength(encrypt_block_size); let currentPos = this.filePos; while (true) { let size = 0; let buf = new Buffer.alloc(cryptLength); if (this.fileLen < cryptLength) { let r = await fsRead(this.fp, buf, 0, this.fileLen, currentPos); size = r.bytesRead; } else { let r = await fsRead(this.fp, buf, 0, cryptLength, currentPos); size = r.bytesRead; } if (size == 0) { await fsClose(this.fp); break; } this.fileLen -= size; let decryptBytes = tea.decryptBytes(Array.from(buf.slice(0, size)), key); await fsWriteFile(cryptFp, new Buffer.from(decryptBytes), {encoding: 'binary', flag: 'a'}); currentPos += size; let now = Date.now(); if (progressCallback && typeof progressCallback === 'function' && currentPos < this.fileSize && now - lastCallAt.getTime() >= this.progressCallInterval) { let percent = Math.floor(currentPos * 100 / this.fileSize); if (percent > 100) { percent = 100; } progressCallback(percent, startAt); lastCallAt = new Date(); } } await fsClose(cryptFp); if (progressCallback && typeof progressCallback === 'function') { progressCallback(100, startAt); } } else { err = new Error(util.format("Decrypt file [%s] exists already!", this.decryptFilePath)); } } else { err = new Error("Password is empty!"); } } else { err = new Error(util.format("This file [%s] is not a encrypt file!", this.inPath)); } } if (callback) { callback(err); } } FileEncrypt.prototype.info = function(key) { let result = {name: ''}; if (this.fp) { let fileHeader = new Buffer.alloc(this.fileHeader.length); fs.readSync(this.fp, fileHeader, 0, 5); if (tea.bytesToStr(Array.from(fileHeader)) == this.fileHeader) { if (key != '') { let fileNamePosBuf = new Buffer.alloc(4); let fileNameLenBuf = new Buffer.alloc(4); let filePosBuf = new Buffer.alloc(4); let fileLenBuf = new Buffer.alloc(8); fs.readSync(this.fp, fileNamePosBuf, 0, 4); fs.readSync(this.fp, fileNameLenBuf, 0, 4); fs.readSync(this.fp, filePosBuf, 0, 4); fs.readSync(this.fp, fileLenBuf, 0, 8); this.fileNamePos = bytesToInt(Array.from(fileNamePosBuf)); this.fileNameLen = bytesToInt(Array.from(fileNameLenBuf)); this.filePos = bytesToInt(Array.from(filePosBuf)); this.fileLen = bytesToLong(Array.from(fileLenBuf)); let fileNameBuf = new Buffer.alloc(this.fileNameLen); fs.readSync(this.fp, fileNameBuf, 0, this.fileNameLen, this.fileNamePos); result.name = tea.bytesToStr(tea.decryptBytes(Array.from(fileNameBuf), key)); } else { throw new Error("Password is empty!"); } } else { throw new Error(util.format("This file [%s] is not a encrypt file!", this.inPath)); } } fs.closeSync(this.fp); return result; }