CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-squareup-okio--okio

A modern I/O library for Android, Java, and Kotlin Multiplatform

Pending
Overview
Eval results
Files

hashing.mddocs/

Hashing and Security

This document covers Okio's cryptographic hashing operations and secure data handling capabilities. Okio provides built-in support for common hash functions and HMAC operations.

Hash Functions Overview

Okio provides direct support for these cryptographic hash functions:

  • MD5: 128-bit hash (deprecated for security, but still available)
  • SHA-1: 160-bit hash (deprecated for security, but still available)
  • SHA-256: 256-bit hash (recommended)
  • SHA-512: 512-bit hash (recommended)

All hash functions are available on both ByteString and Buffer objects, plus HMAC variants.

ByteString Hashing

All hash functions are available as methods on ByteString instances:

Basic Hash Functions

expect open class ByteString {
    // Cryptographic hash functions
    fun md5(): ByteString
    fun sha1(): ByteString
    fun sha256(): ByteString
    fun sha512(): ByteString
    
    // HMAC operations
    fun hmacSha1(key: ByteString): ByteString
    fun hmacSha256(key: ByteString): ByteString
    fun hmacSha512(key: ByteString): ByteString
}

Usage Examples

// Basic hashing of text data
val data = "Hello, Okio Security!".encodeUtf8()

// Generate different hash types
val md5Hash = data.md5()
val sha1Hash = data.sha1()
val sha256Hash = data.sha256()
val sha512Hash = data.sha512()

println("Original: ${data.utf8()}")
println("MD5:      ${md5Hash.hex()}")
println("SHA-1:    ${sha1Hash.hex()}")
println("SHA-256:  ${sha256Hash.hex()}")
println("SHA-512:  ${sha512Hash.hex()}")

// HMAC operations with secret key
val secretKey = "my-secret-key".encodeUtf8()
val hmacSha256 = data.hmacSha256(secretKey)
val hmacSha512 = data.hmacSha512(secretKey)

println("HMAC-SHA256: ${hmacSha256.hex()}")
println("HMAC-SHA512: ${hmacSha512.hex()}")

File Integrity Verification

val fs = FileSystem.SYSTEM
val filePath = "/tmp/important-file.txt".toPath()

// Create a file with content
fs.write(filePath) {
    writeUtf8("This is important data that needs integrity verification.")
}

// Calculate file hash
val fileContent = fs.read(filePath) { readByteString() }
val originalHash = fileContent.sha256()
println("Original SHA-256: ${originalHash.hex()}")

// Simulate file modification
fs.write(filePath) {
    writeUtf8("This is important data that needs integrity verification. MODIFIED!")
}

// Verify integrity
val modifiedContent = fs.read(filePath) { readByteString() }
val modifiedHash = modifiedContent.sha256()

if (originalHash == modifiedHash) {
    println("✓ File integrity verified")
} else {
    println("✗ File has been modified!")
    println("Modified SHA-256: ${modifiedHash.hex()}")
}

Buffer Hashing

Buffer objects provide the same hash functions as ByteString:

Buffer Hash Methods

expect class Buffer {
    // Hash functions (same as ByteString)
    fun md5(): ByteString
    fun sha1(): ByteString
    fun sha256(): ByteString
    fun sha512(): ByteString
    fun hmacSha1(key: ByteString): ByteString
    fun hmacSha256(key: ByteString): ByteString
    fun hmacSha512(key: ByteString): ByteString
}

Streaming Hash Calculation

// Calculate hash of streaming data without loading everything into memory
fun calculateStreamingHash(source: Source): ByteString {
    val buffer = Buffer()
    val hashBuffer = Buffer()
    
    source.use { input ->
        // Read data in chunks and accumulate for hashing
        while (!input.exhausted()) {
            val bytesRead = input.read(buffer, 8192L) // 8KB chunks
            if (bytesRead > 0) {
                // Copy to hash buffer
                buffer.copyTo(hashBuffer)
                buffer.clear()
            }
        }
    }
    
    return hashBuffer.sha256()
}

// Usage with large file
val largeFile = "/tmp/large-file.txt".toPath()
val fs = FileSystem.SYSTEM

// Create large file
fs.write(largeFile) {
    repeat(10000) { i ->
        writeUtf8("Line $i: This is a line in a large file for hash testing.\n")
    }
}

// Calculate hash efficiently
val fileHash = calculateStreamingHash(fs.source(largeFile))
println("Large file SHA-256: ${fileHash.hex()}")

HashingSource and HashingSink

For continuous hash calculation during I/O operations, Okio provides HashingSource and HashingSink.

HashingSource

expect class HashingSource(source: Source, digest: Digest) : Source {
    val hash: ByteString
    
    override fun read(sink: Buffer, byteCount: Long): Long
    override fun timeout(): Timeout
    override fun close()
}

HashingSink

expect class HashingSink(sink: Sink, digest: Digest) : Sink {
    val hash: ByteString
    
    override fun write(source: Buffer, byteCount: Long)
    override fun flush()
    override fun timeout(): Timeout
    override fun close()
}

Digest Interface

interface Digest {
    fun update(input: ByteArray, offset: Int, byteCount: Int)
    fun digest(): ByteArray
    fun reset()
}

Usage Examples

// Create digest instances (platform-specific implementation)
fun createSha256Digest(): Digest = // Platform-specific SHA-256 digest implementation

// Hash data while reading
fun hashWhileReading(source: Source): Pair<ByteString, ByteString> {
    val sha256Digest = createSha256Digest()
    val hashingSource = HashingSource(source, sha256Digest)
    val buffer = Buffer()
    
    hashingSource.use { hasher ->
        buffer.writeAll(hasher)
    }
    
    return Pair(buffer.readByteString(), hasher.hash)
}

// Hash data while writing
fun hashWhileWriting(data: ByteString, sink: Sink): ByteString {
    val sha256Digest = createSha256Digest()
    val hashingSink = HashingSink(sink, sha256Digest)
    
    hashingSink.use { hasher ->
        hasher.write(Buffer().write(data), data.size.toLong())
    }
    
    return hashingSink.hash
}

// File copy with integrity verification
fun copyWithHash(sourcePath: Path, targetPath: Path): ByteString {
    val fs = FileSystem.SYSTEM
    val sha256Digest = createSha256Digest()
    
    val hash = fs.sink(targetPath).use { targetSink ->
        val hashingSink = HashingSink(targetSink, sha256Digest)
        
        fs.source(sourcePath).use { sourceFile ->
            hashingSink.writeAll(sourceFile)
        }
        
        hashingSink.hash
    }
    
    println("File copied with SHA-256: ${hash.hex()}")
    return hash
}

Password Hashing and Key Derivation

While Okio doesn't provide built-in password hashing functions like bcrypt or Argon2, you can use HMAC for key derivation:

PBKDF2-style Key Derivation

// Simple PBKDF2-style key derivation using HMAC-SHA256
fun deriveKey(password: String, salt: ByteString, iterations: Int, keyLength: Int): ByteString {
    var derivedKey = (password + salt.utf8()).encodeUtf8()
    
    repeat(iterations) {
        derivedKey = derivedKey.hmacSha256(salt)
    }
    
    // Truncate or extend to desired length
    return if (derivedKey.size >= keyLength) {
        derivedKey.substring(0, keyLength)
    } else {
        // For simplicity, just repeat if needed (not cryptographically ideal)
        val buffer = Buffer()
        while (buffer.size < keyLength) {
            buffer.write(derivedKey)
        }
        buffer.readByteString(keyLength.toLong())
    }
}

// Usage
val password = "user-password"
val salt = "random-salt-12345".encodeUtf8()
val iterations = 10000
val keyLength = 32 // 256 bits

val derivedKey = deriveKey(password, salt, iterations, keyLength)
println("Derived key: ${derivedKey.hex()}")

