├── .gitignore ├── .readme ├── decrypt.png ├── encrypt.png ├── integrity.png └── test.png ├── bin ├── decrypt.sh ├── encrypt.sh ├── get_passphrase.sh ├── test.sh ├── totp.sh └── verify_integrity.sh ├── package.json ├── readme.md └── vault.tar.gz.gpg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vault/ 3 | vault.tar.gz 4 | 5 | -------------------------------------------------------------------------------- /.readme/decrypt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oeo/ward/73811e03a45ba77bc87e277caa0384033ccd91c7/.readme/decrypt.png -------------------------------------------------------------------------------- /.readme/encrypt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oeo/ward/73811e03a45ba77bc87e277caa0384033ccd91c7/.readme/encrypt.png -------------------------------------------------------------------------------- /.readme/integrity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oeo/ward/73811e03a45ba77bc87e277caa0384033ccd91c7/.readme/integrity.png -------------------------------------------------------------------------------- /.readme/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oeo/ward/73811e03a45ba77bc87e277caa0384033ccd91c7/.readme/test.png -------------------------------------------------------------------------------- /bin/decrypt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to securely remove files 4 | secure_remove() { 5 | if command -v shred > /dev/null; then 6 | shred -u "$1" 7 | else 8 | rm -P "$1" 9 | fi 10 | } 11 | 12 | # Prompt for passphrase 13 | #echo "Enter the passphrase for decryption:" 14 | #read -s PASSPHRASE 15 | echo -n "Enter the passphrase for decryption: " 16 | read -s PASSPHRASE 17 | echo >&2 18 | 19 | # Create a temporary file to store the passphrase 20 | PASSPHRASE_FILE=$(mktemp) 21 | echo "$PASSPHRASE" > "$PASSPHRASE_FILE" 22 | 23 | # Decrypt the encrypted tar archive 24 | gpg --batch --passphrase-file "$PASSPHRASE_FILE" --decrypt vault.tar.gz.gpg > vault_with_checksum.tar.gz 25 | 26 | # Check if decryption was successful 27 | if [ $? -ne 0 ]; then 28 | echo "Failed to decrypt archive. Exiting." 29 | secure_remove "$PASSPHRASE_FILE" 30 | exit 1 31 | fi 32 | 33 | # Extract the checksum and the actual tar content 34 | STORED_CHECKSUM=$(head -c 64 vault_with_checksum.tar.gz) 35 | tail -c +65 vault_with_checksum.tar.gz > vault.tar.gz 36 | 37 | # Extract the tar archive 38 | tar -xzf vault.tar.gz 39 | 40 | # Clean up 41 | secure_remove "$PASSPHRASE_FILE" 42 | secure_remove vault_with_checksum.tar.gz 43 | secure_remove vault.tar.gz 44 | 45 | echo "Decryption process completed." 46 | echo "Stored checksum: $STORED_CHECKSUM" 47 | -------------------------------------------------------------------------------- /bin/encrypt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to securely read passphrase 4 | read_passphrase() { 5 | local passphrase passphrase_confirm 6 | 7 | while true; do 8 | echo -n "Enter the passphrase for encryption: " >&2 9 | read -s passphrase 10 | echo >&2 11 | 12 | echo -n "Confirm the passphrase: " >&2 13 | read -s passphrase_confirm 14 | echo >&2 15 | 16 | if [ "$passphrase" = "$passphrase_confirm" ]; then 17 | echo "$passphrase" 18 | return 0 19 | else 20 | echo "Passphrases do not match. Please try again." >&2 21 | fi 22 | done 23 | } 24 | 25 | # Check if the vault directory exists 26 | if [ ! -d "./vault" ]; then 27 | echo "Error: ./vault directory not found." 28 | exit 1 29 | fi 30 | 31 | # Check if there are any files in the vault directory 32 | if [ -z "$(ls -A ./vault)" ]; then 33 | echo "Error: The vault directory is empty." 34 | exit 1 35 | fi 36 | 37 | # Count the number of files in the vault directory 38 | FILE_COUNT=$(find ./vault -type f | wc -l) 39 | 40 | # Read passphrase from stdin if available, otherwise prompt 41 | if [ -t 0 ]; then 42 | PASSPHRASE=$(read_passphrase) 43 | else 44 | read -s PASSPHRASE 45 | fi 46 | 47 | # Verify that we got a passphrase 48 | if [ -z "$PASSPHRASE" ]; then 49 | echo "Error: No passphrase provided. Exiting." 50 | exit 1 51 | fi 52 | 53 | # Generate checksum of the vault directory 54 | VAULT_CHECKSUM=$(find ./vault -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1) 55 | 56 | # Create a tar archive of the vault directory 57 | tar -czf vault_content.tar.gz ./vault 58 | 59 | # Prepend the checksum to the tar archive 60 | (echo -n "$VAULT_CHECKSUM"; cat vault_content.tar.gz) > vault.tar.gz 61 | rm vault_content.tar.gz 62 | 63 | # Encrypt the tar archive (now including the checksum) 64 | echo "$PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \ 65 | --symmetric --cipher-algo AES256 --s2k-mode 3 --s2k-count 65011712 \ 66 | --s2k-digest-algo SHA512 --no-symkey-cache \ 67 | --output vault.tar.gz.gpg vault.tar.gz 68 | 69 | # Check if encryption was successful 70 | if [ $? -eq 0 ]; then 71 | echo -e "Encryption successful: vault.tar.gz.gpg" 72 | # Remove the unencrypted tar file 73 | rm vault.tar.gz 74 | 75 | # Function to get file size in bytes and convert to MB 76 | get_file_size_in_mb() { 77 | local file=$1 78 | local file_size_bytes 79 | local file_size_mb 80 | 81 | # Get file size in bytes using appropriate `stat` syntax for the platform 82 | if [[ "$OSTYPE" == "darwin"* ]]; then 83 | # macOS 84 | file_size_bytes=$(stat -f%z "$file") 85 | else 86 | # Linux and other Unix-like systems 87 | file_size_bytes=$(stat -c%s "$file") 88 | fi 89 | 90 | # Convert bytes to MB with two decimal places 91 | file_size_mb=$(echo "scale=2; $file_size_bytes / 1024 / 1024" | bc) 92 | 93 | echo "$file_size_mb" 94 | } 95 | 96 | ENCRYPTED_SIZE=$(get_file_size_in_mb vault.tar.gz.gpg) 97 | 98 | printf "Files encrypted: %s\n" "$(echo "$FILE_COUNT" | xargs)" 99 | printf "Vault archive size: %s\n" "$(echo "$ENCRYPTED_SIZE" | xargs)mb" 100 | printf "Vault checksum: %s\n" "$(echo "$VAULT_CHECKSUM" | xargs)" 101 | else 102 | echo -e "\nEncryption failed" 103 | exit 1 104 | fi 105 | 106 | -------------------------------------------------------------------------------- /bin/get_passphrase.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the passphrase is stored in an environment variable 4 | if [ -n "$WARD_PASSPHRASE" ]; then 5 | echo "$WARD_PASSPHRASE" 6 | exit 0 7 | fi 8 | 9 | # If not, and we're in an interactive environment, prompt the user 10 | if [ -t 0 ]; then 11 | echo -n "Enter the passphrase for encryption: " >&2 12 | read -s passphrase 13 | echo >&2 14 | echo "$passphrase" 15 | else 16 | echo "Error: WARD_PASSPHRASE environment variable not set in non-interactive mode." >&2 17 | exit 1 18 | fi 19 | 20 | -------------------------------------------------------------------------------- /bin/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors for output 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | NC='\033[0m' # No Color 7 | 8 | # Test function 9 | run_test() { 10 | if eval "$1"; then 11 | echo -e "${GREEN}[PASS]${NC} $2" 12 | else 13 | echo -e "${RED}[FAIL]${NC} $2" 14 | exit 1 15 | fi 16 | } 17 | 18 | # Setup 19 | echo "Setting up test environment..." 20 | TEST_DIR="$(mktemp -d)" 21 | cp bin/encrypt.sh bin/decrypt.sh bin/verify_integrity.sh "$TEST_DIR/" 22 | cd "$TEST_DIR" 23 | mkdir vault 24 | echo "Test content 1" > vault/test1.md 25 | echo "Test content 2" > vault/test2.txt 26 | echo '{"key": "value"}' > vault/data.json 27 | echo "XML Data" > vault/data.xml 28 | 29 | # Use a fixed passphrase for testing 30 | TEST_PASSPHRASE="testpassword123" 31 | 32 | # Test encryption 33 | echo "Testing encryption..." 34 | ENCRYPTION_OUTPUT=$(./encrypt.sh < vault/test1.md 61 | VERIFY_OUTPUT=$(echo "$TEST_PASSPHRASE" | ./verify_integrity.sh) 62 | echo "$VERIFY_OUTPUT" 63 | run_test "echo \"$VERIFY_OUTPUT\" | grep -q 'Integrity check failed'" "Integrity verification fails for modified vault" 64 | 65 | # Restore original content 66 | rm -rf vault 67 | mv vault_original vault 68 | 69 | # Remove decrypted files 70 | rm -rf vault 71 | 72 | # Test decryption with incorrect password 73 | echo "Testing decryption with incorrect password..." 74 | run_test "! ./decrypt.sh <<< 'wrongpassword'" "Decryption script fails with incorrect password" 75 | run_test "[ ! -d vault ]" "Vault directory is not created with wrong password" 76 | 77 | # Cleanup 78 | echo "Cleaning up..." 79 | cd .. 80 | rm -rf "$TEST_DIR" 81 | 82 | echo "All tests completed successfully!" 83 | 84 | -------------------------------------------------------------------------------- /bin/totp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if oathtool is installed 4 | if ! command -v oathtool &> /dev/null; then 5 | echo "Error: oathtool is not installed. Please install oath-toolkit." 6 | exit 1 7 | fi 8 | 9 | # Check if a secret key is provided 10 | if [ $# -eq 0 ]; then 11 | echo "Error: No secret key provided." 12 | echo "Usage: $0 " 13 | exit 1 14 | fi 15 | 16 | # Get the secret key from all arguments 17 | SECRET_KEY="$*" 18 | 19 | # Function to validate and process the secret key 20 | process_secret() { 21 | local secret="$1" 22 | # Remove spaces 23 | secret=$(echo "$secret" | tr -d ' ') 24 | # Convert to uppercase 25 | secret=$(echo "$secret" | tr '[:lower:]' '[:upper:]') 26 | # Check if it's a valid base32 string 27 | if echo "$secret" | grep -qE '^[A-Z2-7]+=*$' && [ ${#secret} -ge 16 ]; then 28 | echo "$secret" 29 | return 0 30 | else 31 | echo "Error: Invalid secret key. It should be a base32 encoded string (with or without spaces)." 32 | return 1 33 | fi 34 | } 35 | 36 | # Process the secret key 37 | PROCESSED_SECRET=$(process_secret "$SECRET_KEY") 38 | if [ $? -ne 0 ]; then 39 | echo "$PROCESSED_SECRET" 40 | exit 1 41 | fi 42 | 43 | # Generate the TOTP code 44 | TOTP_CODE=$(oathtool --totp -b "$PROCESSED_SECRET") 45 | 46 | # Print the TOTP code 47 | echo "Your TOTP code is: $TOTP_CODE" 48 | -------------------------------------------------------------------------------- /bin/verify_integrity.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the vault directory and encrypted file exist 4 | if [ ! -d "./vault" ] || [ ! -f "vault.tar.gz.gpg" ]; then 5 | echo "Error: vault directory or encrypted file not found." 6 | exit 1 7 | fi 8 | 9 | # Generate current checksum of the vault directory 10 | CURRENT_CHECKSUM=$(find ./vault -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -d' ' -f1) 11 | echo "Current vault checksum: $CURRENT_CHECKSUM" 12 | 13 | # Prompt for passphrase 14 | echo -n "Enter the passphrase for decryption: " 15 | read -s PASSPHRASE 16 | echo 17 | 18 | # Decrypt and extract just the checksum from the encrypted file 19 | STORED_CHECKSUM=$(echo "$PASSPHRASE" | gpg --batch --passphrase-fd 0 --decrypt vault.tar.gz.gpg 2>/dev/null | head -c 64) 20 | echo "Stored checksum: $STORED_CHECKSUM" 21 | 22 | # Compare checksums 23 | if [ "$CURRENT_CHECKSUM" = "$STORED_CHECKSUM" ]; then 24 | echo "Integrity check passed: The vault directory matches the encrypted state." 25 | exit 0 26 | else 27 | echo "Integrity check failed: The vault directory has been modified since the last encryption." 28 | exit 1 29 | fi 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ward", 3 | "version": "1.0.0", 4 | "description": "a portable vault using bash and gpg", 5 | "scripts": { 6 | "encrypt": "bash bin/encrypt.sh", 7 | "decrypt": "bash bin/decrypt.sh", 8 | "totp": "bash bin/totp.sh", 9 | "test": "bash bin/test.sh", 10 | "verify": "bash bin/verify_integrity.sh" 11 | }, 12 | "author": "taky@taky.com", 13 | "license": "MIT" 14 | } 15 | 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ward: a personal vault 2 | 3 | ward is a bunch of bash scripts that will keep your secret files safe but also as accessible as you'd like. i designed it to use it dangerously within git. 4 | consider it digital safe for your sensitive files. it encrypts your stuff, keeps it integrity-checked, it is written in bash and it's pretty straightforward: 5 | 6 | - encrypts your files using gpg 7 | - lets you check if someone's messed with your encrypted stuff 8 | - generates totp codes if you're storing those sorts of secrets and need to recover accounts 9 | 10 | ## prereqs 11 | 12 | make sure you've got these installed: 13 | 14 | - gpg 15 | - oath-toolkit (for totp) 16 | - bc (basic math and comes with most systems) 17 | 18 | ## getting started 19 | 20 | 1. install the essentials: 21 | ``` 22 | # ubuntu/debian 23 | sudo apt-get install gnupg oath-toolkit bc 24 | 25 | # osx/homebrew 26 | brew install gnupg oath-toolkit 27 | ``` 28 | 29 | 2. clone the repository 30 | ``` 31 | git clone https://github.com/oeo/ward.git 32 | cd ward 33 | ``` 34 | 35 | 3. decrypt the example vault.tar.gz.gpg 36 | ``` 37 | yarn decrypt # or ./bin/decrypt.sh 38 | ``` 39 | 40 | the default vault decryption password is `letmein`. 41 | 42 | 43 | 44 | ## simple usage 45 | ``` 46 | mkdir vault 47 | echo 123 > vault/123.txt 48 | yarn encrypt # or ./bin/encrypt.sh 49 | ``` 50 | 51 | 52 | 53 | here are the yarn commands you'll be using: 54 | 55 | - `yarn encrypt`: encrypt your vault directory 56 | - `yarn decrypt`: decrypt your encrypted vault file 57 | - `yarn verify`: verify the checksum of your vault 58 | - `yarn totp `: generate a totp code using a secret 59 | - `yarn test`: run unit tests 60 | 61 | ### vaulting your files 62 | 63 | 1. throw whatever you want to encrypt into a folder called `vault` 64 | 2. run `yarn encrypt` 65 | 3. type in a passphrase 66 | 4. boom, you've got yourself an encrypted `vault.tar.gz.gpg` 67 | 68 | ### getting your stuff back 69 | 70 | 1. make sure `vault.tar.gz.gpg` is where it should be 71 | 1. run `yarn decrypt` 72 | 1. enter your passphrase 73 | 1. your files will pop back into the `vault` folder 74 | 75 | ### integrity verification 76 | run `yarn verify` to ensure the archive hasn't been tampered with 77 | 78 | 79 | 80 | ### two-factor auth functionality 81 | ``` 82 | yarn totp 83 | ``` 84 | 85 | ### running tests 86 | ``` 87 | yarn test 88 | ``` 89 | 90 | 91 | 92 | ## notes 93 | 94 | - the `vault` folder doesn't self-destruct after encryption, clean up if you're paranoid 95 | - although it is included in the .gitignore, of course 96 | 97 | ## environment variables 98 | 99 | if you're feeling lazy and a bit risky you can set `WARD_PASSPHRASE` in your environment. 100 | 101 | ## license 102 | 103 | mit 104 | 105 | -------------------------------------------------------------------------------- /vault.tar.gz.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oeo/ward/73811e03a45ba77bc87e277caa0384033ccd91c7/vault.tar.gz.gpg --------------------------------------------------------------------------------