Added reset.sh, cleaned up readme and CLI.

This commit is contained in:
Andrew Lalis 2023-03-21 14:30:09 +01:00
parent 6fd042f594
commit 332a07b7a4
7 changed files with 218 additions and 56 deletions

View File

@ -1,2 +1,45 @@
# scrambler # scrambler
Tool for encrypting and decrypting entire directories on-the-fly. 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.
As of writing, this application uses an AES-256 block cipher, with a passphrase that's hashed by SHA-256.
## Usage
```shell
# Show help information.
scrambler -h
scrambler --help
# Encrypt or decrypt the current directory.
scrambler
# Encrypt or decrypt a specified directory.
scrambler my-dir
# Encrypt or decrypt a single file.
scrambler path/to/my/file.txt
# Encrypt or decrypt a directory recursively.
scrambler my-dir -r
# Add -v for verbose output.
scrambler myfile.txt -v
# Explicitly choose to encrypt with -e.
scrambler my-dir -e
# Explicitly choose to decrypt with -d.
scrambler my-encrypted-dir -d
# Supply a passphrase from a file.
scrambler my-dir -p my-passphrase.txt
```
## Download
You can download a compatible release from the [releases](https://github.com/andrewlalis/scrambler/releases) page. If you can't find any release that's available for your system, you can build it yourself:
```shell
git clone git@github.com:andrewlalis/scrambler.git
cd scrambler
dub build --build=release
```

4
reset.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
rm -rf test-dir
cp -R test-dir-tpl test-dir

View File

@ -14,6 +14,10 @@ import cipher_utils;
import cli_utils; import cli_utils;
int main(string[] args) { int main(string[] args) {
if (args.length >= 2 && (args[1] == "-h" || args[1] == "--help")) {
printUsage();
return 0;
}
Params params; Params params;
int result = parseParams(args, params); int result = parseParams(args, params);
if (result != 0) { if (result != 0) {
@ -21,80 +25,98 @@ int main(string[] args) {
return result; return result;
} }
BlockCipher cipher = null; string passphrase;
// TODO: Use args to determine block cipher. if (params.passphraseFile !is null && exists(params.passphraseFile) && isFile(params.passphraseFile)) {
writeln("Enter a passphrase:"); if (params.verbose) {
string password = readPassphrase(); writefln!"Reading passphrase from \"%s\""(params.passphraseFile);
if (password is null) { }
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; return 2;
} }
HashFunction hash = new SHA256(); HashFunction hash = new SHA256();
auto secureKeyVector = hash.process(password); auto secureKeyVector = hash.process(passphrase);
cipher = new AES256(); BlockCipher cipher = new AES256();
cipher.setKey(secureKeyVector); cipher.setKey(secureKeyVector);
ubyte[] buffer = new ubyte[cipher.blockSize]; ubyte[] buffer = new ubyte[cipher.blockSize];
if (isDir(params.target)) { if (isDir(params.target)) {
if (params.action == Action.ENCRYPT) { if (params.action == Action.ENCRYPT) {
encryptDir(params.target, cipher, buffer, params.recursive); encryptDir(params.target, cipher, buffer, params.recursive, params.verbose);
} else { } else {
decryptDir(params.target, cipher, buffer, params.recursive); bool success = decryptDir(params.target, cipher, buffer, params.recursive, params.verbose);
if (!success) {
stderr.writeln("Decryption failed.");
return 3;
}
} }
} else if (isFile(params.target)) { } else if (isFile(params.target)) {
if (params.action == Action.ENCRYPT) { if (params.action == Action.ENCRYPT) {
encryptAndRemoveFile(params.target, cipher, buffer); encryptAndRemoveFile(params.target, cipher, buffer, params.verbose);
} else { } else {
decryptAndRemoveFile(params.target, cipher, buffer); bool success = decryptAndRemoveFile(params.target, cipher, buffer, params.verbose);
if (!success) {
stderr.writeln("Decryption failed.");
return 3;
}
} }
} else {
stderr.writeln("Target is not a directory or file.");
return 1;
} }
return 0; return 0;
} }
void encryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer) { void encryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer, bool verbose) {
string encryptedFilename = filename ~ ".enc"; string encryptedFilename = filename ~ ENCRYPTED_SUFFIX;
encryptFile(filename, encryptedFilename, cipher, buffer); encryptFile(filename, encryptedFilename, cipher, buffer, verbose);
std.file.remove(filename); std.file.remove(filename);
} }
void decryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer) { bool decryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer, bool verbose) {
string decryptedFilename = filename[0 .. $-4]; string decryptedFilename = filename[0 .. $-ENCRYPTED_SUFFIX.length];
decryptFile(filename, decryptedFilename, cipher, buffer); bool success = decryptFile(filename, decryptedFilename, cipher, buffer, verbose);
if (!success) return false;
std.file.remove(filename); std.file.remove(filename);
return true;
} }
void encryptDir(string dirname, BlockCipher cipher, ref ubyte[] buffer, bool recursive) { void encryptDir(string dirname, BlockCipher cipher, ref ubyte[] buffer, bool recursive, bool verbose) {
string[] dirsToTraverse; string[] dirsToTraverse;
foreach (DirEntry entry; dirEntries(dirname, SpanMode.shallow, false)) { foreach (DirEntry entry; dirEntries(dirname, SpanMode.shallow, false)) {
if (entry.isFile && !endsWith(entry.name, ".enc")) { if (entry.isFile && !endsWith(entry.name, ENCRYPTED_SUFFIX)) {
encryptAndRemoveFile(entry.name, cipher, buffer); encryptAndRemoveFile(entry.name, cipher, buffer, verbose);
} else if (entry.isDir && recursive) { } else if (entry.isDir && recursive) {
dirsToTraverse ~= entry.name; dirsToTraverse ~= entry.name;
} }
} }
if (recursive) { if (recursive) {
foreach (string childDirname; dirsToTraverse) { foreach (string childDirname; dirsToTraverse) {
encryptDir(childDirname, cipher, buffer, recursive); encryptDir(childDirname, cipher, buffer, recursive, verbose);
} }
} }
} }
void decryptDir(string dirname, BlockCipher cipher, ref ubyte[] buffer, bool recursive) { bool decryptDir(string dirname, BlockCipher cipher, ref ubyte[] buffer, bool recursive, bool verbose) {
string[] dirsToTraverse; string[] dirsToTraverse;
foreach (DirEntry entry; dirEntries(dirname, SpanMode.shallow, false)) { foreach (DirEntry entry; dirEntries(dirname, SpanMode.shallow, false)) {
if (entry.isFile && endsWith(entry.name, ".enc")) { if (entry.isFile && endsWith(entry.name, ENCRYPTED_SUFFIX)) {
decryptAndRemoveFile(entry.name, cipher, buffer); bool success = decryptAndRemoveFile(entry.name, cipher, buffer, verbose);
if (!success) return false;
} else if (entry.isDir && recursive) { } else if (entry.isDir && recursive) {
dirsToTraverse ~= entry.name; dirsToTraverse ~= entry.name;
} }
} }
if (recursive) { if (recursive) {
foreach (string childDirname; dirsToTraverse) { foreach (string childDirname; dirsToTraverse) {
decryptDir(childDirname, cipher, buffer, recursive); bool success = decryptDir(childDirname, cipher, buffer, recursive, verbose);
if (!success) return false;
} }
} }
return true;
} }

View File

@ -2,13 +2,31 @@ module cipher_utils;
import botan.block.block_cipher : BlockCipher; import botan.block.block_cipher : BlockCipher;
import std.stdio; import std.stdio;
import std.file;
public void encryptFile(string filename, string outputFilename, BlockCipher cipher, ref ubyte[] buffer) { public const string ENCRYPTED_SUFFIX = ".enc";
public void encryptFile(string filename, string outputFilename, BlockCipher cipher, ref ubyte[] buffer, bool verbose) {
assert(buffer.length == cipher.blockSize, "Buffer length must match cipher block size."); assert(buffer.length == cipher.blockSize, "Buffer length must match cipher block size.");
if (verbose) {
writefln!"Encrypting file \"%s\" of %d bytes to \"%s\" using cipher %s."(
filename,
getSize(filename),
outputFilename,
cipher.name
);
}
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.
writeSizeBytes(buffer, fIn.size); writeSizeBytes(buffer, fIn.size);
// Fill the rest of the block with an incrementing series of bytes, so we can easily validate decryption.
if (buffer.length > 8) {
ubyte marker = 1;
for (size_t i = 8; i < buffer.length; i++) {
buffer[i] = marker++;
}
}
cipher.encrypt(buffer); cipher.encrypt(buffer);
fOut.rawWrite(buffer); fOut.rawWrite(buffer);
// Then write the rest of the file. // Then write the rest of the file.
@ -18,17 +36,49 @@ public void encryptFile(string filename, string outputFilename, BlockCipher ciph
} }
fIn.close(); fIn.close();
fOut.close(); fOut.close();
if (verbose) {
writefln!" Encrypted file has a size of %d bytes."(getSize(outputFilename));
}
} }
public void decryptFile(string filename, string outputFilename, BlockCipher cipher, ref ubyte[] buffer) { public bool decryptFile(string filename, string outputFilename, BlockCipher cipher, ref ubyte[] buffer, bool verbose) {
assert(buffer.length == cipher.blockSize, "Buffer length must match cipher block size."); assert(buffer.length == cipher.blockSize, "Buffer length must match cipher block size.");
if (verbose) {
writefln!"Decrypting file \"%s\" of %d bytes to \"%s\" using cipher %s."(
filename,
getSize(filename),
outputFilename,
cipher.name
);
}
File fIn = File(filename, "rb"); File fIn = File(filename, "rb");
File fOut = File(outputFilename, "wb");
// 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.
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++;
}
}
ulong size = readSizeBytes(buffer); ulong size = readSizeBytes(buffer);
if (verbose) {
writefln!" Original file had size of %d bytes."(size);
}
ulong bytesWritten = 0; ulong bytesWritten = 0;
File fOut = File(outputFilename, "wb");
// Then read the rest of the file. // Then read the rest of the file.
foreach (ubyte[] chunk; fIn.byChunk(buffer)) { foreach (ubyte[] chunk; fIn.byChunk(buffer)) {
cipher.decrypt(buffer); cipher.decrypt(buffer);
@ -41,27 +91,48 @@ public void decryptFile(string filename, string outputFilename, BlockCipher ciph
} }
fIn.close(); fIn.close();
fOut.close(); fOut.close();
if (verbose) {
writefln!" Decrypted file has a size of %d bytes."(getSize(outputFilename));
}
return true;
}
union LongByteArrayUnion {
ulong longValue;
ubyte[8] bytes;
} }
private void writeSizeBytes(ref ubyte[] bytes, ulong size) { private void writeSizeBytes(ref ubyte[] bytes, ulong size) {
assert(bytes.length >= 4, "Array length must be at least 4."); assert(bytes.length >= 8, "Array length must be at least 8.");
bytes[0] = size & 0xFF; LongByteArrayUnion u;
bytes[1] = (size << 8) & 0xFF; u.longValue = size;
bytes[2] = (size << 16) & 0xFF; for (size_t i = 0; i < 8; i++) bytes[i] = u.bytes[i];
bytes[3] = (size << 24) & 0xFF; if (bytes.length > 8) {
if (bytes.length > 4) { for (size_t i = 8; i < bytes.length; i++) {
for (size_t i = 4; i < bytes.length; i++) {
bytes[i] = 0; bytes[i] = 0;
} }
} }
} }
private ulong readSizeBytes(ref ubyte[] bytes) { private ulong readSizeBytes(ref ubyte[] bytes) {
assert(bytes.length >= 4, "Array length must be at least 4."); assert(bytes.length >= 8, "Array length must be at least 8.");
ulong size = 0; LongByteArrayUnion u;
size += bytes[0]; for (size_t i = 0; i < 8; i++) u.bytes[i] = bytes[i];
size += bytes[1] << 8; return u.longValue;
size += bytes[2] << 16; }
size += bytes[3] << 24;
return size; unittest {
ubyte[] buffer = new ubyte[16];
void doAssert(ulong size) {
import std.format;
writeSizeBytes(buffer, size);
ulong r = readSizeBytes(buffer);
assert(r == size, format!"Value read: %d does not match expected: %d."(r, size));
}
doAssert(0);
doAssert(1);
doAssert(42);
doAssert(74_092_382_742_030);
} }

