diff --git a/README.md b/README.md index d0b24c7..9f6f641 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # scrambler -Tool for encrypting and decrypting entire directories on-the-fly. +A tool for encrypting and decrypting entire directories on-the-fly. scrambler uses a block cipher and passphrase to encrypt and decrypt files in-place, as a means for quickly securing content without having to move things and/or create archives. scrambler appends a `.enc` extension to encrypted files, so it can use context clues to determine whether you want to perform encryption or decryption. diff --git a/build-release.sh b/build-release.sh new file mode 100755 index 0000000..ce3d5a0 --- /dev/null +++ b/build-release.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +dub clean +rm -f scrambler + +dub build --non-interactive --build=release --color=on --compiler=/home/andrew/Downloads/ldc2-1.32.0-linux-x86_64/bin/ldc2 diff --git a/source/app.d b/source/app.d index b29bb90..f4e2417 100644 --- a/source/app.d +++ b/source/app.d @@ -25,21 +25,9 @@ int main(string[] args) { return result; } - string passphrase; - if (params.passphraseFile !is null && exists(params.passphraseFile) && isFile(params.passphraseFile)) { - if (params.verbose) { - writefln!"Reading passphrase from \"%s\""(params.passphraseFile); - } - passphrase = readText(params.passphraseFile).strip(); - } else { - write("Enter passphrase: "); - passphrase = readPassphrase(); - writeln(); - } - if (passphrase is null || passphrase.length == 0) { - stderr.writeln("Invalid or missing passphrase."); - return 2; - } + auto nullablePassphrase = getPassphrase(params); + if (nullablePassphrase.isNull) return 2; + string passphrase = nullablePassphrase.get(); HashFunction hash = new SHA256(); auto secureKeyVector = hash.process(passphrase); @@ -53,7 +41,7 @@ int main(string[] args) { } else { bool success = decryptDir(params.target, cipher, buffer, params.recursive, params.verbose); if (!success) { - stderr.writeln("Decryption failed."); + stderr.writeln("Decryption failed. The passphrase is probably incorrect."); return 3; } } @@ -63,7 +51,7 @@ int main(string[] args) { } else { bool success = decryptAndRemoveFile(params.target, cipher, buffer, params.verbose); if (!success) { - stderr.writeln("Decryption failed."); + stderr.writeln("Decryption failed. The passphrase is probably incorrect."); return 3; } } diff --git a/source/cipher_utils.d b/source/cipher_utils.d index a4e3c4c..9b637a4 100644 --- a/source/cipher_utils.d +++ b/source/cipher_utils.d @@ -3,6 +3,7 @@ module cipher_utils; import botan.block.block_cipher : BlockCipher; import std.stdio; import std.file; +import std.datetime.stopwatch; public const string ENCRYPTED_SUFFIX = ".enc"; @@ -16,6 +17,7 @@ public void encryptFile(string filename, string outputFilename, BlockCipher ciph cipher.name ); } + StopWatch sw = StopWatch(AutoStart.yes); File fIn = File(filename, "rb"); File fOut = File(outputFilename, "wb"); // First, write one block containing the file's size. @@ -36,8 +38,9 @@ public void encryptFile(string filename, string outputFilename, BlockCipher ciph } fIn.close(); fOut.close(); + sw.stop(); if (verbose) { - writefln!" Encrypted file has a size of %d bytes."(getSize(outputFilename)); + writefln!" Encrypted file in %d ms, new size %d bytes."(sw.peek().total!"msecs", getSize(outputFilename)); } } @@ -51,27 +54,15 @@ public bool decryptFile(string filename, string outputFilename, BlockCipher ciph cipher.name ); } + StopWatch sw = StopWatch(AutoStart.yes); File fIn = File(filename, "rb"); // First, read one block containing the file's size. fIn.rawRead(buffer); cipher.decrypt(buffer); // Verify the sequence of values to ensure decryption was successful. - if (buffer.length > 8) { - ubyte expectedMarker = 1; - for (size_t i = 8; i < buffer.length; i++) { - if (buffer[i] != expectedMarker) { - if (verbose) { - writefln!" Decryption validation failed. Expected byte at index %d to be %d, but got %d."( - i, - expectedMarker, - buffer[i] - ); - } - fIn.close(); - return false; - } - expectedMarker++; - } + if (buffer.length > 8 && !validateBufferDecryptionMarker(buffer[8..$], verbose)) { + fIn.close(); + return false; } ulong size = readSizeBytes(buffer); if (verbose) { @@ -91,8 +82,27 @@ public bool decryptFile(string filename, string outputFilename, BlockCipher ciph } fIn.close(); fOut.close(); + sw.stop(); if (verbose) { - writefln!" Decrypted file has a size of %d bytes."(getSize(outputFilename)); + writefln!" Decrypted file in %d ms, new size %d bytes."(sw.peek().total!"msecs", getSize(outputFilename)); + } + return true; +} + +private bool validateBufferDecryptionMarker(ubyte[] bufferSlice, bool verbose) { + ubyte expectedMarker = 1; + for (size_t i = 0; i < bufferSlice.length; i++) { + if (bufferSlice[i] != expectedMarker) { + if (verbose) { + writefln!" Decryption validation failed. Expected byte at index %d to be %d, but got %d."( + i, + expectedMarker, + bufferSlice[i] + ); + } + return false; + } + expectedMarker++; } return true; } diff --git a/source/cli_utils.d b/source/cli_utils.d index 13fc4fa..95d7d39 100644 --- a/source/cli_utils.d +++ b/source/cli_utils.d @@ -1,6 +1,7 @@ module cli_utils; import cipher_utils : ENCRYPTED_SUFFIX; +import std.typecons; enum Action { ENCRYPT, @@ -12,6 +13,7 @@ struct Params { string target = "."; bool recursive = false; bool verbose = false; + bool noSuffix = false; string passphraseFile = null; } @@ -28,6 +30,7 @@ int parseParams(string[] args, ref Params params) { "verbose|v", ¶ms.verbose, "encrypt|e", &isEncrypting, "decrypt|d", &isDecrypting, + "no-suffix|s", ¶ms.noSuffix, "passphrase-file|p", ¶ms.passphraseFile ); if (isEncrypting && isDecrypting) { @@ -52,7 +55,14 @@ int parseParams(string[] args, ref Params params) { return 0; } -Action determineBestAction(string target) { +/** + * Determines the most appropriate action to perform on a target file or + * directory, based on its contents. + * Params: + * target = The target to check. + * Returns: Whether to encrypt or decrypt. + */ +private Action determineBestAction(string target) { import std.file; import std.algorithm : endsWith; @@ -68,7 +78,10 @@ Action determineBestAction(string target) { } } -void printUsage() { +/** + * Prints a standard usage/help message to stdout. + */ +public void printUsage() { import std.stdio : writeln; writeln(q"HELP Usage: scrambler [target] [options] @@ -94,7 +107,39 @@ Scrambler will try to determine which operation to do based on the presence of HELP"); } -string readPassphrase() { +public Nullable!string getPassphrase(Params params) { + import std.file; + import std.stdio; + import std.string : strip; + if (params.passphraseFile !is null && params.passphraseFile.length > 0) { + if (exists(params.passphraseFile) && isFile(params.passphraseFile)) { + if (params.verbose) { + writefln!"Reading passphrase from \"%s\""(params.passphraseFile); + } + return nullable(readText(params.passphraseFile).strip()); + } else { + stderr.writefln!"Invalid or missing passphrase file: \"%s\""(params.passphraseFile); + return Nullable!string.init; + } + } + + // No passphrase file specified, so read from stdin. + write("Enter passphrase: "); + string passphrase = readPassphrase(); + writeln(); + if (passphrase is null || passphrase.length == 0) { + stderr.writeln("Invalid or missing passphrase."); + return Nullable!string.init; + } + return nullable(passphrase); +} + +/** + * Reads a passphrase from stdin while disabling terminal echoing, so that the + * entered password does not appear. + * Returns: The passphrase string, or null if we could not securely read it. + */ +public string readPassphrase() { import std.stdio; import std.string : strip; version(Posix) { @@ -137,7 +182,7 @@ string readPassphrase() { SetConsoleMode(hIn, prev_con_mode); return password; } else { - stderr.writeln("Cannot securely read password from terminal."); + stderr.writeln("Cannot securely read password from terminal. Please supply a passphrase file with -p."); return null; } }