// Use the same inputs to verify
val verificationKey = deriveKey(password, salt, iterations, keyLength)
println("Keys match: ${derivedKey == verificationKey}")

Data Verification and Checksums

Message Authentication

// Create authenticated message with HMAC
data class AuthenticatedMessage(
    val data: ByteString,
    val signature: ByteString
) {
    companion object {
        fun create(message: String, secretKey: ByteString): AuthenticatedMessage {
            val data = message.encodeUtf8()
            val signature = data.hmacSha256(secretKey)
            return AuthenticatedMessage(data, signature)
        }
    }
    
    fun verify(secretKey: ByteString): Boolean {
        val expectedSignature = data.hmacSha256(secretKey)
        return expectedSignature == signature
    }
    
    fun getMessage(): String = data.utf8()
}

// Usage
val secretKey = "shared-secret-key".encodeUtf8()
val message = "This is a secure message that needs authentication."

// Create authenticated message
val authMessage = AuthenticatedMessage.create(message, secretKey)
println("Message: ${authMessage.getMessage()}")
println("Signature: ${authMessage.signature.hex()}")

// Verify message
val isValid = authMessage.verify(secretKey)
println("Message is valid: $isValid")

// Test with wrong key
val wrongKey = "wrong-secret-key".encodeUtf8()
val isValidWithWrongKey = authMessage.verify(wrongKey)
println("Message valid with wrong key: $isValidWithWrongKey")

File Checksum Utilities

// Generate checksum file (like sha256sum)
fun generateChecksumFile(filePaths: List<Path>, checksumPath: Path) {
    val fs = FileSystem.SYSTEM
    
    fs.write(checksumPath) {
        filePaths.forEach { filePath ->
            if (fs.exists(filePath) && fs.metadata(filePath).isRegularFile) {
                val content = fs.read(filePath) { readByteString() }
                val hash = content.sha256()
                writeUtf8("${hash.hex()}  ${filePath.name}\n")
            }
        }
    }
}

// Verify checksums
fun verifyChecksums(checksumPath: Path, baseDir: Path): List<Pair<String, Boolean>> {
    val fs = FileSystem.SYSTEM
    val results = mutableListOf<Pair<String, Boolean>>()
    
    fs.read(checksumPath) {
        while (!exhausted()) {
            val line = readUtf8Line() ?: break
            val parts = line.split("  ", limit = 2)
            
            if (parts.size == 2) {
                val expectedHash = parts[0]
                val fileName = parts[1]
                val filePath = baseDir / fileName
                
                if (fs.exists(filePath)) {
                    val actualContent = fs.read(filePath) { readByteString() }
                    val actualHash = actualContent.sha256().hex()
                    val matches = expectedHash.equals(actualHash, ignoreCase = true)
                    results.add(fileName to matches)
                    
                    println("$fileName: ${if (matches) "OK" else "FAILED"}")
                } else {
                    results.add(fileName to false)
                    println("$fileName: NOT FOUND")
                }
            }
        }
    }
    
    return results
}

// Usage
val testDir = "/tmp/checksum-test".toPath()
val fs = FileSystem.SYSTEM

fs.createDirectory(testDir)

// Create test files
val testFiles = listOf("file1.txt", "file2.txt", "file3.txt")
testFiles.forEach { fileName ->
    fs.write(testDir / fileName) {
        writeUtf8("Content of $fileName")
    }
}

// Generate checksums
val checksumFile = testDir / "checksums.sha256"
generateChecksumFile(testFiles.map { testDir / it }, checksumFile)

// Verify checksums
println("Checksum verification:")
val results = verifyChecksums(checksumFile, testDir)
val allValid = results.all { it.second }
println("All files valid: $allValid")

Security Best Practices

Constant-Time Comparison

// Prevent timing attacks when comparing hashes
fun constantTimeEquals(a: ByteString, b: ByteString): Boolean {
    if (a.size != b.size) return false
    
    var result = 0
    for (i in 0 until a.size) {
        result = result or (a[i].toInt() xor b[i].toInt())
    }
    
    return result == 0
}

