0
# Hashing and Security
1
2
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.
3
4
## Hash Functions Overview
5
6
Okio provides direct support for these cryptographic hash functions:
7
8
- **MD5**: 128-bit hash (deprecated for security, but still available)
9
- **SHA-1**: 160-bit hash (deprecated for security, but still available)
10
- **SHA-256**: 256-bit hash (recommended)
11
- **SHA-512**: 512-bit hash (recommended)
12
13
All hash functions are available on both `ByteString` and `Buffer` objects, plus HMAC variants.
14
15
## ByteString Hashing
16
17
All hash functions are available as methods on ByteString instances:
18
19
### Basic Hash Functions
20
21
```kotlin { .api }
22
expect open class ByteString {
23
// Cryptographic hash functions
24
fun md5(): ByteString
25
fun sha1(): ByteString
26
fun sha256(): ByteString
27
fun sha512(): ByteString
28
29
// HMAC operations
30
fun hmacSha1(key: ByteString): ByteString
31
fun hmacSha256(key: ByteString): ByteString
32
fun hmacSha512(key: ByteString): ByteString
33
}
34
```
35
36
### Usage Examples
37
38
```kotlin
39
// Basic hashing of text data
40
val data = "Hello, Okio Security!".encodeUtf8()
41
42
// Generate different hash types
43
val md5Hash = data.md5()
44
val sha1Hash = data.sha1()
45
val sha256Hash = data.sha256()
46
val sha512Hash = data.sha512()
47
48
println("Original: ${data.utf8()}")
49
println("MD5: ${md5Hash.hex()}")
50
println("SHA-1: ${sha1Hash.hex()}")
51
println("SHA-256: ${sha256Hash.hex()}")
52
println("SHA-512: ${sha512Hash.hex()}")
53
54
// HMAC operations with secret key
55
val secretKey = "my-secret-key".encodeUtf8()
56
val hmacSha256 = data.hmacSha256(secretKey)
57
val hmacSha512 = data.hmacSha512(secretKey)
58
59
println("HMAC-SHA256: ${hmacSha256.hex()}")
60
println("HMAC-SHA512: ${hmacSha512.hex()}")
61
```
62
63
### File Integrity Verification
64
65
```kotlin
66
val fs = FileSystem.SYSTEM
67
val filePath = "/tmp/important-file.txt".toPath()
68
69
// Create a file with content
70
fs.write(filePath) {
71
writeUtf8("This is important data that needs integrity verification.")
72
}
73
74
// Calculate file hash
75
val fileContent = fs.read(filePath) { readByteString() }
76
val originalHash = fileContent.sha256()
77
println("Original SHA-256: ${originalHash.hex()}")
78
79
// Simulate file modification
80
fs.write(filePath) {
81
writeUtf8("This is important data that needs integrity verification. MODIFIED!")
82
}
83
84
// Verify integrity
85
val modifiedContent = fs.read(filePath) { readByteString() }
86
val modifiedHash = modifiedContent.sha256()
87
88
if (originalHash == modifiedHash) {
89
println("✓ File integrity verified")
90
} else {
91
println("✗ File has been modified!")
92
println("Modified SHA-256: ${modifiedHash.hex()}")
93
}
94
```
95
96
## Buffer Hashing
97
98
Buffer objects provide the same hash functions as ByteString:
99
100
### Buffer Hash Methods
101
102
```kotlin { .api }
103
expect class Buffer {
104
// Hash functions (same as ByteString)
105
fun md5(): ByteString
106
fun sha1(): ByteString
107
fun sha256(): ByteString
108
fun sha512(): ByteString
109
fun hmacSha1(key: ByteString): ByteString
110
fun hmacSha256(key: ByteString): ByteString
111
fun hmacSha512(key: ByteString): ByteString
112
}
113
```
114
115
### Streaming Hash Calculation
116
117
```kotlin
118
// Calculate hash of streaming data without loading everything into memory
119
fun calculateStreamingHash(source: Source): ByteString {
120
val buffer = Buffer()
121
val hashBuffer = Buffer()
122
123
source.use { input ->
124
// Read data in chunks and accumulate for hashing
125
while (!input.exhausted()) {
126
val bytesRead = input.read(buffer, 8192L) // 8KB chunks
127
if (bytesRead > 0) {
128
// Copy to hash buffer
129
buffer.copyTo(hashBuffer)
130
buffer.clear()
131
}
132
}
133
}
134
135
return hashBuffer.sha256()
136
}
137
138
// Usage with large file
139
val largeFile = "/tmp/large-file.txt".toPath()
140
val fs = FileSystem.SYSTEM
141
142
// Create large file
143
fs.write(largeFile) {
144
repeat(10000) { i ->
145
writeUtf8("Line $i: This is a line in a large file for hash testing.\n")
146
}
147
}
148
149
// Calculate hash efficiently
150
val fileHash = calculateStreamingHash(fs.source(largeFile))
151
println("Large file SHA-256: ${fileHash.hex()}")
152
```
153
154
## HashingSource and HashingSink
155
156
For continuous hash calculation during I/O operations, Okio provides `HashingSource` and `HashingSink`.
157
158
### HashingSource
159
160
```kotlin { .api }
161
expect class HashingSource(source: Source, digest: Digest) : Source {
162
val hash: ByteString
163
164
override fun read(sink: Buffer, byteCount: Long): Long
165
override fun timeout(): Timeout
166
override fun close()
167
}
168
```
169
170
### HashingSink
171
172
```kotlin { .api }
173
expect class HashingSink(sink: Sink, digest: Digest) : Sink {
174
val hash: ByteString
175
176
override fun write(source: Buffer, byteCount: Long)
177
override fun flush()
178
override fun timeout(): Timeout
179
override fun close()
180
}
181
```
182
183
### Digest Interface
184
185
```kotlin { .api }
186
interface Digest {
187
fun update(input: ByteArray, offset: Int, byteCount: Int)
188
fun digest(): ByteArray
189
fun reset()
190
}
191
```
192
193
### Usage Examples
194
195
```kotlin
196
// Create digest instances (platform-specific implementation)
197
fun createSha256Digest(): Digest = // Platform-specific SHA-256 digest implementation
198
199
// Hash data while reading
200
fun hashWhileReading(source: Source): Pair<ByteString, ByteString> {
201
val sha256Digest = createSha256Digest()
202
val hashingSource = HashingSource(source, sha256Digest)
203
val buffer = Buffer()
204
205
hashingSource.use { hasher ->
206
buffer.writeAll(hasher)
207
}
208
209
return Pair(buffer.readByteString(), hasher.hash)
210
}
211
212
// Hash data while writing
213
fun hashWhileWriting(data: ByteString, sink: Sink): ByteString {
214
val sha256Digest = createSha256Digest()
215
val hashingSink = HashingSink(sink, sha256Digest)
216
217
hashingSink.use { hasher ->
218
hasher.write(Buffer().write(data), data.size.toLong())
219
}
220
221
return hashingSink.hash
222
}
223
224
// File copy with integrity verification
225
fun copyWithHash(sourcePath: Path, targetPath: Path): ByteString {
226
val fs = FileSystem.SYSTEM
227
val sha256Digest = createSha256Digest()
228
229
val hash = fs.sink(targetPath).use { targetSink ->
230
val hashingSink = HashingSink(targetSink, sha256Digest)
231
232
fs.source(sourcePath).use { sourceFile ->
233
hashingSink.writeAll(sourceFile)
234
}
235
236
hashingSink.hash
237
}
238
239
println("File copied with SHA-256: ${hash.hex()}")
240
return hash
241
}
242
```
243
244
## Password Hashing and Key Derivation
245
246
While Okio doesn't provide built-in password hashing functions like bcrypt or Argon2, you can use HMAC for key derivation:
247
248
### PBKDF2-style Key Derivation
249
250
```kotlin
251
// Simple PBKDF2-style key derivation using HMAC-SHA256
252
fun deriveKey(password: String, salt: ByteString, iterations: Int, keyLength: Int): ByteString {
253
var derivedKey = (password + salt.utf8()).encodeUtf8()
254
255
repeat(iterations) {
256
derivedKey = derivedKey.hmacSha256(salt)
257
}
258
259
// Truncate or extend to desired length
260
return if (derivedKey.size >= keyLength) {
261
derivedKey.substring(0, keyLength)
262
} else {
263
// For simplicity, just repeat if needed (not cryptographically ideal)
264
val buffer = Buffer()
265
while (buffer.size < keyLength) {
266
buffer.write(derivedKey)
267
}
268
buffer.readByteString(keyLength.toLong())
269
}
270
}
271
272
// Usage
273
val password = "user-password"
274
val salt = "random-salt-12345".encodeUtf8()
275
val iterations = 10000
276
val keyLength = 32 // 256 bits
277
278
val derivedKey = deriveKey(password, salt, iterations, keyLength)
279
println("Derived key: ${derivedKey.hex()}")
280
281
// Use the same inputs to verify
282
val verificationKey = deriveKey(password, salt, iterations, keyLength)
283
println("Keys match: ${derivedKey == verificationKey}")
284
```
285
286
## Data Verification and Checksums
287
288
### Message Authentication
289
290
```kotlin
291
// Create authenticated message with HMAC
292
data class AuthenticatedMessage(
293
val data: ByteString,
294
val signature: ByteString
295
) {
296
companion object {
297
fun create(message: String, secretKey: ByteString): AuthenticatedMessage {
298
val data = message.encodeUtf8()
299
val signature = data.hmacSha256(secretKey)
300
return AuthenticatedMessage(data, signature)
301
}
302
}
303
304
fun verify(secretKey: ByteString): Boolean {
305
val expectedSignature = data.hmacSha256(secretKey)
306
return expectedSignature == signature
307
}
308
309
fun getMessage(): String = data.utf8()
310
}
311
312
// Usage
313
val secretKey = "shared-secret-key".encodeUtf8()
314
val message = "This is a secure message that needs authentication."
315
316
// Create authenticated message
317
val authMessage = AuthenticatedMessage.create(message, secretKey)
318
println("Message: ${authMessage.getMessage()}")
319
println("Signature: ${authMessage.signature.hex()}")
320
321
// Verify message
322
val isValid = authMessage.verify(secretKey)
323
println("Message is valid: $isValid")
324
325
// Test with wrong key
326
val wrongKey = "wrong-secret-key".encodeUtf8()
327
val isValidWithWrongKey = authMessage.verify(wrongKey)
328
println("Message valid with wrong key: $isValidWithWrongKey")
329
```
330
331
### File Checksum Utilities
332
333
```kotlin
334
// Generate checksum file (like sha256sum)
335
fun generateChecksumFile(filePaths: List<Path>, checksumPath: Path) {
336
val fs = FileSystem.SYSTEM
337
338
fs.write(checksumPath) {
339
filePaths.forEach { filePath ->
340
if (fs.exists(filePath) && fs.metadata(filePath).isRegularFile) {
341
val content = fs.read(filePath) { readByteString() }
342
val hash = content.sha256()
343
writeUtf8("${hash.hex()} ${filePath.name}\n")
344
}
345
}
346
}
347
}
348
349
// Verify checksums
350
fun verifyChecksums(checksumPath: Path, baseDir: Path): List<Pair<String, Boolean>> {
351
val fs = FileSystem.SYSTEM
352
val results = mutableListOf<Pair<String, Boolean>>()
353
354
fs.read(checksumPath) {
355
while (!exhausted()) {
356
val line = readUtf8Line() ?: break
357
val parts = line.split(" ", limit = 2)
358
359
if (parts.size == 2) {
360
val expectedHash = parts[0]
361
val fileName = parts[1]
362
val filePath = baseDir / fileName
363
364
if (fs.exists(filePath)) {
365
val actualContent = fs.read(filePath) { readByteString() }
366
val actualHash = actualContent.sha256().hex()
367
val matches = expectedHash.equals(actualHash, ignoreCase = true)
368
results.add(fileName to matches)
369
370
println("$fileName: ${if (matches) "OK" else "FAILED"}")
371
} else {
372
results.add(fileName to false)
373
println("$fileName: NOT FOUND")
374
}
375
}
376
}
377
}
378
379
return results
380
}
381
382
// Usage
383
val testDir = "/tmp/checksum-test".toPath()
384
val fs = FileSystem.SYSTEM
385
386
fs.createDirectory(testDir)
387
388
// Create test files
389
val testFiles = listOf("file1.txt", "file2.txt", "file3.txt")
390
testFiles.forEach { fileName ->
391
fs.write(testDir / fileName) {
392
writeUtf8("Content of $fileName")
393
}
394
}
395
396
// Generate checksums
397
val checksumFile = testDir / "checksums.sha256"
398
generateChecksumFile(testFiles.map { testDir / it }, checksumFile)
399
400
// Verify checksums
401
println("Checksum verification:")
402
val results = verifyChecksums(checksumFile, testDir)
403
val allValid = results.all { it.second }
404
println("All files valid: $allValid")
405
```
406
407
## Security Best Practices
408
409
### Constant-Time Comparison
410
411
```kotlin
412
// Prevent timing attacks when comparing hashes
413
fun constantTimeEquals(a: ByteString, b: ByteString): Boolean {
414
if (a.size != b.size) return false
415
416
var result = 0
417
for (i in 0 until a.size) {
418
result = result or (a[i].toInt() xor b[i].toInt())
419
}
420
421
return result == 0
422
}
423
424
// Secure hash comparison
425
fun verifyPasswordHash(password: String, salt: ByteString, storedHash: ByteString): Boolean {
426
val inputHash = (password + salt.utf8()).encodeUtf8().sha256()
427
return constantTimeEquals(inputHash, storedHash)
428
}
429
```
430
431
### Random Salt Generation
432
433
```kotlin
434
// Generate cryptographically secure random salt
435
fun generateSalt(length: Int = 16): ByteString {
436
val random = java.security.SecureRandom()
437
val saltBytes = ByteArray(length)
438
random.nextBytes(saltBytes)
439
return saltBytes.toByteString()
440
}
441
442
// Usage in password hashing
443
fun hashPassword(password: String): Pair<ByteString, ByteString> {
444
val salt = generateSalt()
445
val hash = (password + salt.utf8()).encodeUtf8().sha256()
446
return Pair(salt, hash)
447
}
448
449
val (salt, hash) = hashPassword("user-password")
450
println("Salt: ${salt.hex()}")
451
println("Hash: ${hash.hex()}")
452
```
453
454
## Performance Considerations
455
456
### Choosing Hash Functions
457
458
```kotlin
459
// Benchmark different hash functions
460
fun benchmarkHashFunctions(data: ByteString) {
461
val hashFunctions = listOf(
462
"MD5" to { data: ByteString -> data.md5() },
463
"SHA-1" to { data: ByteString -> data.sha1() },
464
"SHA-256" to { data: ByteString -> data.sha256() },
465
"SHA-512" to { data: ByteString -> data.sha512() }
466
)
467
468
hashFunctions.forEach { (name, hashFunc) ->
469
val startTime = System.nanoTime()
470
val hash = hashFunc(data)
471
val endTime = System.nanoTime()
472
473
val durationMs = (endTime - startTime) / 1_000_000.0
474
println("$name: ${hash.hex().take(16)}... (${durationMs}ms)")
475
}
476
}
477
478
// Test with different data sizes
479
val smallData = "Small test data".encodeUtf8()
480
val largeData = "Large test data. ".repeat(10000).encodeUtf8()
481
482
println("Small data (${smallData.size} bytes):")
483
benchmarkHashFunctions(smallData)
484
485
println("\nLarge data (${largeData.size} bytes):")
486
benchmarkHashFunctions(largeData)
487
```
488
489
### Memory-Efficient Hashing
490
491
```kotlin
492
// Hash large files without loading into memory
493
fun hashLargeFile(filePath: Path): ByteString {
494
val fs = FileSystem.SYSTEM
495
val buffer = Buffer()
496
497
fs.source(filePath).buffer().use { source ->
498
while (!source.exhausted()) {
499
val chunk = source.readByteString(minOf(65536L, source.buffer.size)) // 64KB chunks
500
buffer.write(chunk)
501
}
502
}
503
504
return buffer.sha256()
505
}
506
```
507
508
## Error Handling
509
510
Hash operations are generally safe, but I/O operations during hashing can fail:
511
512
```kotlin
513
// Robust hash calculation with error handling
514
fun safeHashFile(filePath: Path): ByteString? {
515
return try {
516
val fs = FileSystem.SYSTEM
517
fs.read(filePath) { readByteString() }.sha256()
518
} catch (e: FileNotFoundException) {
519
println("File not found: $filePath")
520
null
521
} catch (e: IOException) {
522
println("I/O error reading file: ${e.message}")
523
null
524
} catch (e: Exception) {
525
println("Unexpected error: ${e.message}")
526
null
527
}
528
}
529
530
// Verify multiple files with error handling
531
fun verifyFileHashes(fileHashes: Map<Path, String>): Map<Path, String> {
532
val results = mutableMapOf<Path, String>()
533
534
fileHashes.forEach { (path, expectedHash) ->
535
val actualHash = safeHashFile(path)
536
val status = when {
537
actualHash == null -> "ERROR"
538
actualHash.hex().equals(expectedHash, ignoreCase = true) -> "OK"
539
else -> "MISMATCH"
540
}
541
results[path] = status
542
println("${path.name}: $status")
543
}
544
545
return results
546
}
547
```