View File

@ -1,5 +1,7 @@
module cli_utils; module cli_utils;
import cipher_utils : ENCRYPTED_SUFFIX;
enum Action { enum Action {
ENCRYPT, ENCRYPT,
DECRYPT DECRYPT
@ -10,6 +12,7 @@ struct Params {
string target = "."; string target = ".";
bool recursive = false; bool recursive = false;
bool verbose = false; bool verbose = false;
string passphraseFile = null;
} }
int parseParams(string[] args, ref Params params) { int parseParams(string[] args, ref Params params) {
@ -24,19 +27,12 @@ int parseParams(string[] args, ref Params params) {
"recursive|r", &params.recursive, "recursive|r", &params.recursive,
"verbose|v", &params.verbose, "verbose|v", &params.verbose,
"encrypt|e", &isEncrypting, "encrypt|e", &isEncrypting,
"decrypt|d", &isDecrypting "decrypt|d", &isDecrypting,
"passphrase-file|p", &params.passphraseFile
); );
if (isEncrypting && isDecrypting) { if (isEncrypting && isDecrypting) {
stderr.writeln("Invalid arguments: Cannot specify both the --encrypt and --decrypt flags."); stderr.writeln("Invalid arguments: Cannot specify both the --encrypt and --decrypt flags.");
} }
if (isEncrypting) {
params.action = Action.ENCRYPT;
} else if (isDecrypting) {
params.action = Action.DECRYPT;
} else {
writeln("Determining if we should encrypt or decrypt");
params.action = Action.ENCRYPT;
}
if (args.length > 1) { if (args.length > 1) {
params.target = args[1]; params.target = args[1];
@ -45,9 +41,33 @@ int parseParams(string[] args, ref Params params) {
return 1; return 1;
} }
} }
if (isEncrypting) {
params.action = Action.ENCRYPT;
} else if (isDecrypting) {
params.action = Action.DECRYPT;
} else {
params.action = determineBestAction(params.target);
}
return 0; return 0;
} }
Action determineBestAction(string target) {
import std.file;
import std.algorithm : endsWith;
if (isFile(target)) {
return endsWith(target, ENCRYPTED_SUFFIX) ? Action.DECRYPT : Action.ENCRYPT;
} else if (isDir(target)) {
foreach (DirEntry entry; dirEntries(target, SpanMode.breadth, false)) {
if (entry.isFile && endsWith(entry.name, ENCRYPTED_SUFFIX)) return Action.DECRYPT;
}
return Action.ENCRYPT;
} else {
return Action.ENCRYPT;
}
}
void printUsage() { void printUsage() {
import std.stdio : writeln; import std.stdio : writeln;
writeln(q"HELP writeln(q"HELP
@ -60,6 +80,9 @@ The following options are available:
directory. directory.
-d | --decrypt Do a decryption operation on the target file or -d | --decrypt Do a decryption operation on the target file or
directory. directory.
-p | --passphrase-file A file to read the passphrase from, instead of
prompting the user for a passphrase in the command
line.
-r | --recursive Recursively encrypt/decrypt nested directories. -r | --recursive Recursively encrypt/decrypt nested directories.
-v | --verbose Show verbose output during runtime. -v | --verbose Show verbose output during runtime.
-s | --no-suffix Do not add the ".enc" suffix to files. -s | --no-suffix Do not add the ".enc" suffix to files.
@ -67,8 +90,7 @@ The following options are available:
Encrypted files are suffixed with ".enc" to indicate that they're encrypted and Encrypted files are suffixed with ".enc" to indicate that they're encrypted and
cannot be read as usual. If neither --encrypt nor --decrypt flags are provided, cannot be read as usual. If neither --encrypt nor --decrypt flags are provided,
Scrambler will try to determine which operation to do based on the presence of Scrambler will try to determine which operation to do based on the presence of
".enc" file(s) at the target location. It's recommended that you always do ".enc" file(s) at the target location.
provide an explicit --encrypt or --decrypt flag.
HELP"); HELP");
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB