Block Time-Lock Messages

December 12, 2024 by CryptoPatrick bitcoin cryptography rust

Key Idea

Bitcoin blocks are timestamped and generated approximately every 10 minutes. These timestamps, along with data from the block (e.g., block hash), can act as a dynamic, time-dependent key or seed in a cryptographic scheme.

How It Works

Block Hash as Key Material: Use the hash of a specific Bitcoin block as part of the key material. For example, block N’s hash (or a portion of it) could serve as the encryption key for data until block N+1 is mined.

Encryption Process:

  1. Obtain the latest block hash (or a specific part of the block data)
  2. Use a cryptographic function like AES to encrypt data with the block hash as the key

Decryption Process:

  1. The recipient waits for the corresponding block to be mined or refers to the same block that was used for encryption
  2. They retrieve the block hash from a reliable source (e.g., Bitcoin network or block explorer)
  3. Decrypt the data using the block hash as the key

Time-Locking: By specifying which block hash to use, you can enforce a time-locking mechanism. For example, “This message can only be decrypted after block N+2” requires the recipient to wait until that block is mined.

Algorithm

  1. At encryption time, take the current latest block_id (BID) and its blockhash - this is the root of the encryption
  2. State how much time you want to lock the message for (e.g., 20 minutes)
  3. Divide the time by the average new blocktime to get the number of block hashes that need to be collected between BID and THEN
  4. n is the number of consecutive individual blockhashes that needs to be collected, starting from the current BID
  5. Encrypt the message with the current blockhash and a private key
  6. To decrypt the message, the hash of BID + n consecutive hashes must be entered to reveal the message

Implementation in Rust

use sha2::{Sha256, Digest};
use aes_gcm::aead::{Aead, KeyInit, OsRng};
use aes_gcm::{Aes256Gcm, Nonce};

fn encrypt_with_block_hash(data: &[u8], block_hash: &str) -> Vec<u8> {
    // Hash the block hash to derive a symmetric key
    let mut hasher = Sha256::new();
    hasher.update(block_hash.as_bytes());
    let key = hasher.finalize();

    // Use AES-GCM encryption
    let cipher = Aes256Gcm::new_from_slice(&key).expect("Key generation failed");
    let nonce = Nonce::from_slice(b"unique nonce"); // Use a secure method to generate this in production
    cipher.encrypt(nonce, data).expect("Encryption failed")
}

Decrypt:

fn decrypt_with_block_hash(encrypted_data: &[u8], block_hash: &str) -> Vec<u8> {
    let mut hasher = Sha256::new();
    hasher.update(block_hash.as_bytes());
    let key = hasher.finalize();

    let cipher = Aes256Gcm::new_from_slice(&key).expect("Key generation failed");
    let nonce = Nonce::from_slice(b"unique nonce"); // Same nonce as used during encryption
    cipher.decrypt(nonce, encrypted_data).expect("Decryption failed")
}

Use Cases

Block Hash Retrieval: Use a Bitcoin RPC client or a block explorer API to get the latest block hash.

Time-Locking Messages: Encrypt messages that can only be decrypted after a specific block is mined.

Proof of Time: The encryption key’s dependency on the block hash proves that the data was encrypted at a specific point in the blockchain timeline.

Dependencies

[dependencies]
aes-gcm = "0.10" # AES-GCM for encryption
aes-gcm-siv = "0.10"
rand = "0.8" # For generating random initialization vectors (IVs)
clap = "4.0" # Command-line argument parsing
use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; // Or `Aes128Gcm`
use aes_gcm::aead::{Aead, OsRng}; // Random number generator
use clap::{Parser, Subcommand};
use std::fs;

Command-Line Interface

CLI Structure:

/// Struct for command-line argument parsing
#[derive(Parser)]
#[command(name = "File Encryptor")]
#[command(about = "Encrypt and decrypt files using a password", long_about = None)]
struct Cli {
    #[command(subcommand)]
    operation: Operation,
}

#[derive(Subcommand)]
enum Operation {
    Encrypt {
        #[arg(short, long)]
        input: String,
        #[arg(short, long)]
        output: String,
        #[arg(short, long)]
        password: String,
    },
    Decrypt {
        #[arg(short, long)]
        input: String,
        #[arg(short, long)]
        output: String,
        #[arg(short, long)]
        password: String,
    },
}

fn derive_key(password: &str) -> [u8; 32] {
    let mut hasher = sha2::Sha256::new();
    hasher.update(password.as_bytes());
    let result = hasher.finalize();
    let mut key = [0u8; 32];
    key.copy_from_slice(&result[..32]);
    key
}

fn encrypt_file(input_path: &str, output_path: &str, password: &str) -> Result<(), Box<dyn std::error::Error>> {
    let plaintext = fs::read(input_path)?;
    let key = derive_key(password);
    let cipher = Aes256Gcm::new_from_slice(&key).expect("Invalid key length");
    let nonce = aes_gcm::Nonce::from_slice(&rand::random::<[u8; 12]>());
    let ciphertext = cipher.encrypt(nonce, plaintext.as_ref())
        .expect("Encryption failed");
    let mut output_data = nonce.to_vec();
    output_data.extend_from_slice(&ciphertext);
    fs::write(output_path, output_data)?;
    println!("File encrypted successfully!");
    Ok(())
}

fn decrypt_file(input_path: &str, output_path: &str, password: &str) -> Result<(), Box<dyn std::error::Error>> {
    let input_data = fs::read(input_path)?;
    let (nonce, ciphertext) = input_data.split_at(12);
    let key = derive_key(password);
    let cipher = Aes256Gcm::new_from_slice(&key).expect("Invalid key length");
    let plaintext = cipher.decrypt(Nonce::from_slice(nonce), ciphertext)
        .expect("Decryption failed: Incorrect password or corrupted file");
    fs::write(output_path, plaintext)?;
    println!("File decrypted successfully!");
    Ok(())
}

fn main() {
    let cli = Cli::parse();
    match cli.operation {
        Operation::Encrypt { input, output, password } => {
            if let Err(e) = encrypt_file(&input, &output, &password) {
                eprintln!("Error during encryption: {}", e);
            }
        }
        Operation::Decrypt { input, output, password } => {
            if let Err(e) = decrypt_file(&input, &output, &password) {
                eprintln!("Error during decryption: {}", e);
            }
        }
    }
}

How The Encryption Works

Key Derivation:

  • A SHA-256 hash of the password is used as the encryption key
  • This ensures consistent and strong key generation

Nonce:

  • A 12-byte random nonce is generated for each encryption
  • It is prepended to the ciphertext and stored in the output file

Encrypt/Decrypt Workflow:

  • During encryption, the file content is read, encrypted with AES-GCM, and written along with the nonce to the output file
  • During decryption, the nonce is extracted, and the ciphertext is decrypted

Usage

Encrypt a file:

cargo run -- encrypt -i input.txt -o encrypted.bin -p mypassword

Decrypt a file:

cargo run -- decrypt -i encrypted.bin -o decrypted.txt -p mypassword

Security Notes

Password Strength: Ensure the password is strong, as it directly affects the security of the encryption.

Nonce Management: The nonce is unique per file and stored with the ciphertext. Reusing nonces with the same key would compromise security.

Error Handling: The program gracefully handles errors, like incorrect passwords or corrupted files, and informs the user.

This program is secure and flexible for encrypting and decrypting files with a password. You can extend it further by adding support for key files or integrating it with a secure storage system.

'I write to understand as much as to be understood.' —Elie Wiesel
(c) 2024 CryptoPatrick