Cleaned up, added release script.

This commit is contained in:
Andrew Lalis 2023-03-22 08:04:21 +01:00
parent 332a07b7a4
commit c2e86bd374
5 changed files with 89 additions and 40 deletions

View File

@ -1,5 +1,5 @@
# scrambler # 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. 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.

6
build-release.sh Executable file
View File

@ -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

View File

@ -25,21 +25,9 @@ int main(string[] args) {
return result; return result;
} }
string passphrase; auto nullablePassphrase = getPassphrase(params);
if (params.passphraseFile !is null && exists(params.passphraseFile) && isFile(params.passphraseFile)) { if (nullablePassphrase.isNull) return 2;
if (params.verbose) { string passphrase = nullablePassphrase.get();
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;
}
HashFunction hash = new SHA256(); HashFunction hash = new SHA256();
auto secureKeyVector = hash.process(passphrase); auto secureKeyVector = hash.process(passphrase);
@ -53,7 +41,7 @@ int main(string[] args) {
} else { } else {
bool success = decryptDir(params.target, cipher, buffer, params.recursive, params.verbose); bool success = decryptDir(params.target, cipher, buffer, params.recursive, params.verbose);
if (!success) { if (!success) {
stderr.writeln("Decryption failed."); stderr.writeln("Decryption failed. The passphrase is probably incorrect.");
return 3; return 3;
} }
} }
@ -63,7 +51,7 @@ int main(string[] args) {
} else { } else {
bool success = decryptAndRemoveFile(params.target, cipher, buffer, params.verbose); bool success = decryptAndRemoveFile(params.target, cipher, buffer, params.verbose);
if (!success) { if (!success) {
stderr.writeln("Decryption failed."); stderr.writeln("Decryption failed. The passphrase is probably incorrect.");
return 3; return 3;
} }
} }

View File

@ -3,6 +3,7 @@ module cipher_utils;
import botan.block.block_cipher : BlockCipher; import botan.block.block_cipher : BlockCipher;
import std.stdio; import std.stdio;
import std.file; import std.file;
import std.datetime.stopwatch;
public const string ENCRYPTED_SUFFIX = ".enc"; public const string ENCRYPTED_SUFFIX = ".enc";
@ -16,6 +17,7 @@ public void encryptFile(string filename, string outputFilename, BlockCipher ciph
cipher.name cipher.name
); );
} }
StopWatch sw = StopWatch(AutoStart.yes);
File fIn = File(filename, "rb"); File fIn = File(filename, "rb");
File fOut = File(outputFilename, "wb"); File fOut = File(outputFilename, "wb");
// First, write one block containing the file's size. // First, write one block containing the file's size.
@ -36,8 +38,9 @@ public void encryptFile(string filename, string outputFilename, BlockCipher ciph
} }
fIn.close(); fIn.close();
fOut.close(); fOut.close();
sw.stop();
if (verbose) { 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 cipher.name
); );
} }
StopWatch sw = StopWatch(AutoStart.yes);
File fIn = File(filename, "rb"); File fIn = File(filename, "rb");
// First, read one block containing the file's size. // First, read one block containing the file's size.
fIn.rawRead(buffer); fIn.rawRead(buffer);
cipher.decrypt(buffer); cipher.decrypt(buffer);
// Verify the sequence of values to ensure decryption was successful. // Verify the sequence of values to ensure decryption was successful.
if (buffer.length > 8) { if (buffer.length > 8 && !validateBufferDecryptionMarker(buffer[8..$], verbose)) {
ubyte expectedMarker = 1; fIn.close();
for (size_t i = 8; i < buffer.length; i++) { return false;
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++;
}
} }
ulong size = readSizeBytes(buffer); ulong size = readSizeBytes(buffer);
if (verbose) { if (verbose) {
@ -91,8 +82,27 @@ public bool decryptFile(string filename, string outputFilename, BlockCipher ciph
} }
fIn.close(); fIn.close();
fOut.close(); fOut.close();
sw.stop();
if (verbose) { 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; return true;
} }

View File

@ -1,6 +1,7 @@
module cli_utils; module cli_utils;
import cipher_utils : ENCRYPTED_SUFFIX; import cipher_utils : ENCRYPTED_SUFFIX;
import std.typecons;
enum Action { enum Action {
ENCRYPT, ENCRYPT,
@ -12,6 +13,7 @@ struct Params {
string target = "."; string target = ".";
bool recursive = false; bool recursive = false;
bool verbose = false; bool verbose = false;
bool noSuffix = false;
string passphraseFile = null; string passphraseFile = null;
} }
@ -28,6 +30,7 @@ int parseParams(string[] args, ref Params params) {
"verbose|v", &params.verbose, "verbose|v", &params.verbose,
"encrypt|e", &isEncrypting, "encrypt|e", &isEncrypting,
"decrypt|d", &isDecrypting, "decrypt|d", &isDecrypting,
"no-suffix|s", &params.noSuffix,
"passphrase-file|p", &params.passphraseFile "passphrase-file|p", &params.passphraseFile
); );
if (isEncrypting && isDecrypting) { if (isEncrypting && isDecrypting) {
@ -52,7 +55,14 @@ int parseParams(string[] args, ref Params params) {
return 0; 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.file;
import std.algorithm : endsWith; 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; import std.stdio : writeln;
writeln(q"HELP writeln(q"HELP
Usage: scrambler [target] [options] Usage: scrambler [target] [options]
@ -94,7 +107,39 @@ Scrambler will try to determine which operation to do based on the presence of
HELP"); 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.stdio;
import std.string : strip; import std.string : strip;
version(Posix) { version(Posix) {
@ -137,7 +182,7 @@ string readPassphrase() {
SetConsoleMode(hIn, prev_con_mode); SetConsoleMode(hIn, prev_con_mode);
return password; return password;
} else { } 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; return null;
} }
} }