Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,19 +272,19 @@ Explanation:

## Computational Cost Estimation for Brute-Force Recovery

### Coercion-Resistant Vault Example (24-bit second preimage)
### Coercion-Resistant Vault Example (26-bit second preimage)

The `scripts/complex-scheme.sh` example uses these parameters for the second layer:
- **Bit length**: 24 bits (16,777,216 possible values, but only 8,388,608 candidates since MSB=1)
- **Bit length**: 26 bits (67,108,864 possible values, but only 33,554,432 candidates since MSB=1)
- **Argon2id parameters**: 7 iterations, 4GB memory
- **Expected derivation time**: ~30 seconds per attempt (on typical hardware)

### Time Requirements

From the benchmark table above, brute-forcing a 24-bit preimage requires:
- **16 threads** (desktop): 182 days worst-case
- **128 threads** (single large instance): ~22.7 days expected
- **2048 threads** (distributed cluster): 1.4 days expected, 6.5 days at 99th percentile
From the benchmark table above, brute-forcing a 26-bit preimage requires:
- **16 threads** (desktop): 1 year 363 days worst-case, 364 days expected
- **128 threads** (single large instance): ~91 days expected
- **2048 threads** (distributed cluster): 5.7 days expected, 26 days at 99th percentile

### Cloud Computing Cost Analysis

Expand All @@ -295,37 +295,37 @@ For memory-hard operations requiring 4GB per thread:
- Instance type: `r6i.32xlarge` (128 vCPUs, 1024 GB RAM)
- Can run 128 parallel threads (1 per vCPU, each using 4GB RAM)
- Cost: ~$8.06/hour
- Time needed: ~22.7 days (with 128 threads)
- **Total cost: ~$4,390**
- Time needed: ~91 days (with 128 threads)
- **Total cost: ~$17,600**

**Option 2: Compute-Optimized Cluster**
- Instance type: `c6i.4xlarge` (16 vCPUs, 32 GB RAM)
- Can run 8 parallel threads (limited by RAM: 32GB/4GB = 8)
- Cost: ~$0.68/hour per instance
- Need 256 instances for 2048 threads
- Time needed: ~1.4 days expected
- **Total cost: ~$5,850** (expected case)
- **Total cost: ~$27,000** (99th percentile, 6.5 days)
- Time needed: ~5.7 days expected
- **Total cost: ~$23,800** (expected case)
- **Total cost: ~$108,500** (99th percentile, 26 days)

**Option 3: Spot Instances**
- Using spot pricing can reduce costs by 60-90%
- Less reliable, may be interrupted
- **Estimated cost: $1,000-$10,000** depending on availability
- **Estimated cost: $4,000-$40,000** depending on availability

#### Other Cloud Providers

**Google Cloud Platform**
- `n2-highmem-128` (128 vCPUs, 864 GB RAM)
- Can run 128 parallel threads (1 per vCPU)
- Cost: ~$6.74/hour
- Time needed: ~22.7 days
- **Total cost: ~$3,670**
- Time needed: ~91 days
- **Total cost: ~$14,700**

**Local Hardware Investment**
- 64-core AMD Threadripper: ~$4,000
- 256GB RAM: ~$1,000
- Can run 64 threads continuously
- Time: ~45 days
- Time: ~182 days (6 months)
- **One-time cost: ~$5,000** (reusable hardware)

### Cost Factors to Consider
Expand All @@ -334,13 +334,13 @@ For memory-hard operations requiring 4GB per thread:
2. **Memory Requirements**: Each thread needs 4GB RAM, which can limit thread count on lower-memory instances
3. **Spot vs On-Demand**: Spot instances can reduce costs by 60-90% but may be interrupted
4. **Coordination Overhead**: Managing 2048 threads across 256+ machines requires significant orchestration
5. **Electricity Costs**: For local hardware, add ~$200-500 for 45 days of operation
5. **Electricity Costs**: For local hardware, add ~$800-2,000 for 182 days of operation

### Conclusion

Realistic cost estimates for brute-forcing a 24-bit preimage:
- **Budget approach**: $1,000-$5,000 using spot instances or local hardware
- **Fast approach**: $6,000-$30,000 for on-demand cloud computing
Realistic cost estimates for brute-forcing a 26-bit preimage:
- **Budget approach**: $5,000-$20,000 using local hardware or spot instances
- **Fast approach**: $24,000-$110,000 for on-demand cloud computing
- **Worst case**: Higher costs if extremely unlucky (99.9th percentile)

These costs make brute-force recovery feasible for high-value assets while remaining prohibitively expensive for casual attackers. The actual cost depends heavily on:
Expand Down
59 changes: 39 additions & 20 deletions scripts/complex-scheme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,67 @@

set -Eeuo pipefail

# The first preimage requires patience to derive and is impossible to brute-force
OUTPUT_DIR="${1:-complex-example-output}"
SALT="${2:-./salt}"
SECOND_PREIMAGE_NBITS="${3:-26}"
DAYS="${4:-30}"

# 32 bytes ought to be enough for anyone
KEYFILE_SIZE="32"

# The first preimage requires patience to derive and is impossible to brute-force.
# There is no good reason to use a low value here
FIRST_PREIMAGE_NBITS=63
# if OPS=10 takes 60s, then for 30 days:
# >>> 30 * 24 * 3600 / 60 * 10
# 432000
MONTHS_LONG_OPS=432000
# Calculate ops based on input days parameter
# if OPS=10 takes 60s, then for N days:
# N * 24 * 3600 / 60 * 10 = N * 14400
MONTHS_LONG_OPS=$((DAYS * 14400))
# This is 16GB
LARGE_MEM_LIMIT_KBYTES=16777216

# The second preimage is easy to derive but requires money/time to brute-force
SECOND_PREIMAGE_NBITS=24
SECONDS_LONG_OPS=7
# This is 4GB
SMALL_MEM_LIMIT_KBYTES=4194304

if [[ ! -f "$SALT" ]]; then
echo "Salt file not found"
exit 1
fi

if [[ $SECOND_PREIMAGE_NBITS -lt 20 ]]; then
echo "Warning: low second preimage nbits ($SECOND_PREIMAGE_NBITS) may make it easy to brute-force"
fi

mkdir -p "$OUTPUT_DIR"
cp "$SALT" "$OUTPUT_DIR/salt"

echo "Generating first preimage/key"
first_key=$(./target/*/release/wskdf-cli output-random-key -n $FIRST_PREIMAGE_NBITS --preimage-output first-preimage --key-output - --salt-input salt --ops-limit $MONTHS_LONG_OPS --mem-limit-kbytes $LARGE_MEM_LIMIT_KBYTES --params-output first-key-params.json)
echo "Generating first preimage/key (time-locked for $DAYS days), output: $OUTPUT_DIR/first-preimage-nbits-$FIRST_PREIMAGE_NBITS"
first_key=$(./target/*/release/wskdf-cli output-random-key -n $FIRST_PREIMAGE_NBITS --preimage-output $OUTPUT_DIR/first-preimage-nbits-$FIRST_PREIMAGE_NBITS --key-output - --salt-input $OUTPUT_DIR/salt --ops-limit $MONTHS_LONG_OPS --mem-limit-kbytes $LARGE_MEM_LIMIT_KBYTES --params-output $OUTPUT_DIR/first-key-params-nbits-$FIRST_PREIMAGE_NBITS.json)

echo "Generating second preimage/key"
second_key=$(./target/*/release/wskdf-cli output-random-key -n $SECOND_PREIMAGE_NBITS --preimage-output second-preimage --key-output - --salt-input salt --ops-limit $SECONDS_LONG_OPS --mem-limit-kbytes $SMALL_MEM_LIMIT_KBYTES --params-output second-key-params.json)
echo "Generating second preimage/key, output: $OUTPUT_DIR/second-preimage-nbits-$SECOND_PREIMAGE_NBITS"
second_key=$(./target/*/release/wskdf-cli output-random-key -n $SECOND_PREIMAGE_NBITS --preimage-output $OUTPUT_DIR/second-preimage-nbits-$SECOND_PREIMAGE_NBITS --key-output - --salt-input $OUTPUT_DIR/salt --ops-limit $SECONDS_LONG_OPS --mem-limit-kbytes $SMALL_MEM_LIMIT_KBYTES --params-output $OUTPUT_DIR/second-key-params-nbits-$SECOND_PREIMAGE_NBITS.json)

echo "Encrypting second preimage"
gpg --symmetric --batch --passphrase-fd 0 --cipher-algo AES256 --output encrypted-second-preimage.gpg "second-preimage" <<< "$first_key"
echo "Encrypting second preimage, output: $OUTPUT_DIR/encrypted-second-preimage-nbits-$SECOND_PREIMAGE_NBITS.gpg"
gpg --symmetric --batch --passphrase-fd 0 --cipher-algo AES256 --output $OUTPUT_DIR/encrypted-second-preimage-nbits-$SECOND_PREIMAGE_NBITS.gpg "$OUTPUT_DIR/second-preimage-nbits-$SECOND_PREIMAGE_NBITS" <<< "$first_key"

echo "Generating final keyfile"
./scripts/encrypted-keyfile-generate.sh 10M encrypted-final-keyfile.gpg <<< "$second_key"
echo "Generating final keyfile, output: $OUTPUT_DIR/encrypted-final-keyfile.gpg"
./scripts/encrypted-keyfile-generate.sh "$KEYFILE_SIZE" $OUTPUT_DIR/encrypted-final-keyfile.gpg <<< "$second_key"

echo "Deriving again second key (sanity check)"
second_key=$(./target/*/release/wskdf-cli derive-key --preimage-input second-preimage --key-output - --salt-input salt --ops-limit $SECONDS_LONG_OPS --mem-limit-kbytes $SMALL_MEM_LIMIT_KBYTES)
second_key=$(./target/*/release/wskdf-cli derive-key --preimage-input $OUTPUT_DIR/second-preimage-nbits-$SECOND_PREIMAGE_NBITS --key-output - --salt-input $OUTPUT_DIR/salt --ops-limit $SECONDS_LONG_OPS --mem-limit-kbytes $SMALL_MEM_LIMIT_KBYTES)

echo "Decrypting final keyfile"
gpg --decrypt --batch --passphrase-fd 0 --cipher-algo AES256 "encrypted-final-keyfile.gpg" <<< "$second_key" > final-keyfile
gpg --decrypt --batch --passphrase-fd 0 --cipher-algo AES256 "$OUTPUT_DIR/encrypted-final-keyfile.gpg" <<< "$second_key" > "$OUTPUT_DIR/final-keyfile"

echo "Done"
echo "Done. Generated keyfile: $OUTPUT_DIR/final-keyfile"

# To decrypt the second-preimage, use:

# first_key=$(./target/*/release/wskdf-cli derive-key --preimage-input first-preimage --key-output - --salt-input salt --ops-limit $MONTHS_LONG_OPS --mem-limit-kbytes $LARGE_MEM_LIMIT_KBYTES)
# gpg --decrypt --batch --passphrase-fd 0 --cipher-algo AES256 "encrypted-second-preimage.gpg" <<< "$first_key" > second-preimage-recovered
# first_key=$(./target/*/release/wskdf-cli derive-key --preimage-input $OUTPUT_DIR/first-preimage-nbits-$FIRST_PREIMAGE_NBITS --key-output - --salt-input $OUTPUT_DIR/salt --ops-limit $MONTHS_LONG_OPS --mem-limit-kbytes $LARGE_MEM_LIMIT_KBYTES)
# gpg --decrypt --batch --passphrase-fd 0 --cipher-algo AES256 "$OUTPUT_DIR/encrypted-second-preimage-nbits-$SECOND_PREIMAGE_NBITS.gpg" <<< "$first_key" > second-preimage-nbits-$SECOND_PREIMAGE_NBITS-recovered


# To brute force the second preimage, use:

# INPUT_FILE="encrypted-final-keyfile.gpg" OUTPUT_FILE="final-keyfile-recovered" ./target/*/release/wskdf-cli find-key --command ./scripts/gpg_decrypt.sh --preimage-output second-preimage-recovered --n-bits $SECOND_PREIMAGE_NBITS --threads 16 --salt-input salt --ops-limit $SECONDS_LONG_OPS --mem-limit-kbytes $SMALL_MEM_LIMIT_KBYTES
# INPUT_FILE="$OUTPUT_DIR/encrypted-final-keyfile.gpg" OUTPUT_FILE="$OUTPUT_DIR/final-keyfile-recovered" ./target/*/release/wskdf-cli find-key --command ./scripts/gpg_decrypt.sh --preimage-output $OUTPUT_DIR/second-preimage-nbits-$SECOND_PREIMAGE_NBITS-recovered --n-bits $SECOND_PREIMAGE_NBITS --threads 16 --salt-input $OUTPUT_DIR/salt --ops-limit $SECONDS_LONG_OPS --mem-limit-kbytes $SMALL_MEM_LIMIT_KBYTES
2 changes: 1 addition & 1 deletion wskdf-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ fn main() -> anyhow::Result<()> {

fn ensure_file_does_not_exists(path: &std::path::Path, message: &str) -> anyhow::Result<()> {
if path.as_os_str() != "-" {
ensure!(!path.exists(), "{message}");
ensure!(!path.exists(), "{path}: {message}", path = path.display());
}
Ok(())
}
Expand Down