Cleaned up, added release script.
This commit is contained in:
parent
332a07b7a4
commit
c2e86bd374
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
22
source/app.d
22
source/app.d
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,28 +54,16 @@ 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;
|
|
||||||
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();
|
fIn.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
expectedMarker++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ulong size = readSizeBytes(buffer);
|
ulong size = readSizeBytes(buffer);
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
writefln!" Original file had size of %d bytes."(size);
|
writefln!" Original file had size of %d bytes."(size);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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", ¶ms.verbose,
|
"verbose|v", ¶ms.verbose,
|
||||||
"encrypt|e", &isEncrypting,
|
"encrypt|e", &isEncrypting,
|
||||||
"decrypt|d", &isDecrypting,
|
"decrypt|d", &isDecrypting,
|
||||||
|
"no-suffix|s", ¶ms.noSuffix,
|
||||||
"passphrase-file|p", ¶ms.passphraseFile
|
"passphrase-file|p", ¶ms.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue