├── .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
--------------------------------------------------------------------------------