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
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;
int main(string[] args) {
if (args.length >= 2 && (args[1] == "-h" || args[1] == "--help")) {
printUsage();
return 0;
}
Params params;
int result = parseParams(args, params);
if (result != 0) {
@ -21,80 +25,98 @@ int main(string[] args) {
return result;
}
BlockCipher cipher = null;
// TODO: Use args to determine block cipher.
writeln("Enter a passphrase:");
string password = readPassphrase();
if (password is null) {
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;
}
HashFunction hash = new SHA256();
auto secureKeyVector = hash.process(password);
cipher = new AES256();
auto secureKeyVector = hash.process(passphrase);
BlockCipher cipher = new AES256();
cipher.setKey(secureKeyVector);
ubyte[] buffer = new ubyte[cipher.blockSize];
if (isDir(params.target)) {
if (params.action == Action.ENCRYPT) {
encryptDir(params.target, cipher, buffer, params.recursive);
encryptDir(params.target, cipher, buffer, params.recursive, params.verbose);
} 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)) {
if (params.action == Action.ENCRYPT) {
encryptAndRemoveFile(params.target, cipher, buffer);
encryptAndRemoveFile(params.target, cipher, buffer, params.verbose);
} 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;
}
void encryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer) {
string encryptedFilename = filename ~ ".enc";
encryptFile(filename, encryptedFilename, cipher, buffer);
void encryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer, bool verbose) {
string encryptedFilename = filename ~ ENCRYPTED_SUFFIX;
encryptFile(filename, encryptedFilename, cipher, buffer, verbose);
std.file.remove(filename);
}
void decryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer) {
string decryptedFilename = filename[0 .. $-4];
decryptFile(filename, decryptedFilename, cipher, buffer);
bool decryptAndRemoveFile(string filename, BlockCipher cipher, ref ubyte[] buffer, bool verbose) {
string decryptedFilename = filename[0 .. $-ENCRYPTED_SUFFIX.length];
bool success = decryptFile(filename, decryptedFilename, cipher, buffer, verbose);
if (!success) return false;
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;
foreach (DirEntry entry; dirEntries(dirname, SpanMode.shallow, false)) {
if (entry.isFile && !endsWith(entry.name, ".enc")) {
encryptAndRemoveFile(entry.name, cipher, buffer);
if (entry.isFile && !endsWith(entry.name, ENCRYPTED_SUFFIX)) {
encryptAndRemoveFile(entry.name, cipher, buffer, verbose);
} else if (entry.isDir && recursive) {
dirsToTraverse ~= entry.name;
}
}
if (recursive) {
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;
foreach (DirEntry entry; dirEntries(dirname, SpanMode.shallow, false)) {
if (entry.isFile && endsWith(entry.name, ".enc")) {
decryptAndRemoveFile(entry.name, cipher, buffer);
if (entry.isFile && endsWith(entry.name, ENCRYPTED_SUFFIX)) {
bool success = decryptAndRemoveFile(entry.name, cipher, buffer, verbose);
if (!success) return false;
} else if (entry.isDir && recursive) {
dirsToTraverse ~= entry.name;
}
}
if (recursive) {
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 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.");
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 fOut = File(outputFilename, "wb");
// First, write one block containing the file's 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);
fOut.rawWrite(buffer);
// Then write the rest of the file.
@ -18,17 +36,49 @@ public void encryptFile(string filename, string outputFilename, BlockCipher ciph
}
fIn.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.");
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 fOut = File(outputFilename, "wb");
// 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++;
}
}
ulong size = readSizeBytes(buffer);
if (verbose) {
writefln!" Original file had size of %d bytes."(size);
}
ulong bytesWritten = 0;
File fOut = File(outputFilename, "wb");
// Then read the rest of the file.
foreach (ubyte[] chunk; fIn.byChunk(buffer)) {
cipher.decrypt(buffer);
@ -41,27 +91,48 @@ public void decryptFile(string filename, string outputFilename, BlockCipher ciph
}
fIn.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) {
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++) {
assert(bytes.length >= 8, "Array length must be at least 8.");
LongByteArrayUnion u;
u.longValue = size;
for (size_t i = 0; i < 8; i++) bytes[i] = u.bytes[i];
if (bytes.length > 8) {
for (size_t i = 8; 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;
assert(bytes.length >= 8, "Array length must be at least 8.");
LongByteArrayUnion u;
for (size_t i = 0; i < 8; i++) u.bytes[i] = bytes[i];
return u.longValue;
}
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;
import cipher_utils : ENCRYPTED_SUFFIX;
enum Action {
ENCRYPT,
DECRYPT
@ -10,6 +12,7 @@ struct Params {
string target = ".";
bool recursive = false;
bool verbose = false;
string passphraseFile = null;
}
int parseParams(string[] args, ref Params params) {
@ -24,19 +27,12 @@ int parseParams(string[] args, ref Params params) {
"recursive|r", &params.recursive,
"verbose|v", &params.verbose,
"encrypt|e", &isEncrypting,
"decrypt|d", &isDecrypting
"decrypt|d", &isDecrypting,
"passphrase-file|p", &params.passphraseFile
);
if (isEncrypting && isDecrypting) {
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) {
params.target = args[1];
@ -45,9 +41,33 @@ int parseParams(string[] args, ref Params params) {
return 1;
}
}
if (isEncrypting) {
params.action = Action.ENCRYPT;
} else if (isDecrypting) {
params.action = Action.DECRYPT;
} else {
params.action = determineBestAction(params.target);
}
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() {
import std.stdio : writeln;
writeln(q"HELP
@ -60,6 +80,9 @@ The following options are available:
directory.
-d | --decrypt Do a decryption operation on the target file or
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.
-v | --verbose Show verbose output during runtime.
-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
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
".enc" file(s) at the target location. It's recommended that you always do
provide an explicit --encrypt or --decrypt flag.
".enc" file(s) at the target location.
HELP");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB