A modern I/O library for Android, Java, and Kotlin Multiplatform
—
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.
Okio provides direct support for these cryptographic hash functions:
All hash functions are available on both ByteString and Buffer objects, plus HMAC variants.
All hash functions are available as methods on ByteString instances:
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
}// 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()}")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 objects provide the same hash functions as ByteString:
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
}// 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()}")For continuous hash calculation during I/O operations, Okio provides HashingSource and HashingSink.
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()
}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()
}interface Digest {
fun update(input: ByteArray, offset: Int, byteCount: Int)
fun digest(): ByteArray
fun reset()
}// 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
}While Okio doesn't provide built-in password hashing functions like bcrypt or Argon2, you can use HMAC for 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}")// 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")// 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")// 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)
}// 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()}")// 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)// 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()
}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