Secure file verification with Ruby and SHA384

Ensuring file integrity is crucial for developers, especially when handling sensitive data or distributing software. SHA384 hashing provides a robust method to verify that files haven't been altered or corrupted. Let's explore how you can implement SHA384 hashing in Ruby using the OpenSSL library.
Why SHA384?
SHA384 is part of the SHA-2 family, offering a secure hashing algorithm that generates a unique 384-bit (48-byte) hash. It's widely used for verifying file integrity, ensuring data hasn't been tampered with during transfers or storage.
SHA384 hashing enhances file security by:
- Detecting file corruption or unauthorized modifications.
- Providing a unique fingerprint for each file.
- Ensuring data integrity during transfers.
- Offering stronger security than SHA256 with minimal performance impact.
Setting up your Ruby environment
Ruby's OpenSSL integration comes built-in, but you'll need a proper Ruby installation. Here's how to set it up using rbenv:
On macOS or Linux with homebrew
brew install rbenv
rbenv init
# Follow the printed Instructions to update your shell configuration
On Linux without homebrew
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv && src/configure && make -C src
echo 'export PATH=\"$HOME/.rbenv/bin:$PATH\"' >> ~/.bashrc
~/.rbenv/bin/rbenv init
# Follow the printed Instructions to update your shell configuration
After installation, install Ruby:
rbenv install 3.2.2 # or latest stable version
rbenv global 3.2.2
ruby -v # Verify installation
Hashing files with Ruby and openssl
Here's a robust implementation for hashing files using Ruby's built-in OpenSSL library:
require 'openssl'
def sha384_hash(file_path)
digest = OpenSSL::Digest.new('SHA384')
File.open(file_path, 'rb') do |file|
buffer = ""
# Read file in chunks to handle large files efficiently
digest.update(buffer) while file.read(8192, buffer)
end
digest.hexdigest
end
# Usage example with error handling
begin
hash = sha384_hash('example.txt')
puts \"SHA384 hash: #{hash}\"
rescue Errno::ENOENT
puts \"Error: File not found\"
rescue => e
puts \"Error: #{e.message}\"
end
This implementation:
- Reads the file in chunks (8KB at a time), efficiently handling large files without loading them entirely into memory.
- Uses binary mode (
'rb'
) for file reading to ensure consistent results across platforms. - Includes proper error handling for common issues like missing files.
Verifying file integrity
To verify file integrity, compare the generated hash against a known good hash. It's crucial to use a constant-time comparison function to prevent timing attacks, where an attacker could deduce information about the hash by measuring the time it takes for the comparison to complete.
require 'openssl'
# Constant-time comparison function
def secure_compare(a, b)
# Ensure strings are of the same type and encoding for accurate byte comparison
a = a.b unless a.encoding == Encoding::BINARY
b = b.b unless b.encoding == Encoding::BINARY
return false unless a.bytesize == b.bytesize
l = a.unpack \"C*\"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
# Assume sha384_hash function from previous example is defined
expected_hash = \"your_known_good_hash_here\" # Replace with the actual expected hash
begin
file_hash = sha384_hash('example.txt')
if secure_compare(file_hash, expected_hash)
puts \"File integrity verified.\"
else
puts \"File integrity compromised!\"
end
rescue Errno::ENOENT
puts \"Error: File not found\"
rescue => e
puts \"Error verifying file: #{e.message}\"
end
Cross-platform considerations
When verifying files across different operating systems, be aware of potential line-ending differences (CRLF on Windows vs. LF on macOS/Linux) that can affect hash values if files are read in text mode. Always use binary mode when reading files for hashing to ensure consistent results:
# Always use binary mode ('rb') for consistent cross-platform results
File.open(file_path, 'rb') do |file|
# Hashing logic here...
end
If you absolutely must compare hashes of text files generated on different platforms and cannot guarantee binary mode was used initially, you might consider normalizing line endings before hashing. However, the best practice is to always hash the file byte-for-byte using binary mode.
# Example of normalizing line endings (use with caution)
def normalize_and_hash(file_path)
content = File.read(file_path, mode: 'rb')
# Normalize to LF line endings
normalized = content.gsub(/\r\n?/, \"\n\")
digest = OpenSSL::Digest.new('SHA384')
digest.update(normalized)
digest.hexdigest
end
Automating file verification
Automating file verification can streamline your workflow, especially when dealing with multiple files, such as during software distribution or data validation pipelines. Here's a script to automate verification for multiple files against a list of known hashes:
require 'openssl'
# Assume sha384_hash and secure_compare functions are defined as above
files_to_verify = {
'example.txt' => 'known_good_hash_for_example_txt',
'another_file.dat' => 'known_good_hash_for_another_file'
# Add more files and their expected hashes here
}
files_to_verify.each do |file, expected_hash|
begin
actual_hash = sha384_hash(file)
if secure_compare(actual_hash, expected_hash)
puts \"#{file}: Verified ✅\"
else
puts \"#{file}: Verification failed ❌ (Hash mismatch)\"
end
rescue Errno::ENOENT
puts \"#{file}: Verification failed ❌ (File not found)\"
rescue => e
puts \"#{file}: Error - #{e.message} ❌\"
end
end
Troubleshooting common issues
When working with SHA384 hashing in Ruby, you might encounter these common issues:
- Inconsistent hashes across platforms: Ensure you're using binary mode (
'rb'
) when opening files for hashing. Line ending differences in text mode will alter the hash. - Memory issues with large files: Verify you're using the chunked reading approach (reading
the file piece by piece) as shown in the
sha384_hash
example, rather than loading the entire file into memory. - Incorrect hash comparisons: Use a constant-time comparison function (
secure_compare
) to prevent timing side-channel attacks. Simple string comparison (==
) can be vulnerable. - Encoding issues: While binary mode mitigates most encoding problems for hashing, be mindful if you manipulate file content as strings before hashing. Ensure consistent encoding.
- Performance concerns: For extremely large files or high-throughput scenarios, native extensions or external tools might offer better performance, though Ruby's OpenSSL binding is generally efficient.
Best practices for secure file handling and verification
- Always use binary mode: Read files in binary mode (
File.open(file_path, 'rb')
) for hashing to ensure consistency across platforms and prevent issues with line endings or character encodings. - Process large files in chunks: Avoid reading entire large files into memory. Use buffered
reading (e.g.,
file.read(chunk_size, buffer)
) to manage memory efficiently. - Implement proper error handling: Anticipate issues like file not found (
Errno::ENOENT
), permissions errors, or I/O errors, and handle them gracefully. - Store hashes securely: Keep the known good hashes separate from the files they verify. Store them in a secure database, configuration file, or signed manifest.
- Use constant-time comparison: When verifying hashes, always use a constant-time string comparison function to prevent timing attacks.
- Keep libraries updated: Ensure your Ruby installation and its OpenSSL library are up-to-date to benefit from the latest security patches.
- Consider the source of truth: Establish a secure process for generating and distributing the initial "known good" hashes.
Using SHA384 with Transloadit
Transloadit's Robot ecosystem includes the 🤖 /file/hash Robot as part of our Media Cataloging service. This Robot supports various hashing algorithms including SHA384, allowing you to generate hashes as part of your file processing workflows. Here's an example Assembly Instruction:
{
\"steps\": {
\"hash\": {
\"robot\": \"/file/hash\",
\"use\": \":original\",
\"algorithm\": \"sha384\"
}
}
}
By integrating SHA384 hashing into your Ruby applications, you add a strong layer of security, ensuring your files remain integral and tamper-proof throughout their lifecycle.