// Secure hash comparison
fun verifyPasswordHash(password: String, salt: ByteString, storedHash: ByteString): Boolean {
    val inputHash = (password + salt.utf8()).encodeUtf8().sha256()
    return constantTimeEquals(inputHash, storedHash)
}

Random Salt Generation

// Generate cryptographically secure random salt
fun generateSalt(length: Int = 16): ByteString {
    val random = java.security.SecureRandom()
    val saltBytes = ByteArray(length)
    random.nextBytes(saltBytes)
    return saltBytes.toByteString()
}

// Usage in password hashing
fun hashPassword(password: String): Pair<ByteString, ByteString> {
    val salt = generateSalt()
    val hash = (password + salt.utf8()).encodeUtf8().sha256()
    return Pair(salt, hash)
}

val (salt, hash) = hashPassword("user-password")
println("Salt: ${salt.hex()}")
println("Hash: ${hash.hex()}")

Performance Considerations

Choosing Hash Functions

// Benchmark different hash functions
fun benchmarkHashFunctions(data: ByteString) {
    val hashFunctions = listOf(
        "MD5" to { data: ByteString -> data.md5() },
        "SHA-1" to { data: ByteString -> data.sha1() },
        "SHA-256" to { data: ByteString -> data.sha256() },
        "SHA-512" to { data: ByteString -> data.sha512() }
    )
    
    hashFunctions.forEach { (name, hashFunc) ->
        val startTime = System.nanoTime()
        val hash = hashFunc(data)
        val endTime = System.nanoTime()
        
        val durationMs = (endTime - startTime) / 1_000_000.0
        println("$name: ${hash.hex().take(16)}... (${durationMs}ms)")
    }
}

// Test with different data sizes
val smallData = "Small test data".encodeUtf8()
val largeData = "Large test data. ".repeat(10000).encodeUtf8()

println("Small data (${smallData.size} bytes):")
benchmarkHashFunctions(smallData)

println("\nLarge data (${largeData.size} bytes):")
benchmarkHashFunctions(largeData)

Memory-Efficient Hashing

// Hash large files without loading into memory
fun hashLargeFile(filePath: Path): ByteString {
    val fs = FileSystem.SYSTEM
    val buffer = Buffer()
    
    fs.source(filePath).buffer().use { source ->
        while (!source.exhausted()) {
            val chunk = source.readByteString(minOf(65536L, source.buffer.size)) // 64KB chunks
            buffer.write(chunk)
        }
    }
    
    return buffer.sha256()
}

Error Handling

Hash operations are generally safe, but I/O operations during hashing can fail:

// Robust hash calculation with error handling
fun safeHashFile(filePath: Path): ByteString? {
    return try {
        val fs = FileSystem.SYSTEM
        fs.read(filePath) { readByteString() }.sha256()
    } catch (e: FileNotFoundException) {
        println("File not found: $filePath")
        null
    } catch (e: IOException) {
        println("I/O error reading file: ${e.message}")
        null
    } catch (e: Exception) {
        println("Unexpected error: ${e.message}")
        null
    }
}

// Verify multiple files with error handling
fun verifyFileHashes(fileHashes: Map<Path, String>): Map<Path, String> {
    val results = mutableMapOf<Path, String>()
    
    fileHashes.forEach { (path, expectedHash) ->
        val actualHash = safeHashFile(path)
        val status = when {
            actualHash == null -> "ERROR"
            actualHash.hex().equals(expectedHash, ignoreCase = true) -> "OK"
            else -> "MISMATCH"
        }
        results[path] = status
        println("${path.name}: $status")
    }
    
    return results
}

Install with Tessl CLI

npx tessl i tessl/maven-com-squareup-okio--okio

docs

compression.md

core-io.md

filesystem.md

hashing.md

index.md

utilities.md

tile.json