-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* CipherSink * CipherSource * Simplify CipherSource * Simplify by requiring block cipher * Cipher tests with single byte updates Failing for cipher source * Fix wrong segment pop * Cipher tests with empty data Sink test is failing * Fix cipher test & simplify condition in close * Extract cipher sink doFinal method * Use random seeds * Add failing Cipher test for RSA * Revert "Add failing Cipher test for RSA" This reverts commit 52b8662. There's no point in trying to support it, and it would be very difficult since it's not a block cipher. * Parameterize cipher tests Test all block ciphers which are required to be implemented on JVM, as per the documentation of the `Cipher` class. * Minor optimization to Cipher stream doFinal If `getOutputSize` returns zero, exit early. * Standard file structure License, JVM file name, inline extension. * Javadoc * Extensions on `Cipher` for source/sink Classes are now private * Fix cipher source update size Noticed issue when simplifying test by using `ForwardingSource`, which changed segment usage. * Missing word in comment * Test padding * Remove useless visibility restriction Since classes are now private, there's no point in marking constructor as internal * Fix lint issue * Cryptography documentation * Fix lint check * Apply pull request feedback Co-authored-by: Martin Devillers <[email protected]>
- Loading branch information
Martin Devillers
and
Martin Devillers
authored
Oct 4, 2020
1 parent
6344cc1
commit da3112e
Showing
6 changed files
with
708 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/* | ||
* Copyright (C) 2020 Square, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
@file:JvmMultifileClass | ||
@file:JvmName("Okio") | ||
|
||
package okio | ||
|
||
import java.io.IOException | ||
import javax.crypto.Cipher | ||
|
||
private class CipherSink( | ||
private val sink: BufferedSink, | ||
private val cipher: Cipher | ||
) : Sink { | ||
|
||
private val blockSize = cipher.blockSize | ||
private var closed = false | ||
|
||
init { | ||
// Require block cipher, and check for unsupported (too large) block size (should never happen with standard algorithms) | ||
require(blockSize > 0) { "Block cipher required $cipher" } | ||
require(blockSize <= Segment.SIZE) { "Cipher block size $blockSize too large $cipher" } | ||
} | ||
|
||
@Throws(IOException::class) | ||
override fun write(source: Buffer, byteCount: Long) { | ||
checkOffsetAndCount(source.size, 0, byteCount) | ||
check(!closed) { "closed" } | ||
|
||
var remaining = byteCount | ||
while (remaining > 0) { | ||
val size = update(source, remaining) | ||
remaining -= size | ||
} | ||
} | ||
|
||
private fun update(source: Buffer, remaining: Long): Int { | ||
val head = source.head!! | ||
val size = minOf(remaining, head.limit - head.pos).toInt() | ||
val buffer = sink.buffer | ||
|
||
// For block cipher, output size cannot exceed input size in update | ||
val s = buffer.writableSegment(size) | ||
|
||
val ciphered = cipher.update(head.data, head.pos, size, s.data, s.limit) | ||
|
||
s.limit += ciphered | ||
buffer.size += ciphered | ||
|
||
if (s.pos == s.limit) { | ||
// We allocated a tail segment, but didn't end up needing it. Recycle! | ||
buffer.head = s.pop() | ||
SegmentPool.recycle(s) | ||
} | ||
|
||
// Mark those bytes as read. | ||
source.size -= size | ||
head.pos += size | ||
|
||
if (head.pos == head.limit) { | ||
source.head = head.pop() | ||
SegmentPool.recycle(head) | ||
} | ||
return size | ||
} | ||
|
||
override fun flush() = | ||
sink.flush() | ||
|
||
override fun timeout() = | ||
sink.timeout() | ||
|
||
@Throws(IOException::class) | ||
override fun close() { | ||
if (closed) return | ||
closed = true | ||
|
||
var thrown = doFinal() | ||
|
||
try { | ||
sink.close() | ||
} catch (e: Throwable) { | ||
if (thrown == null) thrown = e | ||
} | ||
|
||
if (thrown != null) throw thrown | ||
} | ||
|
||
private fun doFinal(): Throwable? { | ||
val outputSize = cipher.getOutputSize(0) | ||
if (outputSize == 0) return null | ||
|
||
var thrown: Throwable? = null | ||
val buffer = sink.buffer | ||
|
||
// For block cipher, output size cannot exceed block size in doFinal | ||
val s = buffer.writableSegment(outputSize) | ||
|
||
try { | ||
val ciphered = cipher.doFinal(s.data, s.limit) | ||
|
||
s.limit += ciphered | ||
buffer.size += ciphered | ||
} catch (e: Throwable) { | ||
thrown = e | ||
} | ||
|
||
if (s.pos == s.limit) { | ||
buffer.head = s.pop() | ||
SegmentPool.recycle(s) | ||
} | ||
|
||
return thrown | ||
} | ||
} | ||
|
||
/** | ||
* Returns a [Sink] that processes data using this [Cipher] while writing to | ||
* [sink]. | ||
* | ||
* @throws IllegalArgumentException | ||
* If this isn't a block cipher. | ||
*/ | ||
fun Cipher.sink(sink: BufferedSink): Sink = | ||
CipherSink(sink, this) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/* | ||
* Copyright (C) 2020 Square, Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
@file:JvmMultifileClass | ||
@file:JvmName("Okio") | ||
|
||
package okio | ||
|
||
import java.io.IOException | ||
import javax.crypto.Cipher | ||
|
||
private class CipherSource( | ||
private val source: BufferedSource, | ||
private val cipher: Cipher | ||
) : Source { | ||
|
||
private val blockSize = cipher.blockSize | ||
private val buffer = Buffer() | ||
private var final = false | ||
private var closed = false | ||
|
||
init { | ||
// Require block cipher, and check for unsupported (too large) block size (should never happen with standard algorithms) | ||
require(blockSize > 0) { "Block cipher required $cipher" } | ||
require(blockSize <= Segment.SIZE) { "Cipher block size $blockSize too large $cipher" } | ||
} | ||
|
||
@Throws(IOException::class) | ||
override fun read(sink: Buffer, byteCount: Long): Long { | ||
require(byteCount >= 0) { "byteCount < 0: $byteCount" } | ||
check(!closed) { "closed" } | ||
if (byteCount == 0L) return 0 | ||
if (final) return buffer.read(sink, byteCount) | ||
|
||
refill() | ||
|
||
return buffer.read(sink, byteCount) | ||
} | ||
|
||
private fun refill() { | ||
while (buffer.size == 0L) { | ||
if (source.exhausted()) { | ||
final = true | ||
doFinal() | ||
break | ||
} else { | ||
update() | ||
} | ||
} | ||
} | ||
|
||
private fun update() { | ||
val head = source.buffer.head!! | ||
val size = head.limit - head.pos | ||
|
||
// For block cipher, output size cannot exceed input size in update | ||
val s = buffer.writableSegment(size) | ||
|
||
val ciphered = | ||
cipher.update(head.data, head.pos, size, s.data, s.pos) | ||
|
||
source.skip(size.toLong()) | ||
|
||
s.limit += ciphered | ||
buffer.size += ciphered | ||
|
||
if (s.pos == s.limit) { | ||
// We allocated a tail segment, but didn't end up needing it. Recycle! | ||
buffer.head = s.pop() | ||
SegmentPool.recycle(s) | ||
} | ||
} | ||
|
||
private fun doFinal() { | ||
val outputSize = cipher.getOutputSize(0) | ||
if (outputSize == 0) return | ||
|
||
// For block cipher, output size cannot exceed block size in doFinal | ||
val s = buffer.writableSegment(outputSize) | ||
|
||
val ciphered = cipher.doFinal(s.data, s.pos) | ||
|
||
s.limit += ciphered | ||
buffer.size += ciphered | ||
|
||
if (s.pos == s.limit) { | ||
// We allocated a tail segment, but didn't end up needing it. Recycle! | ||
buffer.head = s.pop() | ||
SegmentPool.recycle(s) | ||
} | ||
} | ||
|
||
override fun timeout() = | ||
source.timeout() | ||
|
||
@Throws(IOException::class) | ||
override fun close() { | ||
closed = true | ||
source.close() | ||
} | ||
} | ||
|
||
/** | ||
* Returns a [Source] that processes data using this [Cipher] while reading | ||
* from [source]. | ||
* | ||
* @throws IllegalArgumentException | ||
* If this isn't a block cipher. | ||
*/ | ||
fun Cipher.source(source: BufferedSource): Source = | ||
CipherSource(source, this) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package okio | ||
|
||
import javax.crypto.Cipher | ||
import javax.crypto.spec.IvParameterSpec | ||
import javax.crypto.spec.SecretKeySpec | ||
import kotlin.random.Random | ||
|
||
class CipherFactory( | ||
private val transformation: String, | ||
private val init: Cipher.(mode: Int) -> Unit | ||
) { | ||
val blockSize | ||
get() = cipher.blockSize | ||
|
||
val encrypt: Cipher | ||
get() = create(Cipher.ENCRYPT_MODE) | ||
|
||
val decrypt: Cipher | ||
get() = create(Cipher.DECRYPT_MODE) | ||
|
||
private val cipher: Cipher | ||
get() = Cipher.getInstance(transformation) | ||
|
||
private fun create(mode: Int): Cipher = | ||
cipher.apply { init(mode) } | ||
} | ||
|
||
data class CipherAlgorithm( | ||
val transformation: String, | ||
val padding: Boolean, | ||
val keyLength: Int, | ||
val ivLength: Int? = null | ||
) { | ||
fun createCipherFactory(random: Random): CipherFactory { | ||
val key = random.nextBytes(keyLength) | ||
val secretKeySpec = SecretKeySpec(key, transformation.substringBefore('/')) | ||
return if (ivLength == null) { | ||
CipherFactory(transformation) { mode -> | ||
init(mode, secretKeySpec) | ||
} | ||
} else { | ||
val iv = random.nextBytes(ivLength) | ||
val ivParameterSpec = IvParameterSpec(iv) | ||
CipherFactory(transformation) { mode -> | ||
init(mode, secretKeySpec, ivParameterSpec) | ||
} | ||
} | ||
} | ||
|
||
override fun toString(): String = | ||
transformation | ||
|
||
companion object { | ||
fun getBlockCipherAlgorithms() = listOf( | ||
CipherAlgorithm("AES/CBC/NoPadding", false, 16, 16), | ||
CipherAlgorithm("AES/CBC/PKCS5Padding", true, 16, 16), | ||
CipherAlgorithm("AES/ECB/NoPadding", false, 16), | ||
CipherAlgorithm("AES/ECB/PKCS5Padding", true, 16), | ||
CipherAlgorithm("DES/CBC/NoPadding", false, 8, 8), | ||
CipherAlgorithm("DES/CBC/PKCS5Padding", true, 8, 8), | ||
CipherAlgorithm("DES/ECB/NoPadding", false, 8), | ||
CipherAlgorithm("DES/ECB/PKCS5Padding", true, 8), | ||
CipherAlgorithm("DESede/CBC/NoPadding", false, 24, 8), | ||
CipherAlgorithm("DESede/CBC/PKCS5Padding", true, 24, 8), | ||
CipherAlgorithm("DESede/ECB/NoPadding", false, 24), | ||
CipherAlgorithm("DESede/ECB/PKCS5Padding", true, 24) | ||
) | ||
} | ||
} |
Oops, something went wrong.