From 27979d58e51c81aa1e89e6f716852a62d3bc581a Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Mon, 20 Mar 2023 23:59:09 +0100 Subject: [PATCH] Added starter implementation. --- .gitignore | 16 ++++++ dub.json | 12 +++++ dub.selections.json | 8 +++ source/app.d | 115 +++++++++++++++++++++++++++++++++++++++++ source/cipher_utils.d | 67 ++++++++++++++++++++++++ test-dir-tpl/bank.json | 6 +++ test-dir-tpl/test.txt | 1 + test-dir/bank.json | 6 +++ test-dir/test.txt | 1 + 9 files changed, 232 insertions(+) create mode 100644 .gitignore create mode 100644 dub.json create mode 100644 dub.selections.json create mode 100644 source/app.d create mode 100644 source/cipher_utils.d create mode 100644 test-dir-tpl/bank.json create mode 100644 test-dir-tpl/test.txt create mode 100644 test-dir/bank.json create mode 100644 test-dir/test.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..484cafc --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.dub +docs.json +__dummy.html +docs/ +/scrambler +scrambler.so +scrambler.dylib +scrambler.dll +scrambler.a +scrambler.lib +scrambler-test-* +*.exe +*.pdb +*.o +*.obj +*.lst diff --git a/dub.json b/dub.json new file mode 100644 index 0000000..98264de --- /dev/null +++ b/dub.json @@ -0,0 +1,12 @@ +{ + "authors": [ + "Andrew Lalis" + ], + "copyright": "Copyright © 2023, Andrew Lalis", + "dependencies": { + "botan": "~>1.13.4" + }, + "description": "Tool for encrypting and decrypting entire directories on-the-fly.", + "license": "MIT", + "name": "scrambler" +} \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..78edccd --- /dev/null +++ b/dub.selections.json @@ -0,0 +1,8 @@ +{ + "fileVersion": 1, + "versions": { + "botan": "1.13.4", + "botan-math": "1.0.4", + "memutils": "1.0.9" + } +} diff --git a/source/app.d b/source/app.d new file mode 100644 index 0000000..8ef73e8 --- /dev/null +++ b/source/app.d @@ -0,0 +1,115 @@ +import std.stdio; +import std.path; +import std.file; +import std.algorithm : endsWith; +import std.string : strip; + +import botan.block.block_cipher : BlockCipher; +import botan.block.aes : AES256; +import botan.hash.hash : HashFunction; +import botan.hash.sha2_32 : SHA256; + +import cipher_utils; + +int main(string[] args) { + string action = "encrypt"; + if (args.length >= 2) { + string command = args[1]; + if (command == "encrypt") { + action = "encrypt"; + } else if (command == "decrypt") { + action = "decrypt"; + } else { + stderr.writefln!"Invalid action: %s"(command); + return 1; + } + } + + string target = "."; + if (args.length >= 3) { + target = args[2]; + if (!exists(target)) { + stderr.writefln!"Target \"%s\" doesn't exist."(target); + return 1; + } + } + + BlockCipher cipher = null; + // TODO: Use args to determine block cipher. + writeln("Enter a password:"); + string password = readln().strip(); + + HashFunction hash = new SHA256(); + auto secureKeyVector = hash.process(password); + cipher = new AES256(); + cipher.setKey(secureKeyVector); + + ubyte[] buffer = new ubyte[cipher.blockSize]; + if (isDir(target)) { + if (action == "encrypt") { + encryptDir(target, cipher, buffer, true); + } else if (action == "decrypt") { + decryptDir(target, cipher, buffer, true); + } else { + stderr.writefln!"Unsupported action: %s"(action); + return 1; + } + } else if (isFile(target)) { + if (action == "encrypt") { + encryptAndRemoveFile(target, cipher, buffer); + } else if (action == "decrypt") { + decryptAndRemoveFile(target, cipher, buffer); + } else { + stderr.writefln!"Unsupported action: %s"(action); + return 1; + } + } else { + stderr.writeln("Target is not a directory or file."); + return 1; + } + return 0; +} + +void encryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer) { + string encryptedFilename = filename ~ ".enc"; + encryptFile(filename, encryptedFilename, cipher, buffer); + std.file.remove(filename); +} + +void decryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer) { + string decryptedFilename = filename[0 .. $-4]; + decryptFile(filename, decryptedFilename, cipher, buffer); + std.file.remove(filename); +} + +void encryptDir(string dirname, BlockCipher cipher, ref ubyte[] buffer, bool recursive) { + string[] dirsToTraverse; + foreach (DirEntry entry; dirEntries(dirname, SpanMode.shallow, false)) { + if (entry.isFile && !endsWith(entry.name, ".enc")) { + encryptAndRemoveFile(entry.name, cipher, buffer); + } else if (entry.isDir && recursive) { + dirsToTraverse ~= entry.name; + } + } + if (recursive) { + foreach (string childDirname; dirsToTraverse) { + encryptDir(childDirname, cipher, buffer, recursive); + } + } +} + +void decryptDir(string dirname, BlockCipher cipher, ref ubyte[] buffer, bool recursive) { + string[] dirsToTraverse; + foreach (DirEntry entry; dirEntries(dirname, SpanMode.shallow, false)) { + if (entry.isFile && endsWith(entry.name, ".enc")) { + decryptAndRemoveFile(entry.name, cipher, buffer); + } else if (entry.isDir && recursive) { + dirsToTraverse ~= entry.name; + } + } + if (recursive) { + foreach (string childDirname; dirsToTraverse) { + decryptDir(childDirname, cipher, buffer, recursive); + } + } +} diff --git a/source/cipher_utils.d b/source/cipher_utils.d new file mode 100644 index 0000000..da20ddc --- /dev/null +++ b/source/cipher_utils.d @@ -0,0 +1,67 @@ +module cipher_utils; + +import botan.block.block_cipher : BlockCipher; +import std.stdio; + +public void encryptFile(string filename, string outputFilename, BlockCipher cipher, ref ubyte[] buffer) { + assert(buffer.length == cipher.blockSize, "Buffer length must match cipher block size."); + File fIn = File(filename, "rb"); + File fOut = File(outputFilename, "wb"); + // First, write one block containing the file's size. + writeSizeBytes(buffer, fIn.size); + cipher.encrypt(buffer); + fOut.rawWrite(buffer); + // Then write the rest of the file. + foreach (ubyte[] chunk; fIn.byChunk(buffer)) { + cipher.encrypt(buffer); + fOut.rawWrite(buffer); + } + fIn.close(); + fOut.close(); +} + +public void decryptFile(string filename, string outputFilename, BlockCipher cipher, ref ubyte[] buffer) { + assert(buffer.length == cipher.blockSize, "Buffer length must match cipher block size."); + File fIn = File(filename, "rb"); + File fOut = File(outputFilename, "wb"); + // First, read one block containing the file's size. + fIn.rawRead(buffer); + cipher.decrypt(buffer); + ulong size = readSizeBytes(buffer); + ulong bytesWritten = 0; + // Then read the rest of the file. + foreach (ubyte[] chunk; fIn.byChunk(buffer)) { + cipher.decrypt(buffer); + size_t bytesToWrite = buffer.length; + if (bytesWritten + buffer.length > size) { + bytesToWrite = cast(size_t) (size - bytesWritten); + } + fOut.rawWrite(buffer[0 .. bytesToWrite]); + bytesWritten += bytesToWrite; + } + fIn.close(); + fOut.close(); +} + +private void writeSizeBytes(ref ubyte[] bytes, ulong size) { + assert(bytes.length >= 4, "Array length must be at least 4."); + bytes[0] = size & 0xFF; + bytes[1] = (size << 8) & 0xFF; + bytes[2] = (size << 16) & 0xFF; + bytes[3] = (size << 24) & 0xFF; + if (bytes.length > 4) { + for (size_t i = 4; i < bytes.length; i++) { + bytes[i] = 0; + } + } +} + +private ulong readSizeBytes(ref ubyte[] bytes) { + assert(bytes.length >= 4, "Array length must be at least 4."); + ulong size = 0; + size += bytes[0]; + size += bytes[1] << 8; + size += bytes[2] << 16; + size += bytes[3] << 24; + return size; +} \ No newline at end of file diff --git a/test-dir-tpl/bank.json b/test-dir-tpl/bank.json new file mode 100644 index 0000000..94171c7 --- /dev/null +++ b/test-dir-tpl/bank.json @@ -0,0 +1,6 @@ +{ + "bank": { + "id": 1234, + "password": "hunter2" + } +} \ No newline at end of file diff --git a/test-dir-tpl/test.txt b/test-dir-tpl/test.txt new file mode 100644 index 0000000..cd08755 --- /dev/null +++ b/test-dir-tpl/test.txt @@ -0,0 +1 @@ +Hello world! diff --git a/test-dir/bank.json b/test-dir/bank.json new file mode 100644 index 0000000..94171c7 --- /dev/null +++ b/test-dir/bank.json @@ -0,0 +1,6 @@ +{ + "bank": { + "id": 1234, + "password": "hunter2" + } +} \ No newline at end of file diff --git a/test-dir/test.txt b/test-dir/test.txt new file mode 100644 index 0000000..cd08755 --- /dev/null +++ b/test-dir/test.txt @@ -0,0 +1 @@ +Hello world!