From 1cf83cc168b9d0c68c3ab49dc163a8899ca44fc1 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 11:04:14 -0500 Subject: [PATCH 1/9] Fix up encoding benchmarks to not use loops --- .../encoding/benchmarks/Constants.kt | 2 +- .../encoding/benchmarks/EncoderDecoderOpts.kt | 60 +++++++------------ .../benchmarks/FeedBufferBenchmark.kt | 7 +-- 3 files changed, 22 insertions(+), 47 deletions(-) diff --git a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/Constants.kt b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/Constants.kt index 4ed676a2..c3a079f9 100644 --- a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/Constants.kt +++ b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/Constants.kt @@ -17,7 +17,7 @@ package io.matthewnelson.encoding.benchmarks const val ENC_ITERATIONS_WARMUP = 5 const val ENC_ITERATIONS_MEASURE = 5 -const val ENC_TIME_WARMUP = 1 +const val ENC_TIME_WARMUP = 2 const val ENC_TIME_MEASURE = 3 const val TIME_QUICK = "quick-time" diff --git a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt index 4167edf3..4dce0059 100644 --- a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt +++ b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt @@ -16,63 +16,43 @@ package io.matthewnelson.encoding.benchmarks import io.matthewnelson.encoding.base16.Base16 -import io.matthewnelson.encoding.base32.Base32 import io.matthewnelson.encoding.base32.Base32Crockford import io.matthewnelson.encoding.base32.Base32Default import io.matthewnelson.encoding.base32.Base32Hex import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder -import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import io.matthewnelson.encoding.core.Encoder import io.matthewnelson.encoding.core.EncoderDecoder import kotlinx.benchmark.* abstract class EncoderDecoderBenchmarkBase { - // ":" + // "::" abstract var params: String protected abstract fun encoder(isConstantTime: Boolean): EncoderDecoder<*> - private var bytes = ByteArray(0) - private var chars = "_" + private var byte: Byte = 0 + private var char = '_' private var feedDecoder: Decoder<*>.Feed = Base16.newDecoderFeed {}.apply { close() } private var feedEncoder: Encoder<*>.Feed = Base16.newEncoderFeed {}.apply { close() } @Setup fun setup() { - val (chars, isConstantTime) = params.split(':').let { - it[0] to (it[1] == TIME_CONST) + val encoder = params.split(':').let { params -> + char = params[0][0] + byte = params[1].toByte() + encoder(params[2] == TIME_CONST) } - val encoder = encoder(isConstantTime) - val (cLength, bSize) = when (encoder) { - is Base16 -> 2 to 1 - is Base32<*> -> 8 to 5 - is Base64 -> 4 to 3 - else -> error("Unknown encoder >> $encoder") - } - - this.bytes = chars.decodeToByteArray(encoder) - this.chars = chars - require(this.bytes.size == bSize) { - "bytes.size[${this.bytes.size}] did not match expected size[$bSize] for $encoder" - } - require(this.chars.length == cLength) { - "chars.length[${this.chars.length}] did not match expected length[$cLength] for $encoder" - } feedDecoder = encoder.newDecoderFeed {} feedEncoder = encoder.newEncoderFeed {} } @Benchmark - fun decode() { - chars.forEach { c -> feedDecoder.consume(c) } - } + fun decode() { feedDecoder.consume(char) } @Benchmark - fun encode() { - bytes.forEach { b -> feedEncoder.consume(b) } - } + fun encode() { feedEncoder.consume(byte) } } @State(Scope.Benchmark) @@ -82,10 +62,10 @@ abstract class EncoderDecoderBenchmarkBase { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base16Benchmark: EncoderDecoderBenchmarkBase() { // CHARS: 0123456789ABCDEF - @Param("0A:$TIME_QUICK", "E8:$TIME_QUICK", "0A:$TIME_CONST", "E8:$TIME_CONST") - override var params: String = ":" + @Param("3:0:$TIME_CONST", "d:122:$TIME_CONST") + override var params: String = "::" override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { - return Base16 { this.isConstantTime = isConstantTime } + return Base16() } } @@ -96,8 +76,8 @@ open class Base16Benchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base32CrockfordBenchmark: EncoderDecoderBenchmarkBase() { // CHARS: 0123456789ABCDEFGHJKMNPQRSTVWXYZ - @Param("0AC3DFJ7:$TIME_QUICK", "T9WYR8SZ:$TIME_QUICK", "0AC3DFJ7:$TIME_CONST", "T9WYR8SZ:$TIME_CONST") - override var params: String = ":" + @Param("3:-6:$TIME_QUICK", "x:115:$TIME_QUICK", "3:-6:$TIME_CONST", "x:115:$TIME_CONST") + override var params: String = "::" override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { return Base32Crockford { this.isConstantTime = isConstantTime } } @@ -110,8 +90,8 @@ open class Base32CrockfordBenchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base32DefaultBenchmark: EncoderDecoderBenchmarkBase() { // CHARS: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 - @Param("CA2EF3CB:$TIME_QUICK", "WSY2V4ZZ:$TIME_QUICK", "CA2EF3CB:$TIME_CONST", "WSY2V4ZZ:$TIME_CONST") - override var params: String = ":" + @Param("C:-123:$TIME_QUICK", "w:15:$TIME_QUICK", "C:-123:$TIME_CONST", "w:15:$TIME_CONST") + override var params: String = "::" override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { return Base32Default { this.isConstantTime = isConstantTime } } @@ -124,8 +104,8 @@ open class Base32DefaultBenchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base32HexBenchmark: EncoderDecoderBenchmarkBase() { // CHARS: 0123456789ABCDEFGHIJKLMNOPQRSTUV - @Param("A3B4CC2A:$TIME_QUICK", "V7RS4JM6:$TIME_QUICK", "A3B4CC2A:$TIME_CONST", "V7RS4JM6:$TIME_CONST") - override var params: String = ":" + @Param("A:-12:$TIME_QUICK", "r:42:$TIME_QUICK", "A:-12:$TIME_CONST", "r:42:$TIME_CONST") + override var params: String = "::" override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { return Base32Hex { this.isConstantTime = isConstantTime } } @@ -138,8 +118,8 @@ open class Base32HexBenchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base64Benchmark: EncoderDecoderBenchmarkBase() { // CHARS: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 - @Param("0CaI:$TIME_QUICK", "9tvw:$TIME_QUICK", "0CaI:$TIME_CONST", "9tvw:$TIME_CONST") - override var params: String = ":" + @Param("2:84:$TIME_QUICK", "w:22:$TIME_QUICK", "2:84:$TIME_CONST", "w:22:$TIME_CONST") + override var params: String = "::" override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { return Base64 { this.isConstantTime = isConstantTime } } diff --git a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/FeedBufferBenchmark.kt b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/FeedBufferBenchmark.kt index 4627d48d..bb475a44 100644 --- a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/FeedBufferBenchmark.kt +++ b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/FeedBufferBenchmark.kt @@ -26,16 +26,11 @@ import kotlinx.benchmark.* open class FeedBufferBenchmark { private val buffer = object : FeedBuffer( - blockSize = 3, + blockSize = 10, flush = Flush { _ -> }, finalize = Finalize { _, _ -> } ) {} - @TearDown - fun finalize() { - buffer.finalize() - } - @Benchmark fun update() { repeat(buffer.blockSize) { buffer.update(it) } From 7a0f8dca70eca109b629948b1912d48c191cfca2 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 11:04:28 -0500 Subject: [PATCH 2/9] Remove now-unnecessary benchmarks --- .../encoding/benchmarks/CTCaseBenchmark.kt | 39 --------------- .../benchmarks/DecoderActionBenchmark.kt | 47 ------------------- 2 files changed, 86 deletions(-) delete mode 100644 benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/CTCaseBenchmark.kt delete mode 100644 benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/DecoderActionBenchmark.kt diff --git a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/CTCaseBenchmark.kt b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/CTCaseBenchmark.kt deleted file mode 100644 index b75b1dd5..00000000 --- a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/CTCaseBenchmark.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2024 Matthew Nelson - * - * 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 - * - * https://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. - **/ -package io.matthewnelson.encoding.benchmarks - -import io.matthewnelson.encoding.core.util.CTCase -import kotlinx.benchmark.* - -@State(Scope.Benchmark) -@BenchmarkMode(Mode.AverageTime) -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 3) -@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS) -open class CTCaseBenchmark { - - private val case = CTCase("ABCDEFGH") - - @Benchmark - fun lowercaseFirst(): Char = case.lowercase('A')!! - @Benchmark - fun lowercaseLast(): Char = case.lowercase('H')!! - - @Benchmark - fun uppercaseFirst(): Char = case.uppercase('a')!! - @Benchmark - fun uppercaseLast(): Char = case.uppercase('h')!! -} diff --git a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/DecoderActionBenchmark.kt b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/DecoderActionBenchmark.kt deleted file mode 100644 index b85dca3f..00000000 --- a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/DecoderActionBenchmark.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2024 Matthew Nelson - * - * 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 - * - * https://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. - **/ -package io.matthewnelson.encoding.benchmarks - -import io.matthewnelson.encoding.core.util.DecoderAction -import kotlinx.benchmark.* - -@State(Scope.Benchmark) -@BenchmarkMode(Mode.AverageTime) -@Warmup(iterations = 5, time = 1) -@Measurement(iterations = 5, time = 3) -@OutputTimeUnit(BenchmarkTimeUnit.NANOSECONDS) -open class DecoderActionBenchmark { - - @Param(TIME_QUICK, TIME_CONST) - var params: String = "-" - - private var isConstantTime = false - private val parser = DecoderAction.Parser( - '0'..'9' to DecoderAction { 0 }, - 'A'..'F' to DecoderAction { 0 }, - ) - - @Setup - fun setup() { - isConstantTime = params == TIME_CONST - } - - @Benchmark - fun actionFirst() = parser.parse('0', isConstantTime) - - @Benchmark - fun actionLast() = parser.parse('F', isConstantTime) -} From 2db93e7c59f2cc350b41f4bd271c5db0a4f552f5 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 11:04:50 -0500 Subject: [PATCH 3/9] Clean up documentation --- .../io/matthewnelson/encoding/core/Decoder.kt | 21 +++-------- .../io/matthewnelson/encoding/core/Encoder.kt | 36 +++++++------------ 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt index 98616c35..b0271824 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Decoder.kt @@ -35,24 +35,13 @@ public sealed class Decoder(public val config: C) { * Creates a new [Decoder.Feed], outputting decoded data to * the supplied [Decoder.OutFeed]. * - * e.g. (Reading a file of encoded data) + * e.g. * - * val sb = StringBuilder() - * file.inputStream().reader().use { iStream -> - * myDecoder.newDecoderFeed { decodedByte -> - * sb.append(decodedByte.toInt().toChar()) - * }.use { feed -> - * val buffer = CharArray(4096) - * while (true) { - * val read = iStream.read(buffer) - * if (read == -1) break - * for (i in 0 until read) { - * feed.consume(buffer[i]) - * } - * } - * } + * myDecoder.newDecoderFeed { decodedByte -> + * println(decodedByte) + * }.use { feed -> + * "MYencoDEdTEXt".forEach { c -> feed.consume(c) } * } - * println(sb.toString()) * * @see [Decoder.Feed] * */ diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Encoder.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Encoder.kt index c37eecdc..a90d94d1 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Encoder.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/Encoder.kt @@ -38,17 +38,17 @@ public sealed class Encoder(config: C): Decoder(con * Creates a new [Encoder.Feed], outputting encoded data to * the supplied [Encoder.OutFeed]. * - * e.g. (Writing encoded data to a file) + * e.g. * - * file.outputStream().use { oStream -> - * myEncoder.newEncoderFeed { encodedChar -> - * oStream.write(encodedChar.code) - * }.use { feed -> - * "Hello World!".forEach { c -> - * feed.consume(c.code.toByte()) - * } - * } + * val sb = StringBuilder() + * myEncoder.newEncoderFeed { encodedChar -> + * sb.append(encodedChar) + * }.use { feed -> + * "Hello World!" + * .encodeToByteArray() + * .forEach { b -> feed.consume(b) } * } + * println(sb.toString()) * * @see [Encoder.Feed] * */ @@ -148,11 +148,7 @@ public sealed class Encoder(config: C): Decoder(con * returns the encoded data in the form of a [String]. * * @throws [EncodingSizeException] if the encoded output - * exceeds [Int.MAX_VALUE]. This is **not applicable** for - * most encoding specifications as the majority compress - * data, but is something that can occur with Base16 (hex) - * as it produces 2 characters of output for every 1 byte - * of input. + * exceeds [Int.MAX_VALUE]. * */ @JvmStatic @Throws(EncodingSizeException::class) @@ -173,11 +169,7 @@ public sealed class Encoder(config: C): Decoder(con * returns the encoded data in the form of a [CharArray]. * * @throws [EncodingSizeException] if the encoded output - * exceeds [Int.MAX_VALUE]. This is **not applicable** for - * most encoding specifications as the majority compress - * data, but is something that can occur with Base16 (hex) - * as it produces 2 characters of output for every 1 byte - * of input. + * exceeds [Int.MAX_VALUE]. * */ @JvmStatic @Throws(EncodingSizeException::class) @@ -195,11 +187,7 @@ public sealed class Encoder(config: C): Decoder(con * returns the encoded data in the form of a [ByteArray]. * * @throws [EncodingSizeException] if the encoded output - * exceeds [Int.MAX_VALUE]. This is **not applicable** for - * most encoding specifications as the majority compress - * data, but is something that can occur with Base16 (hex) - * as it produces 2 characters of output for every 1 byte - * of input. + * exceeds [Int.MAX_VALUE]. * @suppress * */ @JvmStatic From 66313fb5b89833b590a384a5824303555df0fb96 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 11:05:26 -0500 Subject: [PATCH 4/9] Fix constant-time Base16 implementation --- README.md | 11 +- library/base16/api/base16.api | 2 +- library/base16/api/base16.klib.api | 4 +- .../matthewnelson/encoding/base16/Base16.kt | 129 ++++++------------ .../matthewnelson/encoding/base16/Builders.kt | 38 +----- .../encoding/base16/Base16UnitTest.kt | 29 +--- 6 files changed, 57 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index 15c2fe80..1b7b424c 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,6 @@ val base16 = Base16 { // Use lowercase instead of uppercase characters when encoding encodeToLowercase = true - - // Use constant-time operations when encoding/decoding sensitive data - isConstantTime = true } // Shortcuts @@ -66,7 +63,6 @@ Base16 val base32Crockford = Base32Crockford { isLenient = true encodeToLowercase = false - isConstantTime = true // Insert hyphens every X characters of encoded output hyphenInterval = 5 @@ -87,7 +83,6 @@ val base32Default = Base32Default { isLenient = true lineBreakInterval = 64 encodeToLowercase = true - isConstantTime = true // Skip padding of the encoded output padEncoded = false @@ -101,7 +96,6 @@ val base32Hex = Base32Hex { lineBreakInterval = 64 encodeToLowercase = false padEncoded = true - isConstantTime = true } // Alternatively, use the static instance with its default settings @@ -116,7 +110,6 @@ val base64 = Base64 { lineBreakInterval = 64 encodeToUrlSafe = false padEncoded = true - isConstantTime = true } // Alternatively, use the static instance with its default settings @@ -204,8 +197,8 @@ file.outputStream().use { oStream -> // automatically, which closes the `Encoder.Feed` // and performs finalization of the operation (such as // adding padding). - "Hello World!".forEach { c -> - feed.consume(c.code.toByte()) + "Hello World!".encodeToByteArray().forEach { b -> + feed.consume(b) } } } diff --git a/library/base16/api/base16.api b/library/base16/api/base16.api index f6af86a9..31b61ba1 100644 --- a/library/base16/api/base16.api +++ b/library/base16/api/base16.api @@ -19,7 +19,7 @@ public final class io/matthewnelson/encoding/base16/Base16$Companion : io/matthe public final class io/matthewnelson/encoding/base16/Base16$Config : io/matthewnelson/encoding/core/EncoderDecoder$Config { public final field encodeToLowercase Z public final field isConstantTime Z - public synthetic fun (ZBZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZBZLkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class io/matthewnelson/encoding/base16/Base16ConfigBuilder { diff --git a/library/base16/api/base16.klib.api b/library/base16/api/base16.klib.api index f250ddcd..987b1ede 100644 --- a/library/base16/api/base16.klib.api +++ b/library/base16/api/base16.klib.api @@ -12,7 +12,7 @@ final class io.matthewnelson.encoding.base16/Base16 : io.matthewnelson.encoding. final class Config : io.matthewnelson.encoding.core/EncoderDecoder.Config { // io.matthewnelson.encoding.base16/Base16.Config|null[0] final val encodeToLowercase // io.matthewnelson.encoding.base16/Base16.Config.encodeToLowercase|{}encodeToLowercase[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base16/Base16.Config.encodeToLowercase.|(){}[0] - final val isConstantTime // io.matthewnelson.encoding.base16/Base16.Config.isConstantTime|{}isConstantTime[0] + final val isConstantTime // io.matthewnelson.encoding.base16/Base16.Config.isConstantTime|(){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base16/Base16.Config.isConstantTime.|(){}[0] } @@ -31,7 +31,7 @@ final class io.matthewnelson.encoding.base16/Base16ConfigBuilder { // io.matthew final var encodeToLowercase // io.matthewnelson.encoding.base16/Base16ConfigBuilder.encodeToLowercase|(kotlin.Boolean){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base16/Base16ConfigBuilder.encodeToLowercase.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base16/Base16ConfigBuilder.encodeToLowercase.|(kotlin.Boolean){}[0] - final var isConstantTime // io.matthewnelson.encoding.base16/Base16ConfigBuilder.isConstantTime|(kotlin.Boolean){}[0] + final var isConstantTime // io.matthewnelson.encoding.base16/Base16ConfigBuilder.isConstantTime|{}isConstantTime[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base16/Base16ConfigBuilder.isConstantTime.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base16/Base16ConfigBuilder.isConstantTime.|(kotlin.Boolean){}[0] final var isLenient // io.matthewnelson.encoding.base16/Base16ConfigBuilder.isLenient|(kotlin.Boolean){}[0] diff --git a/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt b/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt index 7daf0485..2fb876b5 100644 --- a/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt +++ b/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Base16.kt @@ -18,8 +18,6 @@ package io.matthewnelson.encoding.base16 import io.matthewnelson.encoding.core.* -import io.matthewnelson.encoding.core.util.CTCase -import io.matthewnelson.encoding.core.util.DecoderAction import io.matthewnelson.encoding.core.util.DecoderInput import io.matthewnelson.encoding.core.util.FeedBuffer import kotlin.jvm.JvmField @@ -76,8 +74,6 @@ public class Base16(config: Base16.Config): EncoderDecoder(config lineBreakInterval: Byte, @JvmField public val encodeToLowercase: Boolean, - @JvmField - public val isConstantTime: Boolean, ): EncoderDecoder.Config( isLenient = isLenient, lineBreakInterval = lineBreakInterval, @@ -116,10 +112,16 @@ public class Base16(config: Base16.Config): EncoderDecoder(config isLenient = builder.isLenient, lineBreakInterval = builder.lineBreakInterval, encodeToLowercase = builder.encodeToLowercase, - isConstantTime = builder.isConstantTime, ) } } + + /** + * Implementation is always constant-time. Performance impact is negligible. + * @suppress + * */ + @JvmField + public val isConstantTime: Boolean = true } /** @@ -156,85 +158,57 @@ public class Base16(config: Base16.Config): EncoderDecoder(config protected override fun newEncoderFeedProtected(out: OutFeed): Encoder.Feed { return DELEGATE.newEncoderFeedProtected(out) } - - private val CT_CASE = CTCase(table = CHARS_UPPER) - - private val UC_PARSER = DecoderAction.Parser( - '0'..'9' to DecoderAction { char -> - // char ASCII value - // 0 48 0 - // 9 57 9 (ASCII - 48) - char.code - 48 - }, - CT_CASE.uppers to DecoderAction { char -> - // char ASCII value - // A 65 10 - // F 70 15 (ASCII - 55) - char.code - 55 - }, - CT_CASE.lowers to DecoderAction { char -> - // char ASCII value - // A 65 10 - // F 70 15 (ASCII - 55) - char.uppercaseChar().code - 55 - }, - ) - - // Assume input will be lowercase letters. Reorder - // actions to check lowercase before uppercase. - private val LC_PARSER = DecoderAction.Parser( - UC_PARSER.actions[0], - UC_PARSER.actions[2], - UC_PARSER.actions[1], - ) - - // Do not include lowercase letter actions. Constant time - // operations will uppercase the input on every invocation. - private val CT_PARSER = DecoderAction.Parser( - UC_PARSER.actions[0], - UC_PARSER.actions[1], - ) } protected override fun newDecoderFeedProtected(out: Decoder.OutFeed): Decoder.Feed { return object : Decoder.Feed() { private val buffer = DecodingBuffer(out) - private val parser = when { - config.isConstantTime -> CT_PARSER - config.encodeToLowercase -> LC_PARSER - else -> UC_PARSER - } @Throws(EncodingException::class) override fun consumeProtected(input: Char) { - val char = if (config.isConstantTime) { - CT_CASE.uppercase(input) ?: input - } else { - input - } + val code = input.code + + val ge0: Byte = if (code >= '0'.code) 1 else 0 + val le9: Byte = if (code <= '9'.code) 1 else 0 + val geA: Byte = if (code >= 'A'.code) 1 else 0 + val leF: Byte = if (code <= 'F'.code) 1 else 0 + val gea: Byte = if (code >= 'a'.code) 1 else 0 + val lef: Byte = if (code <= 'f'.code) 1 else 0 + + var diff = 0 - val bits = parser.parse(char, isConstantTime = config.isConstantTime) - ?: throw EncodingException("Char[${input}] is not a valid Base16 character") + // char ASCII value + // 0 48 0 + // 9 57 9 (ASCII - 48) + diff += if (ge0 + le9 == 2) -48 else 0 + + // char ASCII value + // A 65 10 + // F 70 15 (ASCII - 55) + diff += if (geA + leF == 2) -55 else 0 + + // char ASCII value + // a 97 10 + // f 102 15 (ASCII - 87) + diff += if (gea + lef == 2) -87 else 0 + + if (diff == 0) { + throw EncodingException("Char[${input}] is not a valid Base16 character") + } - buffer.update(bits) + buffer.update(code + diff) } @Throws(EncodingException::class) - override fun doFinalProtected() { - buffer.finalize() - } + override fun doFinalProtected() { buffer.finalize() } } } protected override fun newEncoderFeedProtected(out: OutFeed): Encoder.Feed { return object : Encoder.Feed() { - private val table = if (config.encodeToLowercase) { - CHARS_LOWER - } else { - CHARS_UPPER - } + private val table = if (config.encodeToLowercase) CHARS_LOWER else CHARS_UPPER override fun consumeProtected(input: Byte) { // A FeedBuffer is not necessary here as every 1 @@ -244,21 +218,8 @@ public class Base16(config: Base16.Config): EncoderDecoder(config val i1 = bits shr 4 val i2 = bits and 0x0f - if (config.isConstantTime) { - var c1: Char? = null - var c2: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - } - - out.output(c1!!) - out.output(c2!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - } + out.output(table[i1]) + out.output(table[i2]) } override fun doFinalProtected() { /* no-op */ } @@ -274,16 +235,12 @@ public class Base16(config: Base16.Config): EncoderDecoder(config for (bits in buffer) { bitBuffer = (bitBuffer shl 4) or bits } - out.output(bitBuffer.toByte()) }, finalize = { modulus, _-> - when (modulus) { - 0 -> { /* no-op */ } - else -> { - // 4*1 = 4 bits. Truncated, fail. - throw truncatedInputEncodingException(modulus) - } + if (modulus != 0) { + // 4*1 = 4 bits. Truncated, fail. + throw truncatedInputEncodingException(modulus) } } ) diff --git a/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Builders.kt b/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Builders.kt index b406b8ff..736fc643 100644 --- a/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Builders.kt +++ b/library/base16/src/commonMain/kotlin/io/matthewnelson/encoding/base16/Builders.kt @@ -71,44 +71,11 @@ public class Base16ConfigBuilder { public constructor() public constructor(config: Config?): this() { if (config == null) return - isConstantTime = config.isConstantTime isLenient = config.isLenient ?: true lineBreakInterval = config.lineBreakInterval encodeToLowercase = config.encodeToLowercase } - /** - * If true, will utilize constant-time operations when - * encoding/decoding data. This will be slower, but help - * mitigate potential timing attacks with sensitive data - * (such as private key material). - * - * If false, will not use constant time operations. - * - * e.g. (NOT constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * for (c in this) { - * if (c == char) return true - * } - * return false - * } - * - * e.g. (YES constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * var result = false - * for (c in this) { - * result = if (c == char) true else result - * } - * return result - * } - * - * Default: `false` - * */ - @JvmField - public var isConstantTime: Boolean = false - /** * If true, spaces and new lines ('\n', '\r', ' ', '\t') * will be skipped over when decoding (against RFC 4648). @@ -180,4 +147,9 @@ public class Base16ConfigBuilder { * Builds a [Base16.Config] for the provided settings. * */ public fun build(): Config = Config.from(this) + + /** @suppress */ + @JvmField + @Deprecated(message = "Implementation is always constant time. Performance impact is negligible.") + public var isConstantTime: Boolean = true } diff --git a/library/base16/src/commonTest/kotlin/io/matthewnelson/encoding/base16/Base16UnitTest.kt b/library/base16/src/commonTest/kotlin/io/matthewnelson/encoding/base16/Base16UnitTest.kt index 17fa4bbe..81dc6ccf 100644 --- a/library/base16/src/commonTest/kotlin/io/matthewnelson/encoding/base16/Base16UnitTest.kt +++ b/library/base16/src/commonTest/kotlin/io/matthewnelson/encoding/base16/Base16UnitTest.kt @@ -24,11 +24,9 @@ import kotlin.test.assertEquals class Base16UnitTest: BaseNEncodingTest() { - private var useConstantTime = false private var useLowercase = false private fun base16(): Base16 = Base16 { - isConstantTime = useConstantTime encodeToLowercase = useLowercase } @@ -140,36 +138,26 @@ class Base16UnitTest: BaseNEncodingTest() { @Test fun givenString_whenEncoded_MatchesRfc4648Spec() { checkEncodeSuccessForDataSet(encodeSuccessDataSet) - useConstantTime = true - checkEncodeSuccessForDataSet(encodeSuccessDataSet) } @Test fun givenBadEncoding_whenDecoded_ReturnsNull() { checkDecodeFailureForDataSet(decodeFailureDataSet) - useConstantTime = true - checkDecodeFailureForDataSet(decodeFailureDataSet) } @Test fun givenEncodedData_whenDecoded_MatchesRfc4648Spec() { checkDecodeSuccessForDataSet(decodeSuccessDataSet) - useConstantTime = true - checkDecodeSuccessForDataSet(decodeSuccessDataSet) } @Test fun givenUniversalDecoderParameters_whenChecked_areSuccessful() { checkUniversalDecoderParameters() - useConstantTime = true - checkUniversalDecoderParameters() } @Test fun givenUniversalEncoderParameters_whenChecked_areSuccessful() { checkUniversalEncoderParameters() - useConstantTime = true - checkUniversalEncoderParameters() } @Test @@ -184,8 +172,6 @@ class Base16UnitTest: BaseNEncodingTest() { } checkEncodeSuccessForDataSet(lowercaseData) - useConstantTime = true - checkEncodeSuccessForDataSet(lowercaseData) } @Test @@ -198,27 +184,20 @@ class Base16UnitTest: BaseNEncodingTest() { fun givenBase16_whenDecodeEncode_thenReturnsSameValue() { val expected = "54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f672e" useLowercase = true - listOf(false, true).forEach { ct -> - useConstantTime = ct - val encoder = base16() - val decoded = expected.decodeToByteArray(encoder) - val actual = decoded.encodeToString(encoder) - assertEquals(expected, actual) - } + val encoder = base16() + val decoded = expected.decodeToByteArray(encoder) + val actual = decoded.encodeToString(encoder) + assertEquals(expected, actual) } @Test fun givenBase16_whenEncodeDecodeRandomData_thenBytesMatch() { checkRandomData() - useConstantTime = true - checkRandomData() } @Test fun givenBase16Lowercase_whenEncodeDecodeRandomData_thenBytesMatch() { useLowercase = true checkRandomData() - useConstantTime = true - checkRandomData() } } From 01253b8085df729a8a6ea9da01a673a44597b3aa Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 12:24:58 -0500 Subject: [PATCH 5/9] Fix constant-time Base64 implementation --- .../encoding/benchmarks/EncoderDecoderOpts.kt | 4 +- library/base64/api/base64.api | 2 +- library/base64/api/base64.klib.api | 4 +- .../matthewnelson/encoding/base64/Base64.kt | 173 +++++++----------- .../matthewnelson/encoding/base64/Builders.kt | 38 +--- .../encoding/base64/Base64DefaultUnitTest.kt | 29 +-- .../encoding/base64/Base64UrlSafeUnitTest.kt | 27 +-- 7 files changed, 81 insertions(+), 196 deletions(-) diff --git a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt index 4dce0059..903d3538 100644 --- a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt +++ b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt @@ -118,9 +118,9 @@ open class Base32HexBenchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base64Benchmark: EncoderDecoderBenchmarkBase() { // CHARS: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 - @Param("2:84:$TIME_QUICK", "w:22:$TIME_QUICK", "2:84:$TIME_CONST", "w:22:$TIME_CONST") + @Param("2:84:$TIME_CONST", "w:22:$TIME_CONST") override var params: String = "::" override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { - return Base64 { this.isConstantTime = isConstantTime } + return Base64() } } diff --git a/library/base64/api/base64.api b/library/base64/api/base64.api index e7edfc73..aa39b62f 100644 --- a/library/base64/api/base64.api +++ b/library/base64/api/base64.api @@ -49,7 +49,7 @@ public final class io/matthewnelson/encoding/base64/Base64$Config : io/matthewne public final field encodeToUrlSafe Z public final field isConstantTime Z public final field padEncoded Z - public synthetic fun (ZBZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZBZZLkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class io/matthewnelson/encoding/base64/Base64$Default : io/matthewnelson/encoding/core/EncoderDecoder { diff --git a/library/base64/api/base64.klib.api b/library/base64/api/base64.klib.api index 358370cb..cebedca7 100644 --- a/library/base64/api/base64.klib.api +++ b/library/base64/api/base64.klib.api @@ -12,7 +12,7 @@ final class io.matthewnelson.encoding.base64/Base64 : io.matthewnelson.encoding. final class Config : io.matthewnelson.encoding.core/EncoderDecoder.Config { // io.matthewnelson.encoding.base64/Base64.Config|null[0] final val encodeToUrlSafe // io.matthewnelson.encoding.base64/Base64.Config.encodeToUrlSafe|{}encodeToUrlSafe[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base64/Base64.Config.encodeToUrlSafe.|(){}[0] - final val isConstantTime // io.matthewnelson.encoding.base64/Base64.Config.isConstantTime|{}isConstantTime[0] + final val isConstantTime // io.matthewnelson.encoding.base64/Base64.Config.isConstantTime|(){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base64/Base64.Config.isConstantTime.|(){}[0] final val padEncoded // io.matthewnelson.encoding.base64/Base64.Config.padEncoded|{}padEncoded[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base64/Base64.Config.padEncoded.|(){}[0] @@ -36,7 +36,7 @@ final class io.matthewnelson.encoding.base64/Base64ConfigBuilder { // io.matthew final var encodeToUrlSafe // io.matthewnelson.encoding.base64/Base64ConfigBuilder.encodeToUrlSafe|(kotlin.Boolean){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base64/Base64ConfigBuilder.encodeToUrlSafe.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base64/Base64ConfigBuilder.encodeToUrlSafe.|(kotlin.Boolean){}[0] - final var isConstantTime // io.matthewnelson.encoding.base64/Base64ConfigBuilder.isConstantTime|(kotlin.Boolean){}[0] + final var isConstantTime // io.matthewnelson.encoding.base64/Base64ConfigBuilder.isConstantTime|{}isConstantTime[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base64/Base64ConfigBuilder.isConstantTime.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base64/Base64ConfigBuilder.isConstantTime.|(kotlin.Boolean){}[0] final var isLenient // io.matthewnelson.encoding.base64/Base64ConfigBuilder.isLenient|(kotlin.Boolean){}[0] diff --git a/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt b/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt index 30099a87..654be62b 100644 --- a/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt +++ b/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Base64.kt @@ -18,7 +18,6 @@ package io.matthewnelson.encoding.base64 import io.matthewnelson.encoding.core.* -import io.matthewnelson.encoding.core.util.DecoderAction import io.matthewnelson.encoding.core.util.DecoderInput import io.matthewnelson.encoding.core.util.FeedBuffer import kotlin.jvm.JvmField @@ -83,8 +82,6 @@ public class Base64(config: Base64.Config): EncoderDecoder(config public val encodeToUrlSafe: Boolean, @JvmField public val padEncoded: Boolean, - @JvmField - public val isConstantTime: Boolean, ): EncoderDecoder.Config( isLenient = isLenient, lineBreakInterval = lineBreakInterval, @@ -130,10 +127,16 @@ public class Base64(config: Base64.Config): EncoderDecoder(config lineBreakInterval = builder.lineBreakInterval, encodeToUrlSafe = builder.encodeToUrlSafe, padEncoded = builder.padEncoded, - isConstantTime = builder.isConstantTime, ) } } + + /** + * Implementation is always constant-time. Performance impact is negligible. + * @suppress + * */ + @JvmField + public val isConstantTime: Boolean = true } /** @@ -200,47 +203,57 @@ public class Base64(config: Base64.Config): EncoderDecoder(config } } - private companion object { + protected override fun newDecoderFeedProtected(out: Decoder.OutFeed): Decoder.Feed { + return object : Decoder.Feed() { + + private val buffer = DecodingBuffer(out) + + @Throws(EncodingException::class) + override fun consumeProtected(input: Char) { + val code = input.code + + val ge0: Byte = if (code >= '0'.code) 1 else 0 + val le9: Byte = if (code <= '9'.code) 1 else 0 + val geA: Byte = if (code >= 'A'.code) 1 else 0 + val leZ: Byte = if (code <= 'Z'.code) 1 else 0 + val gea: Byte = if (code >= 'a'.code) 1 else 0 + val lez: Byte = if (code <= 'z'.code) 1 else 0 + val eqPlu: Byte = if (code == '+'.code) 1 else 0 + val eqMin: Byte = if (code == '-'.code) 1 else 0 + val eqSla: Byte = if (code == '/'.code) 1 else 0 + val eqUSc: Byte = if (code == '_'.code) 1 else 0 + + var diff = 0 - private val PARSER = DecoderAction.Parser( - '0'..'9' to DecoderAction { char -> // char ASCII value - // 0 48 52 - // 9 57 61 (ASCII + 4) - char.code + 4 - }, - 'A'..'Z' to DecoderAction { char -> + // 0 48 52 + // 9 57 61 (ASCII + 4) + diff += if (ge0 + le9 == 2) 4 else 0 + // char ASCII value - // A 65 0 - // Z 90 25 (ASCII - 65) - char.code - 65 - }, - 'a'..'z' to DecoderAction { char -> + // A 65 0 + // Z 90 25 (ASCII - 65) + diff += if (geA + leZ == 2) -65 else 0 + // char ASCII value - // a 97 26 + // a 97 26 // z 122 51 (ASCII - 71) - char.code - 71 - }, - setOf('+', '-') to DecoderAction { _ -> 62 }, - setOf('/', '_') to DecoderAction { _ -> 63 }, - ) - } + diff += if (gea + lez == 2) -71 else 0 - protected override fun newDecoderFeedProtected(out: Decoder.OutFeed): Decoder.Feed { - return object : Decoder.Feed() { - - private val buffer = DecodingBuffer(out) + val h = 62 - code + val k = 63 - code + diff += if (eqPlu + eqMin == 1) h else 0 + diff += if (eqSla + eqUSc == 1) k else 0 - override fun consumeProtected(input: Char) { - val bits = PARSER.parse(input, isConstantTime = config.isConstantTime) - ?: throw EncodingException("Char[$input] is not a valid Base64 character") + if (diff == 0) { + throw EncodingException("Char[$input] is not a valid Base64 character") + } - buffer.update(bits) + buffer.update(code + diff) } - override fun doFinalProtected() { - buffer.finalize() - } + @Throws(EncodingException::class) + override fun doFinalProtected() { buffer.finalize() } } } @@ -249,25 +262,13 @@ public class Base64(config: Base64.Config): EncoderDecoder(config private val buffer = EncodingBuffer( out = out, - table = if (config.encodeToUrlSafe) { - UrlSafe.CHARS - } else { - Default.CHARS - }, - paddingChar = if (config.padEncoded) { - config.paddingChar - } else { - null - }, + table = if (config.encodeToUrlSafe) UrlSafe.CHARS else Default.CHARS, + paddingChar = if (config.padEncoded) config.paddingChar else null, ) - override fun consumeProtected(input: Byte) { - buffer.update(input.toInt()) - } + override fun consumeProtected(input: Byte) { buffer.update(input.toInt()) } - override fun doFinalProtected() { - buffer.finalize() - } + override fun doFinalProtected() { buffer.finalize() } } } @@ -336,29 +337,10 @@ public class Base64(config: Base64.Config): EncoderDecoder(config val i3 = (b1 and 0x0f shl 2) or (b2 and 0xff shr 6) val i4 = (b2 and 0x3f) - if (config.isConstantTime) { - var c1: Char? = null - var c2: Char? = null - var c3: Char? = null - var c4: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - c3 = if (index == i3) c else c3 - c4 = if (index == i4) c else c4 - } - - out.output(c1!!) - out.output(c2!!) - out.output(c3!!) - out.output(c4!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - out.output(table[i3]) - out.output(table[i4]) - } + out.output(table[i1]) + out.output(table[i2]) + out.output(table[i3]) + out.output(table[i4]) }, finalize = { modulus, buffer -> val padCount: Int = when (modulus) { @@ -369,21 +351,8 @@ public class Base64(config: Base64.Config): EncoderDecoder(config val i1 = b0 and 0xff shr 2 val i2 = b0 and 0x03 shl 4 - if (config.isConstantTime) { - var c1: Char? = null - var c2: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - } - - out.output(c1!!) - out.output(c2!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - } + out.output(table[i1]) + out.output(table[i2]) 2 } @@ -396,34 +365,16 @@ public class Base64(config: Base64.Config): EncoderDecoder(config val i2 = (b0 and 0x03 shl 4) or (b1 and 0xff shr 4) val i3 = (b1 and 0x0f shl 2) - if (config.isConstantTime) { - var c1: Char? = null - var c2: Char? = null - var c3: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - c3 = if (index == i3) c else c3 - } - - out.output(c1!!) - out.output(c2!!) - out.output(c3!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - out.output(table[i3]) - } + out.output(table[i1]) + out.output(table[i2]) + out.output(table[i3]) 1 } } if (paddingChar != null) { - repeat(padCount) { - out.output(paddingChar) - } + repeat(padCount) { out.output(paddingChar) } } } ) diff --git a/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Builders.kt b/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Builders.kt index ffc83b3e..084013ef 100644 --- a/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Builders.kt +++ b/library/base64/src/commonMain/kotlin/io/matthewnelson/encoding/base64/Builders.kt @@ -67,45 +67,12 @@ public class Base64ConfigBuilder { public constructor() public constructor(config: Base64.Config?): this() { if (config == null) return - isConstantTime = config.isConstantTime isLenient = config.isLenient ?: true lineBreakInterval = config.lineBreakInterval encodeToUrlSafe = config.encodeToUrlSafe padEncoded = config.padEncoded } - /** - * If true, will utilize constant-time operations when - * encoding/decoding data. This will be slower, but help - * mitigate potential timing attacks with sensitive data - * (such as private key material). - * - * If false, will not use constant time operations. - * - * e.g. (NOT constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * for (c in this) { - * if (c == char) return true - * } - * return false - * } - * - * e.g. (YES constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * var result = false - * for (c in this) { - * result = if (c == char) true else result - * } - * return result - * } - * - * Default: `false` - * */ - @JvmField - public var isConstantTime: Boolean = false - /** * If true, spaces and new lines ('\n', '\r', ' ', '\t') * will be skipped over when decoding (against RFC 4648). @@ -185,4 +152,9 @@ public class Base64ConfigBuilder { } public fun build(): Base64.Config = Base64.Config.from(this) + + /** @suppress */ + @JvmField + @Deprecated(message = "Implementation is always constant time. Performance impact is negligible.") + public var isConstantTime: Boolean = true } diff --git a/library/base64/src/commonTest/kotlin/io/matthewnelson/encoding/base64/Base64DefaultUnitTest.kt b/library/base64/src/commonTest/kotlin/io/matthewnelson/encoding/base64/Base64DefaultUnitTest.kt index 85a899da..e969aa4d 100644 --- a/library/base64/src/commonTest/kotlin/io/matthewnelson/encoding/base64/Base64DefaultUnitTest.kt +++ b/library/base64/src/commonTest/kotlin/io/matthewnelson/encoding/base64/Base64DefaultUnitTest.kt @@ -26,11 +26,7 @@ import kotlin.test.assertEquals class Base64DefaultUnitTest: BaseNEncodingTest() { - private var useConstantTime = false - - private fun base64(): Base64 = Base64 { - isConstantTime = useConstantTime - } + private fun base64(): Base64 = Base64() override val decodeFailureDataSet: Set> = setOf( Data("SGVsbG8gV29ybGQ^", expected = null, message = "Character '^' should return null") @@ -132,55 +128,40 @@ class Base64DefaultUnitTest: BaseNEncodingTest() { @Test fun givenString_whenEncoded_MatchesRfc4648Spec() { checkEncodeSuccessForDataSet(encodeSuccessDataSet) - useConstantTime = true - checkEncodeSuccessForDataSet(encodeSuccessDataSet) } @Test fun givenBadEncoding_whenDecoded_ReturnsNull() { checkDecodeFailureForDataSet(decodeFailureDataSet) - useConstantTime = true - checkDecodeFailureForDataSet(decodeFailureDataSet) } @Test fun givenEncodedData_whenDecoded_MatchesRfc4648Spec() { checkDecodeSuccessForDataSet(decodeSuccessDataSet) - useConstantTime = true - checkDecodeSuccessForDataSet(decodeSuccessDataSet) } @Test fun givenUniversalDecoderParameters_whenChecked_areSuccessful() { checkUniversalDecoderParameters() - useConstantTime = true - checkUniversalDecoderParameters() } @Test fun givenUniversalEncoderParameters_whenChecked_areSuccessful() { checkUniversalEncoderParameters() - useConstantTime = true - checkUniversalEncoderParameters() } @Test fun givenBase64_whenDecodeEncode_thenReturnsSameValue() { val expected = "U2FsdGVkX1/4ZC61vUIS40oz3+re25V1W1fBbNbK/mnRgdvTyYP0kbNMJx7ud1YTXThgcgceR08A/p/NsaNTZQ==" - listOf(false, true).forEach { ct -> - useConstantTime = ct - val encoder = base64() - val decoded = expected.decodeToByteArray(encoder) - val actual = decoded.encodeToString(encoder) - assertEquals(expected, actual) - } + val encoder = base64() + val decoded = expected.decodeToByteArray(encoder) + val actual = decoded.encodeToString(encoder) + assertEquals(expected, actual) } @Test fun givenBase64_whenEncodeDecodeRandomData_thenBytesMatch() { checkRandomData() - useConstantTime = true - checkRandomData() } } diff --git a/library/base64/src/commonTest/kotlin/io/matthewnelson/encoding/base64/Base64UrlSafeUnitTest.kt b/library/base64/src/commonTest/kotlin/io/matthewnelson/encoding/base64/Base64UrlSafeUnitTest.kt index 77002408..d11149c7 100644 --- a/library/base64/src/commonTest/kotlin/io/matthewnelson/encoding/base64/Base64UrlSafeUnitTest.kt +++ b/library/base64/src/commonTest/kotlin/io/matthewnelson/encoding/base64/Base64UrlSafeUnitTest.kt @@ -26,12 +26,10 @@ import kotlin.test.assertEquals class Base64UrlSafeUnitTest: BaseNEncodingTest() { - private var useConstantTime = false private var usePadding = true private fun base64(): Base64 = Base64 { encodeToUrlSafe = true - isConstantTime = useConstantTime padEncoded = usePadding } @@ -150,62 +148,45 @@ class Base64UrlSafeUnitTest: BaseNEncodingTest() { @Test fun givenString_whenEncoded_MatchesRfc4648Spec() { checkEncodeSuccessForDataSet(encodeSuccessDataSet) - useConstantTime = true - checkEncodeSuccessForDataSet(encodeSuccessDataSet) } @Test fun givenBadEncoding_whenDecoded_ReturnsNull() { checkDecodeFailureForDataSet(decodeFailureDataSet) - useConstantTime = true - checkDecodeFailureForDataSet(decodeFailureDataSet) } @Test fun givenEncodedData_whenDecoded_MatchesRfc4648Spec() { checkDecodeSuccessForDataSet(decodeSuccessDataSet) - useConstantTime = true - checkDecodeSuccessForDataSet(decodeSuccessDataSet) } @Test fun givenString_whenEncodedWithoutPaddingExpressed_returnsExpected() { usePadding = false checkDecodeSuccessForDataSet(getDecodeSuccessDataSetWithoutPadding()) - useConstantTime = true - checkDecodeSuccessForDataSet(getDecodeSuccessDataSetWithoutPadding()) } @Test fun givenUniversalDecoderParameters_whenChecked_areSuccessful() { checkUniversalDecoderParameters() - useConstantTime = true - checkUniversalDecoderParameters() } @Test fun givenUniversalEncoderParameters_whenChecked_areSuccessful() { checkUniversalEncoderParameters() - useConstantTime = true - checkUniversalEncoderParameters() } @Test fun givenBase64UrlSafe_whenDecodeEncode_thenReturnsSameValue() { val expected = "U2FsdGVkX1_4ZC61vUIS40oz3-re25V1W1fBbNbK_mnRgdvTyYP0kbNMJx7ud1YTXThgcgceR08A_p_NsaNTZQ==" - listOf(false, true).forEach { ct -> - useConstantTime = ct - val encoder = base64() - val decoded = expected.decodeToByteArray(encoder) - val actual = decoded.encodeToString(encoder) - assertEquals(expected, actual) - } + val encoder = base64() + val decoded = expected.decodeToByteArray(encoder) + val actual = decoded.encodeToString(encoder) + assertEquals(expected, actual) } @Test fun givenBase64UrlSafe_whenEncodeDecodeRandomData_thenBytesMatch() { checkRandomData() - useConstantTime = true - checkRandomData() } } From 6810e5285c501faf00b91556e36c5864a258906f Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 13:51:34 -0500 Subject: [PATCH 6/9] Fix constant-time Base32 implementations --- .../encoding/benchmarks/Constants.kt | 3 - .../encoding/benchmarks/EncoderDecoderOpts.kt | 56 +- library/base32/api/base32.api | 6 +- library/base32/api/base32.klib.api | 12 +- .../matthewnelson/encoding/base32/Base32.kt | 836 +++++++----------- .../matthewnelson/encoding/base32/Builders.kt | 114 +-- .../base32/Base32CrockfordUnitTest.kt | 35 +- .../encoding/base32/Base32DefaultUnitTest.kt | 31 +- .../encoding/base32/Base32HexUnitTest.kt | 31 +- 9 files changed, 359 insertions(+), 765 deletions(-) diff --git a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/Constants.kt b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/Constants.kt index c3a079f9..bac71eaa 100644 --- a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/Constants.kt +++ b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/Constants.kt @@ -19,6 +19,3 @@ const val ENC_ITERATIONS_WARMUP = 5 const val ENC_ITERATIONS_MEASURE = 5 const val ENC_TIME_WARMUP = 2 const val ENC_TIME_MEASURE = 3 - -const val TIME_QUICK = "quick-time" -const val TIME_CONST = "const-time" diff --git a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt index 903d3538..fb4b9bdc 100644 --- a/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt +++ b/benchmarks/src/commonMain/kotlin/io/matthewnelson/encoding/benchmarks/EncoderDecoderOpts.kt @@ -27,25 +27,23 @@ import kotlinx.benchmark.* abstract class EncoderDecoderBenchmarkBase { - // "::" + // ":" abstract var params: String - protected abstract fun encoder(isConstantTime: Boolean): EncoderDecoder<*> + protected abstract val encoder: EncoderDecoder<*> private var byte: Byte = 0 private var char = '_' - private var feedDecoder: Decoder<*>.Feed = Base16.newDecoderFeed {}.apply { close() } - private var feedEncoder: Encoder<*>.Feed = Base16.newEncoderFeed {}.apply { close() } + private val feedDecoder: Decoder<*>.Feed by lazy { encoder.newDecoderFeed {} } + private val feedEncoder: Encoder<*>.Feed by lazy { encoder.newEncoderFeed {} } @Setup fun setup() { - val encoder = params.split(':').let { params -> + params.split(':').let { params -> char = params[0][0] byte = params[1].toByte() - encoder(params[2] == TIME_CONST) } - - feedDecoder = encoder.newDecoderFeed {} - feedEncoder = encoder.newEncoderFeed {} + feedDecoder + feedEncoder } @Benchmark @@ -62,11 +60,9 @@ abstract class EncoderDecoderBenchmarkBase { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base16Benchmark: EncoderDecoderBenchmarkBase() { // CHARS: 0123456789ABCDEF - @Param("3:0:$TIME_CONST", "d:122:$TIME_CONST") - override var params: String = "::" - override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { - return Base16() - } + @Param("3:0", "d:122") + override var params: String = ":" + override val encoder: EncoderDecoder<*> = Base16() } @State(Scope.Benchmark) @@ -76,11 +72,9 @@ open class Base16Benchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base32CrockfordBenchmark: EncoderDecoderBenchmarkBase() { // CHARS: 0123456789ABCDEFGHJKMNPQRSTVWXYZ - @Param("3:-6:$TIME_QUICK", "x:115:$TIME_QUICK", "3:-6:$TIME_CONST", "x:115:$TIME_CONST") - override var params: String = "::" - override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { - return Base32Crockford { this.isConstantTime = isConstantTime } - } + @Param("3:-6", "x:115") + override var params: String = ":" + override val encoder: EncoderDecoder<*> = Base32Crockford() } @State(Scope.Benchmark) @@ -90,11 +84,9 @@ open class Base32CrockfordBenchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base32DefaultBenchmark: EncoderDecoderBenchmarkBase() { // CHARS: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 - @Param("C:-123:$TIME_QUICK", "w:15:$TIME_QUICK", "C:-123:$TIME_CONST", "w:15:$TIME_CONST") - override var params: String = "::" - override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { - return Base32Default { this.isConstantTime = isConstantTime } - } + @Param("C:-123", "w:15") + override var params: String = ":" + override val encoder: EncoderDecoder<*> = Base32Default() } @State(Scope.Benchmark) @@ -104,11 +96,9 @@ open class Base32DefaultBenchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base32HexBenchmark: EncoderDecoderBenchmarkBase() { // CHARS: 0123456789ABCDEFGHIJKLMNOPQRSTUV - @Param("A:-12:$TIME_QUICK", "r:42:$TIME_QUICK", "A:-12:$TIME_CONST", "r:42:$TIME_CONST") - override var params: String = "::" - override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { - return Base32Hex { this.isConstantTime = isConstantTime } - } + @Param("A:-12", "r:42") + override var params: String = ":" + override val encoder: EncoderDecoder<*> = Base32Hex() } @State(Scope.Benchmark) @@ -118,9 +108,7 @@ open class Base32HexBenchmark: EncoderDecoderBenchmarkBase() { @Measurement(iterations = ENC_ITERATIONS_MEASURE, time = ENC_TIME_MEASURE) open class Base64Benchmark: EncoderDecoderBenchmarkBase() { // CHARS: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 - @Param("2:84:$TIME_CONST", "w:22:$TIME_CONST") - override var params: String = "::" - override fun encoder(isConstantTime: Boolean): EncoderDecoder<*> { - return Base64() - } + @Param("2:84", "w:22") + override var params: String = ":" + override val encoder: EncoderDecoder<*> = Base64() } diff --git a/library/base32/api/base32.api b/library/base32/api/base32.api index 63aa8679..acdb7b58 100644 --- a/library/base32/api/base32.api +++ b/library/base32/api/base32.api @@ -79,7 +79,7 @@ public final class io/matthewnelson/encoding/base32/Base32$Crockford$Config : io public final field finalizeWhenFlushed Z public final field hyphenInterval B public final field isConstantTime Z - public synthetic fun (ZZBLjava/lang/Character;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZZBLjava/lang/Character;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class io/matthewnelson/encoding/base32/Base32$Default : io/matthewnelson/encoding/base32/Base32 { @@ -96,7 +96,7 @@ public final class io/matthewnelson/encoding/base32/Base32$Default$Config : io/m public final field encodeToLowercase Z public final field isConstantTime Z public final field padEncoded Z - public synthetic fun (ZBZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZBZZLkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class io/matthewnelson/encoding/base32/Base32$Hex : io/matthewnelson/encoding/base32/Base32 { @@ -113,7 +113,7 @@ public final class io/matthewnelson/encoding/base32/Base32$Hex$Config : io/matth public final field encodeToLowercase Z public final field isConstantTime Z public final field padEncoded Z - public synthetic fun (ZBZZZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ZBZZLkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class io/matthewnelson/encoding/base32/Base32CrockfordConfigBuilder { diff --git a/library/base32/api/base32.klib.api b/library/base32/api/base32.klib.api index 73c22564..49a29897 100644 --- a/library/base32/api/base32.klib.api +++ b/library/base32/api/base32.klib.api @@ -21,7 +21,7 @@ final class io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder { // i final var hyphenInterval // io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder.hyphenInterval|(kotlin.Byte){}[0] final fun (): kotlin/Byte // io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder.hyphenInterval.|(){}[0] final fun (kotlin/Byte) // io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder.hyphenInterval.|(kotlin.Byte){}[0] - final var isConstantTime // io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder.isConstantTime|(kotlin.Boolean){}[0] + final var isConstantTime // io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder.isConstantTime|{}isConstantTime[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder.isConstantTime.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder.isConstantTime.|(kotlin.Boolean){}[0] final var isLenient // io.matthewnelson.encoding.base32/Base32CrockfordConfigBuilder.isLenient|(kotlin.Boolean){}[0] @@ -40,7 +40,7 @@ final class io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder { // io. final var encodeToLowercase // io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder.encodeToLowercase|(kotlin.Boolean){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder.encodeToLowercase.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder.encodeToLowercase.|(kotlin.Boolean){}[0] - final var isConstantTime // io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder.isConstantTime|(kotlin.Boolean){}[0] + final var isConstantTime // io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder.isConstantTime|{}isConstantTime[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder.isConstantTime.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder.isConstantTime.|(kotlin.Boolean){}[0] final var isLenient // io.matthewnelson.encoding.base32/Base32DefaultConfigBuilder.isLenient|(kotlin.Boolean){}[0] @@ -64,7 +64,7 @@ final class io.matthewnelson.encoding.base32/Base32HexConfigBuilder { // io.matt final var encodeToLowercase // io.matthewnelson.encoding.base32/Base32HexConfigBuilder.encodeToLowercase|(kotlin.Boolean){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32HexConfigBuilder.encodeToLowercase.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base32/Base32HexConfigBuilder.encodeToLowercase.|(kotlin.Boolean){}[0] - final var isConstantTime // io.matthewnelson.encoding.base32/Base32HexConfigBuilder.isConstantTime|(kotlin.Boolean){}[0] + final var isConstantTime // io.matthewnelson.encoding.base32/Base32HexConfigBuilder.isConstantTime|{}isConstantTime[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32HexConfigBuilder.isConstantTime.|(){}[0] final fun (kotlin/Boolean) // io.matthewnelson.encoding.base32/Base32HexConfigBuilder.isConstantTime.|(kotlin.Boolean){}[0] final var isLenient // io.matthewnelson.encoding.base32/Base32HexConfigBuilder.isLenient|(kotlin.Boolean){}[0] @@ -96,7 +96,7 @@ sealed class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.matth final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32.Crockford.Config.finalizeWhenFlushed.|(){}[0] final val hyphenInterval // io.matthewnelson.encoding.base32/Base32.Crockford.Config.hyphenInterval|{}hyphenInterval[0] final fun (): kotlin/Byte // io.matthewnelson.encoding.base32/Base32.Crockford.Config.hyphenInterval.|(){}[0] - final val isConstantTime // io.matthewnelson.encoding.base32/Base32.Crockford.Config.isConstantTime|{}isConstantTime[0] + final val isConstantTime // io.matthewnelson.encoding.base32/Base32.Crockford.Config.isConstantTime|(){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32.Crockford.Config.isConstantTime.|(){}[0] } @@ -114,7 +114,7 @@ sealed class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.matth final class Config : io.matthewnelson.encoding.core/EncoderDecoder.Config { // io.matthewnelson.encoding.base32/Base32.Default.Config|null[0] final val encodeToLowercase // io.matthewnelson.encoding.base32/Base32.Default.Config.encodeToLowercase|{}encodeToLowercase[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32.Default.Config.encodeToLowercase.|(){}[0] - final val isConstantTime // io.matthewnelson.encoding.base32/Base32.Default.Config.isConstantTime|{}isConstantTime[0] + final val isConstantTime // io.matthewnelson.encoding.base32/Base32.Default.Config.isConstantTime|(){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32.Default.Config.isConstantTime.|(){}[0] final val padEncoded // io.matthewnelson.encoding.base32/Base32.Default.Config.padEncoded|{}padEncoded[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32.Default.Config.padEncoded.|(){}[0] @@ -134,7 +134,7 @@ sealed class <#A: io.matthewnelson.encoding.core/EncoderDecoder.Config> io.matth final class Config : io.matthewnelson.encoding.core/EncoderDecoder.Config { // io.matthewnelson.encoding.base32/Base32.Hex.Config|null[0] final val encodeToLowercase // io.matthewnelson.encoding.base32/Base32.Hex.Config.encodeToLowercase|{}encodeToLowercase[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32.Hex.Config.encodeToLowercase.|(){}[0] - final val isConstantTime // io.matthewnelson.encoding.base32/Base32.Hex.Config.isConstantTime|{}isConstantTime[0] + final val isConstantTime // io.matthewnelson.encoding.base32/Base32.Hex.Config.isConstantTime|(){}[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32.Hex.Config.isConstantTime.|(){}[0] final val padEncoded // io.matthewnelson.encoding.base32/Base32.Hex.Config.padEncoded|{}padEncoded[0] final fun (): kotlin/Boolean // io.matthewnelson.encoding.base32/Base32.Hex.Config.padEncoded.|(){}[0] diff --git a/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt b/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt index 1e486bd8..c920737a 100644 --- a/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt +++ b/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Base32.kt @@ -22,7 +22,7 @@ import io.matthewnelson.encoding.base32.internal.encodeOutSize import io.matthewnelson.encoding.base32.internal.isCheckSymbol import io.matthewnelson.encoding.base32.internal.toBits import io.matthewnelson.encoding.core.* -import io.matthewnelson.encoding.core.util.* +import io.matthewnelson.encoding.core.util.DecoderInput import io.matthewnelson.encoding.core.util.FeedBuffer import kotlin.jvm.JvmField import kotlin.jvm.JvmSynthetic @@ -91,8 +91,6 @@ public sealed class Base32(config: C): EncoderDecoder< public val checkSymbol: Char?, @JvmField public val finalizeWhenFlushed: Boolean, - @JvmField - public val isConstantTime: Boolean, ): EncoderDecoder.Config( isLenient = isLenient, lineBreakInterval = 0, @@ -191,10 +189,16 @@ public sealed class Base32(config: C): EncoderDecoder< hyphenInterval = if (builder.hyphenInterval > 0) builder.hyphenInterval else 0, checkSymbol = builder.checkSymbol, finalizeWhenFlushed = builder.finalizeWhenFlushed, - isConstantTime = builder.isConstantTime ) } } + + /** + * Implementation is always constant-time. Performance impact is negligible. + * @suppress + * */ + @JvmField + public val isConstantTime: Boolean = true } /** @@ -232,191 +236,142 @@ public sealed class Base32(config: C): EncoderDecoder< protected override fun newEncoderFeedProtected(out: OutFeed): Encoder.Feed { return DELEGATE.newEncoderFeedProtected(out) } + } + + protected override fun newDecoderFeedProtected(out: Decoder.OutFeed): Decoder.Feed { + return object : Decoder.Feed() { - private val CT_CASE = CTCase(table = CHARS_UPPER) + private var isCheckSymbolSet = false + private val buffer = DecodingBuffer(out) + + @Throws(EncodingException::class) + override fun consumeProtected(input: Char) { + if (isCheckSymbolSet) { + // If the set checkByte was not intended, it's only valid as the + // very last character and the previous update call was invalid. + throw EncodingException("CheckSymbol[${config.checkSymbol}] was set") + } + + // Crockford allows for insertion of hyphens, + // which are to be ignored when decoding. + if (input == '-') return + + val code = input.code + + val ge0: Byte = if (code >= '0'.code) 1 else 0 + val le9: Byte = if (code <= '9'.code) 1 else 0 + + val geA: Byte = if (code >= 'A'.code) 1 else 0 + val leH: Byte = if (code <= 'H'.code) 1 else 0 + val eqI: Byte = if (code == 'I'.code) 1 else 0 + val eqL: Byte = if (code == 'L'.code) 1 else 0 + val eqJ: Byte = if (code == 'J'.code) 1 else 0 + val eqK: Byte = if (code == 'K'.code) 1 else 0 + val eqM: Byte = if (code == 'M'.code) 1 else 0 + val eqN: Byte = if (code == 'N'.code) 1 else 0 + val eqO: Byte = if (code == 'O'.code) 1 else 0 + val geP: Byte = if (code >= 'P'.code) 1 else 0 + val leT: Byte = if (code <= 'T'.code) 1 else 0 + val geV: Byte = if (code >= 'V'.code) 1 else 0 + val leZ: Byte = if (code <= 'Z'.code) 1 else 0 + + val gea: Byte = if (code >= 'a'.code) 1 else 0 + val leh: Byte = if (code <= 'h'.code) 1 else 0 + val eqi: Byte = if (code == 'i'.code) 1 else 0 + val eql: Byte = if (code == 'l'.code) 1 else 0 + val eqj: Byte = if (code == 'j'.code) 1 else 0 + val eqk: Byte = if (code == 'k'.code) 1 else 0 + val eqm: Byte = if (code == 'm'.code) 1 else 0 + val eqn: Byte = if (code == 'n'.code) 1 else 0 + val eqo: Byte = if (code == 'o'.code) 1 else 0 + val gep: Byte = if (code >= 'p'.code) 1 else 0 + val let: Byte = if (code <= 't'.code) 1 else 0 + val gev: Byte = if (code >= 'v'.code) 1 else 0 + val lez: Byte = if (code <= 'z'.code) 1 else 0 + + var diff = 0 - private val UC_PARSER = DecoderAction.Parser( - '0'..'9' to DecoderAction { char -> // char ASCII value - // 0 48 0 - // 9 57 9 (ASCII - 48) - char.code - 48 - }, - 'A'..'H' to DecoderAction { char -> + // 0 48 0 + // 9 57 9 (ASCII - 48) + diff += if (ge0 + le9 == 2) -48 else 0 + // char ASCII value - // A 65 10 - // H 72 17 (ASCII - 55) - char.code - 55 - }, - setOf('I', 'L') to DecoderAction { _ -> + // A 65 10 + // H 72 17 (ASCII - 55) + diff += if (geA + leH == 2) -55 else 0 + // Crockford treats characters 'I', 'i', 'L' and 'l' as 1 + val h = 1 - code + diff += if (eqI + eqi + eqL + eql == 1) h else 0 // char ASCII value - // 1 49 1 (ASCII - 48) - '1'.code - 48 - }, - 'J'..'K' to DecoderAction { char -> - // char ASCII value - // J 74 18 - // K 75 19 (ASCII - 56) - char.code - 56 - }, - 'M'..'N' to DecoderAction { char -> + // J 74 18 + // K 75 19 (ASCII - 56) + diff += if (eqJ + eqK == 1) -56 else 0 + // char ASCII value - // M 77 20 - // N 78 21 (ASCII - 57) - char.code - 57 - }, - setOf('O') to DecoderAction { _ -> + // M 77 20 + // N 78 21 (ASCII - 57) + diff += if (eqM + eqN == 1) -57 else 0 + // Crockford treats characters 'O' and 'o' as 0 + val k = 0 - code + diff += if (eqO + eqo == 1) k else 0 // char ASCII value - // 0 48 0 (ASCII - 48) - '0'.code - 48 - }, - 'P'..'T' to DecoderAction { char -> - // char ASCII value - // P 80 22 - // T 84 26 (ASCII - 58) - char.code - 58 - }, - 'V'..'Z' to DecoderAction { char -> - // char ASCII value - // V 86 27 - // Z 90 31 (ASCII - 59) - char.code - 59 - }, - 'a'..'h' to DecoderAction { char -> - // char ASCII value - // A 65 10 - // H 72 17 (ASCII - 55) - char.uppercaseChar().code - 55 - }, - setOf('i', 'l') to DecoderAction { _ -> - // Crockford treats characters 'I', 'i', 'L' and 'l' as 1 + // P 80 22 + // T 84 26 (ASCII - 58) + diff += if (geP + leT == 2) -58 else 0 // char ASCII value - // 1 49 1 (ASCII - 48) - '1'.code - 48 - }, - 'j'..'k' to DecoderAction { char -> - // char ASCII value - // J 74 18 - // K 75 19 (ASCII - 56) - char.uppercaseChar().code - 56 - }, - 'm'..'n' to DecoderAction { char -> - // char ASCII value - // M 77 20 - // N 78 21 (ASCII - 57) - char.uppercaseChar().code - 57 - }, - setOf('o') to DecoderAction { _ -> - // Crockford treats characters 'O' and 'o' as 0 + // V 86 27 + // Z 90 31 (ASCII - 59) + diff += if (geV + leZ == 2) -59 else 0 // char ASCII value - // 0 48 0 (ASCII - 48) - '0'.code - 48 - }, - 'p'..'t' to DecoderAction { char -> + // a 97 10 + // h 104 17 (ASCII - 87) + diff += if (gea + leh == 2) -87 else 0 + // char ASCII value - // P 80 22 - // T 84 26 (ASCII - 58) - char.uppercaseChar().code - 58 - }, - 'v'..'z' to DecoderAction { char -> + // j 106 18 + // k 107 19 (ASCII - 88) + diff += if (eqj + eqk == 1) -88 else 0 + // char ASCII value - // V 86 27 - // Z 90 31 (ASCII - 59) - char.uppercaseChar().code - 59 - }, - ) - - // Assume input will be lowercase letters. Reorder - // actions to check lowercase before uppercase. - private val LC_PARSER = DecoderAction.Parser( - UC_PARSER.actions[0], - UC_PARSER.actions[8], - UC_PARSER.actions[9], - UC_PARSER.actions[10], - UC_PARSER.actions[11], - UC_PARSER.actions[12], - UC_PARSER.actions[13], - UC_PARSER.actions[14], - UC_PARSER.actions[1], - UC_PARSER.actions[2], - UC_PARSER.actions[3], - UC_PARSER.actions[4], - UC_PARSER.actions[5], - UC_PARSER.actions[6], - UC_PARSER.actions[7], - ) - - // Do not include lowercase letter actions. Constant time - // operations will uppercase the input on every invocation. - private val CT_PARSER = DecoderAction.Parser( - UC_PARSER.actions[0], - UC_PARSER.actions[1], - UC_PARSER.actions[2], - UC_PARSER.actions[3], - UC_PARSER.actions[4], - UC_PARSER.actions[5], - UC_PARSER.actions[6], - UC_PARSER.actions[7], - ) - } + // m 109 20 + // n 110 21 (ASCII - 89) + diff += if (eqm + eqn == 1) -89 else 0 - protected override fun newDecoderFeedProtected(out: Decoder.OutFeed): Decoder.Feed { - return object : Decoder.Feed() { + // char ASCII value + // p 112 22 + // t 116 26 (ASCII - 90) + diff += if (gep + let == 2) -90 else 0 - private var isCheckSymbolSet = false - private val buffer = DecodingBuffer(out) - private val parser = when { - config.isConstantTime -> CT_PARSER - config.encodeToLowercase -> LC_PARSER - else -> UC_PARSER - } + // char ASCII value + // v 118 27 + // z 122 31 (ASCII - 91) + diff += if (gev + lez == 2) -91 else 0 - @Throws(EncodingException::class) - override fun consumeProtected(input: Char) { - if (isCheckSymbolSet) { - // If the set checkByte was not intended, it's only a valid - // as the very last character and the previous update call - // was invalid. - throw EncodingException( - "Checksymbol[${config.checkSymbol}] was set, but decoding is still being attempted." - ) + if (diff != 0) { + buffer.update(code + diff) + return } - val char = if (config.isConstantTime) { - CT_CASE.uppercase(input) ?: input - } else { - input + if (!input.isCheckSymbol()) { + throw EncodingException("Char[${input}] is not a valid Base32 Crockford character") } - val bits = parser.parse(char, isConstantTime = config.isConstantTime) - - if (bits == null) { - // Crockford allows for insertion of hyphens, - // which are to be ignored when decoding. - if (input == '-') return - - if (!input.isCheckSymbol(isConstantTime = config.isConstantTime)) { - throw EncodingException("Char[${input}] is not a valid Base32 Crockford character") - } - - if (config.checkSymbol?.uppercaseChar() == input.uppercaseChar()) { - isCheckSymbolSet = true - return - } - - throw EncodingException( - "Char[${input}] IS a checkSymbol, but did " + - "not match config's Checksymbol[${config.checkSymbol}]" - ) + if (config.checkSymbol?.uppercaseChar() == input.uppercaseChar()) { + isCheckSymbolSet = true + return } - buffer.update(bits) + throw EncodingException( + "Char[${input}] IS a checkSymbol, but did " + + "not match config's Checksymbol[${config.checkSymbol}]" + ) } @Throws(EncodingException::class) @@ -444,18 +399,11 @@ public sealed class Base32(config: C): EncoderDecoder< out.output(byte) outputHyphenOnNext = config.hyphenInterval > 0 && ++outCount == config.hyphenInterval }, - table = if (config.encodeToLowercase) { - CHARS_LOWER - } else { - CHARS_UPPER - }, - isConstantTime = config.isConstantTime, + table = if (config.encodeToLowercase) CHARS_LOWER else CHARS_UPPER, paddingChar = null, ) - override fun consumeProtected(input: Byte) { - buffer.update(input.toInt()) - } + override fun consumeProtected(input: Byte) { buffer.update(input.toInt()) } override fun doFinalProtected() { buffer.finalize() @@ -533,8 +481,6 @@ public sealed class Base32(config: C): EncoderDecoder< public val encodeToLowercase: Boolean, @JvmField public val padEncoded: Boolean, - @JvmField - public val isConstantTime: Boolean, ): EncoderDecoder.Config( isLenient = isLenient, lineBreakInterval = lineBreakInterval, @@ -570,10 +516,16 @@ public sealed class Base32(config: C): EncoderDecoder< lineBreakInterval = builder.lineBreakInterval, encodeToLowercase = builder.encodeToLowercase, padEncoded = builder.padEncoded, - isConstantTime = builder.isConstantTime, ) } } + + /** + * Implementation is always constant-time. Performance impact is negligible. + * @suppress + * */ + @JvmField + public val isConstantTime: Boolean = true } /** @@ -611,74 +563,50 @@ public sealed class Base32(config: C): EncoderDecoder< protected override fun newEncoderFeedProtected(out: OutFeed): Encoder.Feed { return DELEGATE.newEncoderFeedProtected(out) } - - private val CT_CASE = CTCase(table = CHARS_UPPER) - - private val UC_PARSER = DecoderAction.Parser( - '2'..'7' to DecoderAction { char -> - // char ASCII value - // 2 50 26 - // 7 55 31 (ASCII - 24) - char.code - 24 - }, - CT_CASE.uppers to DecoderAction { char -> - // char ASCII value - // A 65 0 - // Z 90 25 (ASCII - 65) - char.code - 65 - }, - CT_CASE.lowers to DecoderAction { char -> - // char ASCII value - // A 65 0 - // Z 90 25 (ASCII - 65) - char.uppercaseChar().code - 65 - }, - ) - - // Assume input will be lowercase letters. Reorder - // actions to check lowercase before uppercase. - private val LC_PARSER = DecoderAction.Parser( - UC_PARSER.actions[0], - UC_PARSER.actions[2], - UC_PARSER.actions[1], - ) - - // Do not include lowercase letter actions. Constant time - // operations will uppercase the input on every invocation. - private val CT_PARSER = DecoderAction.Parser( - UC_PARSER.actions[0], - UC_PARSER.actions[1], - ) } protected override fun newDecoderFeedProtected(out: Decoder.OutFeed): Decoder.Feed { return object : Decoder.Feed() { private val buffer = DecodingBuffer(out) - private val parser = when { - config.isConstantTime -> CT_PARSER - config.encodeToLowercase -> LC_PARSER - else -> UC_PARSER - } @Throws(EncodingException::class) override fun consumeProtected(input: Char) { - val char = if (config.isConstantTime) { - CT_CASE.uppercase(input) ?: input - } else { - input - } + val code = input.code + + val ge2: Byte = if (code >= '2'.code) 1 else 0 + val le7: Byte = if (code <= '7'.code) 1 else 0 + val geA: Byte = if (code >= 'A'.code) 1 else 0 + val leZ: Byte = if (code <= 'Z'.code) 1 else 0 + val gea: Byte = if (code >= 'a'.code) 1 else 0 + val lez: Byte = if (code <= 'z'.code) 1 else 0 + + var diff = 0 - val bits = parser.parse(char, isConstantTime = config.isConstantTime) - ?: throw EncodingException("Char[${input}] is not a valid Base32 Default character") + // char ASCII value + // 2 50 26 + // 7 55 31 (ASCII - 24) + diff += if (ge2 + le7 == 2) -24 else 0 + + // char ASCII value + // A 65 0 + // Z 90 25 (ASCII - 65) + diff += if (geA + leZ == 2) -65 else 0 + + // char ASCII value + // a 97 0 + // z 122 25 (ASCII - 97) + diff += if (gea + lez == 2) -97 else 0 - buffer.update(bits) + if (diff == 0) { + throw EncodingException("Char[${input}] is not a valid Base32 Default character") + } + + buffer.update(code + diff) } @Throws(EncodingException::class) - override fun doFinalProtected() { - buffer.finalize() - } + override fun doFinalProtected() { buffer.finalize() } } } @@ -687,26 +615,13 @@ public sealed class Base32(config: C): EncoderDecoder< private val buffer = EncodingBuffer( out = out, - table = if (config.encodeToLowercase) { - CHARS_LOWER - } else { - CHARS_UPPER - }, - isConstantTime = config.isConstantTime, - paddingChar = if (config.padEncoded) { - config.paddingChar - } else { - null - }, + table = if (config.encodeToLowercase) CHARS_LOWER else CHARS_UPPER, + paddingChar = if (config.padEncoded) config.paddingChar else null, ) - override fun consumeProtected(input: Byte) { - buffer.update(input.toInt()) - } + override fun consumeProtected(input: Byte) { buffer.update(input.toInt()) } - override fun doFinalProtected() { - buffer.finalize() - } + override fun doFinalProtected() { buffer.finalize() } } } @@ -762,8 +677,6 @@ public sealed class Base32(config: C): EncoderDecoder< public val encodeToLowercase: Boolean, @JvmField public val padEncoded: Boolean, - @JvmField - public val isConstantTime: Boolean, ): EncoderDecoder.Config( isLenient = isLenient, lineBreakInterval = lineBreakInterval, @@ -799,10 +712,16 @@ public sealed class Base32(config: C): EncoderDecoder< lineBreakInterval = builder.lineBreakInterval, encodeToLowercase = builder.encodeToLowercase, padEncoded = builder.padEncoded, - isConstantTime = builder.isConstantTime, ) } } + + /** + * Implementation is always constant-time. Performance impact is negligible. + * @suppress + * */ + @JvmField + public val isConstantTime: Boolean = true } /** @@ -840,74 +759,50 @@ public sealed class Base32(config: C): EncoderDecoder< override fun newEncoderFeedProtected(out: OutFeed): Encoder.Feed { return DELEGATE.newEncoderFeedProtected(out) } - - private val CT_CASE = CTCase(table = CHARS_UPPER) - - private val UC_PARSER = DecoderAction.Parser( - '0'..'9' to DecoderAction { char -> - // char ASCII value - // 0 48 0 - // 9 57 9 (ASCII - 48) - char.code - 48 - }, - CT_CASE.uppers to DecoderAction { char -> - // char ASCII value - // A 65 10 - // V 86 31 (ASCII - 55) - char.code - 55 - }, - CT_CASE.lowers to DecoderAction { char -> - // char ASCII value - // A 65 10 - // V 86 31 (ASCII - 55) - char.uppercaseChar().code - 55 - }, - ) - - // Assume input will be lowercase letters. Reorder - // actions to check lowercase before uppercase. - private val LC_PARSER = DecoderAction.Parser( - UC_PARSER.actions[0], - UC_PARSER.actions[2], - UC_PARSER.actions[1], - ) - - // Do not include lowercase letter actions. Constant time - // operations will uppercase the input on every invocation. - private val CT_PARSER = DecoderAction.Parser( - UC_PARSER.actions[0], - UC_PARSER.actions[1], - ) } protected override fun newDecoderFeedProtected(out: Decoder.OutFeed): Decoder.Feed { return object : Decoder.Feed() { private val buffer = DecodingBuffer(out) - private val parser = when { - config.isConstantTime -> CT_PARSER - config.encodeToLowercase -> LC_PARSER - else -> UC_PARSER - } @Throws(EncodingException::class) override fun consumeProtected(input: Char) { - val char = if (config.isConstantTime) { - CT_CASE.uppercase(input) ?: input - } else { - input - } + val code = input.code + + val ge0: Byte = if (code >= '0'.code) 1 else 0 + val le9: Byte = if (code <= '9'.code) 1 else 0 + val geA: Byte = if (code >= 'A'.code) 1 else 0 + val leV: Byte = if (code <= 'V'.code) 1 else 0 + val gea: Byte = if (code >= 'a'.code) 1 else 0 + val lev: Byte = if (code <= 'v'.code) 1 else 0 + + var diff = 0 + + // char ASCII value + // 0 48 0 + // 9 57 9 (ASCII - 48) + diff += if (ge0 + le9 == 2) -48 else 0 + + // char ASCII value + // A 65 10 + // V 86 31 (ASCII - 55) + diff += if (geA + leV == 2) -55 else 0 - val bits = parser.parse(char, isConstantTime = config.isConstantTime) - ?: throw EncodingException("Char[${input}] is not a valid Base32 Hex character") + // char ASCII value + // a 97 10 + // v 118 31 (ASCII - 87) + diff += if (gea + lev == 2) -87 else 0 - buffer.update(bits) + if (diff == 0) { + throw EncodingException("Char[${input}] is not a valid Base32 Hex character") + } + + buffer.update(code + diff) } @Throws(EncodingException::class) - override fun doFinalProtected() { - buffer.finalize() - } + override fun doFinalProtected() { buffer.finalize() } } } @@ -916,36 +811,94 @@ public sealed class Base32(config: C): EncoderDecoder< private val buffer = EncodingBuffer( out = out, - table = if (config.encodeToLowercase) { - CHARS_LOWER - } else { - CHARS_UPPER - }, - isConstantTime = config.isConstantTime, - paddingChar = if (config.padEncoded) { - config.paddingChar - } else { - null - }, + table = if (config.encodeToLowercase) CHARS_LOWER else CHARS_UPPER, + paddingChar = if (config.padEncoded) config.paddingChar else null, ) - override fun consumeProtected(input: Byte) { - buffer.update(input.toInt()) - } + override fun consumeProtected(input: Byte) { buffer.update(input.toInt()) } - override fun doFinalProtected() { - buffer.finalize() - } + override fun doFinalProtected() { buffer.finalize() } } } protected override fun name(): String = "Base32.Hex" } + private inner class DecodingBuffer(out: Decoder.OutFeed): FeedBuffer( + blockSize = 8, + flush = { buffer -> + // Append each char's 5 bits to the buffer + var bitBuffer = 0L + for (bits in buffer) { + bitBuffer = (bitBuffer shl 5) or bits.toLong() + } + + // For every 8 chars of input, we accumulate + // 40 bits of output data. Emit 5 bytes. + out.output((bitBuffer shr 32).toByte()) + out.output((bitBuffer shr 24).toByte()) + out.output((bitBuffer shr 16).toByte()) + out.output((bitBuffer shr 8).toByte()) + out.output((bitBuffer ).toByte()) + }, + finalize = { modulus, buffer -> + when (modulus) { + 1, 3, 6 -> { + // 5*1 = 5 bits. Truncated, fail. + // 5*3 = 15 bits. Truncated, fail. + // 5*6 = 30 bits. Truncated, fail. + throw truncatedInputEncodingException(modulus) + } + } + + var bitBuffer = 0L + for (i in 0 until modulus) { + bitBuffer = (bitBuffer shl 5) or buffer[i].toLong() + } + + when (modulus) { + 0 -> { /* no-op */ } + 2 -> { + // 5*2 = 10 bits. Drop 2 + bitBuffer = bitBuffer shr 2 + + // 8/8 = 1 byte + out.output((bitBuffer ).toByte()) + } + 4 -> { + // 5*4 = 20 bits. Drop 4 + bitBuffer = bitBuffer shr 4 + + // 16/8 = 2 bytes + out.output((bitBuffer shr 8).toByte()) + out.output((bitBuffer ).toByte()) + } + 5 -> { + // 5*5 = 25 bits. Drop 1 + bitBuffer = bitBuffer shr 1 + + // 24/8 = 3 bytes + out.output((bitBuffer shr 16).toByte()) + out.output((bitBuffer shr 8).toByte()) + out.output((bitBuffer ).toByte()) + } + 7 -> { + // 5*7 = 35 bits. Drop 3 + bitBuffer = bitBuffer shr 3 + + // 32/8 = 4 bytes + out.output((bitBuffer shr 24).toByte()) + out.output((bitBuffer shr 16).toByte()) + out.output((bitBuffer shr 8).toByte()) + out.output((bitBuffer ).toByte()) + } + } + } + ) + private inner class EncodingBuffer( out: Encoder.OutFeed, table: CharSequence, - isConstantTime: Boolean, paddingChar: Char?, ): FeedBuffer( blockSize = 5, @@ -968,45 +921,14 @@ public sealed class Base32(config: C): EncoderDecoder< val i7 = (bitBuffer shr 5 and 0x1fL).toInt() // 40-7*5 = 5 val i8 = (bitBuffer and 0x1fL).toInt() // 40-8*5 = 0 - if (isConstantTime) { - var c1: Char? = null - var c2: Char? = null - var c3: Char? = null - var c4: Char? = null - var c5: Char? = null - var c6: Char? = null - var c7: Char? = null - var c8: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - c3 = if (index == i3) c else c3 - c4 = if (index == i4) c else c4 - c5 = if (index == i5) c else c5 - c6 = if (index == i6) c else c6 - c7 = if (index == i7) c else c7 - c8 = if (index == i8) c else c8 - } - - out.output(c1!!) - out.output(c2!!) - out.output(c3!!) - out.output(c4!!) - out.output(c5!!) - out.output(c6!!) - out.output(c7!!) - out.output(c8!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - out.output(table[i3]) - out.output(table[i4]) - out.output(table[i5]) - out.output(table[i6]) - out.output(table[i7]) - out.output(table[i8]) - } + out.output(table[i1]) + out.output(table[i2]) + out.output(table[i3]) + out.output(table[i4]) + out.output(table[i5]) + out.output(table[i6]) + out.output(table[i7]) + out.output(table[i8]) }, finalize = { modulus, buffer -> var bitBuffer = 0L @@ -1023,21 +945,8 @@ public sealed class Base32(config: C): EncoderDecoder< val i1 = (bitBuffer shr 3 and 0x1fL).toInt() // 8-1*5 = 3 val i2 = (bitBuffer shl 2 and 0x1fL).toInt() // 5-3 = 2 - if (isConstantTime) { - var c1: Char? = null - var c2: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - } - - out.output(c1!!) - out.output(c2!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - } + out.output(table[i1]) + out.output(table[i2]) 6 } @@ -1048,29 +957,10 @@ public sealed class Base32(config: C): EncoderDecoder< val i3 = (bitBuffer shr 1 and 0x1fL).toInt() // 16-3*5 = 1 val i4 = (bitBuffer shl 4 and 0x1fL).toInt() // 5-1 = 4 - if (isConstantTime) { - var c1: Char? = null - var c2: Char? = null - var c3: Char? = null - var c4: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - c3 = if (index == i3) c else c3 - c4 = if (index == i4) c else c4 - } - - out.output(c1!!) - out.output(c2!!) - out.output(c3!!) - out.output(c4!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - out.output(table[i3]) - out.output(table[i4]) - } + out.output(table[i1]) + out.output(table[i2]) + out.output(table[i3]) + out.output(table[i4]) 4 } @@ -1082,33 +972,11 @@ public sealed class Base32(config: C): EncoderDecoder< val i4 = (bitBuffer shr 4 and 0x1fL).toInt() // 24-4*5 = 4 val i5 = (bitBuffer shl 1 and 0x1fL).toInt() // 5-4 = 1 - if (isConstantTime) { - var c1: Char? = null - var c2: Char? = null - var c3: Char? = null - var c4: Char? = null - var c5: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - c3 = if (index == i3) c else c3 - c4 = if (index == i4) c else c4 - c5 = if (index == i5) c else c5 - } - - out.output(c1!!) - out.output(c2!!) - out.output(c3!!) - out.output(c4!!) - out.output(c5!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - out.output(table[i3]) - out.output(table[i4]) - out.output(table[i5]) - } + out.output(table[i1]) + out.output(table[i2]) + out.output(table[i3]) + out.output(table[i4]) + out.output(table[i5]) 3 } @@ -1123,123 +991,21 @@ public sealed class Base32(config: C): EncoderDecoder< val i6 = (bitBuffer shr 2 and 0x1fL).toInt() // 32-6*5 = 2 val i7 = (bitBuffer shl 3 and 0x1fL).toInt() // 5-2 = 3 - if (isConstantTime) { - var c1: Char? = null - var c2: Char? = null - var c3: Char? = null - var c4: Char? = null - var c5: Char? = null - var c6: Char? = null - var c7: Char? = null - - table.forEachIndexed { index, c -> - c1 = if (index == i1) c else c1 - c2 = if (index == i2) c else c2 - c3 = if (index == i3) c else c3 - c4 = if (index == i4) c else c4 - c5 = if (index == i5) c else c5 - c6 = if (index == i6) c else c6 - c7 = if (index == i7) c else c7 - } - - out.output(c1!!) - out.output(c2!!) - out.output(c3!!) - out.output(c4!!) - out.output(c5!!) - out.output(c6!!) - out.output(c7!!) - } else { - out.output(table[i1]) - out.output(table[i2]) - out.output(table[i3]) - out.output(table[i4]) - out.output(table[i5]) - out.output(table[i6]) - out.output(table[i7]) - } + out.output(table[i1]) + out.output(table[i2]) + out.output(table[i3]) + out.output(table[i4]) + out.output(table[i5]) + out.output(table[i6]) + out.output(table[i7]) 1 } } if (paddingChar != null) { - repeat(padCount) { - out.output(paddingChar) - } + repeat(padCount) { out.output(paddingChar) } } }, ) - - private inner class DecodingBuffer(out: Decoder.OutFeed): FeedBuffer( - blockSize = 8, - flush = { buffer -> - // Append each char's 5 bits to the buffer - var bitBuffer = 0L - for (bits in buffer) { - bitBuffer = (bitBuffer shl 5) or bits.toLong() - } - - // For every 8 chars of input, we accumulate - // 40 bits of output data. Emit 5 bytes. - out.output((bitBuffer shr 32).toByte()) - out.output((bitBuffer shr 24).toByte()) - out.output((bitBuffer shr 16).toByte()) - out.output((bitBuffer shr 8).toByte()) - out.output((bitBuffer ).toByte()) - }, - finalize = { modulus, buffer -> - when (modulus) { - 1, 3, 6 -> { - // 5*1 = 5 bits. Truncated, fail. - // 5*3 = 15 bits. Truncated, fail. - // 5*6 = 30 bits. Truncated, fail. - throw truncatedInputEncodingException(modulus) - } - } - - var bitBuffer = 0L - for (i in 0 until modulus) { - bitBuffer = (bitBuffer shl 5) or buffer[i].toLong() - } - - when (modulus) { - 0 -> { /* no-op */ } - 2 -> { - // 5*2 = 10 bits. Drop 2 - bitBuffer = bitBuffer shr 2 - - // 8/8 = 1 byte - out.output((bitBuffer ).toByte()) - } - 4 -> { - // 5*4 = 20 bits. Drop 4 - bitBuffer = bitBuffer shr 4 - - // 16/8 = 2 bytes - out.output((bitBuffer shr 8).toByte()) - out.output((bitBuffer ).toByte()) - } - 5 -> { - // 5*5 = 25 bits. Drop 1 - bitBuffer = bitBuffer shr 1 - - // 24/8 = 3 bytes - out.output((bitBuffer shr 16).toByte()) - out.output((bitBuffer shr 8).toByte()) - out.output((bitBuffer ).toByte()) - } - 7 -> { - // 5*7 = 35 bits. Drop 3 - bitBuffer = bitBuffer shr 3 - - // 32/8 = 4 bytes - out.output((bitBuffer shr 24).toByte()) - out.output((bitBuffer shr 16).toByte()) - out.output((bitBuffer shr 8).toByte()) - out.output((bitBuffer ).toByte()) - } - } - } - ) } diff --git a/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Builders.kt b/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Builders.kt index 9b4a9ff9..10484bfa 100644 --- a/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Builders.kt +++ b/library/base32/src/commonMain/kotlin/io/matthewnelson/encoding/base32/Builders.kt @@ -146,7 +146,6 @@ public class Base32CrockfordConfigBuilder { public constructor() public constructor(config: Base32.Crockford.Config?): this() { if (config == null) return - isConstantTime = config.isConstantTime isLenient = config.isLenient ?: true encodeToLowercase = config.encodeToLowercase hyphenInterval = config.hyphenInterval @@ -154,38 +153,6 @@ public class Base32CrockfordConfigBuilder { finalizeWhenFlushed = config.finalizeWhenFlushed } - /** - * If true, will utilize constant-time operations when - * encoding/decoding data. This will be slower, but help - * mitigate potential timing attacks with sensitive data - * (such as private key material). - * - * If false, will not use constant time operations. - * - * e.g. (NOT constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * for (c in this) { - * if (c == char) return true - * } - * return false - * } - * - * e.g. (YES constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * var result = false - * for (c in this) { - * result = if (c == char) true else result - * } - * return result - * } - * - * Default: `false` - * */ - @JvmField - public var isConstantTime: Boolean = false - /** * If true, spaces and new lines ('\n', '\r', ' ', '\t') * will be skipped over when decoding (against Crockford spec). @@ -339,6 +306,11 @@ public class Base32CrockfordConfigBuilder { * Builds a [Base32.Crockford.Config] for the provided settings. * */ public fun build(): Base32.Crockford.Config = Base32.Crockford.Config.from(this) + + /** @suppress */ + @JvmField + @Deprecated(message = "Implementation is always constant time. Performance impact is negligible.") + public var isConstantTime: Boolean = true } /** @@ -352,45 +324,12 @@ public class Base32DefaultConfigBuilder { public constructor() public constructor(config: Base32.Default.Config?): this() { if (config == null) return - isConstantTime = config.isConstantTime isLenient = config.isLenient ?: true lineBreakInterval = config.lineBreakInterval encodeToLowercase = config.encodeToLowercase padEncoded = config.padEncoded } - /** - * If true, will utilize constant-time operations when - * encoding/decoding data. This will be slower, but help - * mitigate potential timing attacks with sensitive data - * (such as private key material). - * - * If false, will not use constant time operations. - * - * e.g. (NOT constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * for (c in this) { - * if (c == char) return true - * } - * return false - * } - * - * e.g. (YES constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * var result = false - * for (c in this) { - * result = if (c == char) true else result - * } - * return result - * } - * - * Default: `false` - * */ - @JvmField - public var isConstantTime: Boolean = false - /** * If true, spaces and new lines ('\n', '\r', ' ', '\t') * will be skipped over when decoding (against RFC 4648). @@ -475,6 +414,11 @@ public class Base32DefaultConfigBuilder { * Builds a [Base32.Default.Config] for the provided settings. * */ public fun build(): Base32.Default.Config = Base32.Default.Config.from(this) + + /** @suppress */ + @JvmField + @Deprecated(message = "Implementation is always constant time. Performance impact is negligible.") + public var isConstantTime: Boolean = true } /** @@ -488,45 +432,12 @@ public class Base32HexConfigBuilder { public constructor() public constructor(config: Base32.Hex.Config?): this() { if (config == null) return - isConstantTime = config.isConstantTime isLenient = config.isLenient ?: true lineBreakInterval = config.lineBreakInterval encodeToLowercase = config.encodeToLowercase padEncoded = config.padEncoded } - /** - * If true, will utilize constant-time operations when - * encoding/decoding data. This will be slower, but help - * mitigate potential timing attacks with sensitive data - * (such as private key material). - * - * If false, will not use constant time operations. - * - * e.g. (NOT constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * for (c in this) { - * if (c == char) return true - * } - * return false - * } - * - * e.g. (YES constant-time operation) - * - * fun String.containsChar(char: Char): Boolean { - * var result = false - * for (c in this) { - * result = if (c == char) true else result - * } - * return result - * } - * - * Default: `false` - * */ - @JvmField - public var isConstantTime: Boolean = false - /** * If true, spaces and new lines ('\n', '\r', ' ', '\t') * will be skipped over when decoding (against RFC 4648). @@ -611,4 +522,9 @@ public class Base32HexConfigBuilder { * Builds a [Base32.Hex.Config] for the provided settings. * */ public fun build(): Base32.Hex.Config = Base32.Hex.Config.from(this) + + /** @suppress */ + @JvmField + @Deprecated(message = "Implementation is always constant time. Performance impact is negligible.") + public var isConstantTime: Boolean = true } diff --git a/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32CrockfordUnitTest.kt b/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32CrockfordUnitTest.kt index d6750377..195f1308 100644 --- a/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32CrockfordUnitTest.kt +++ b/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32CrockfordUnitTest.kt @@ -27,7 +27,6 @@ import kotlin.test.* class Base32CrockfordUnitTest: BaseNEncodingTest() { private val validCheckSymbols = listOf('*', '~', '$', '=', 'U', 'u') - private var useConstantTime = false private var useLowercase = false private var symbol: Char? = null set(value) { @@ -39,7 +38,6 @@ class Base32CrockfordUnitTest: BaseNEncodingTest() { } private fun base32(): Base32.Crockford = Base32Crockford { - isConstantTime = useConstantTime encodeToLowercase = useLowercase checkSymbol(symbol) } @@ -284,22 +282,16 @@ class Base32CrockfordUnitTest: BaseNEncodingTest() { @Test fun givenString_whenEncoded_MatchesSpec() { checkEncodeSuccessForDataSet(encodeSuccessDataSet) - useConstantTime = true - checkEncodeSuccessForDataSet(encodeSuccessDataSet) } @Test fun givenBadEncoding_whenDecoded_ReturnsNull() { checkDecodeFailureForDataSet(decodeFailureDataSet) - useConstantTime = true - checkDecodeFailureForDataSet(decodeFailureDataSet) } @Test fun givenEncodedData_whenDecoded_MatchesSpec() { checkDecodeSuccessForDataSet(decodeSuccessDataSet) - useConstantTime = true - checkDecodeSuccessForDataSet(decodeSuccessDataSet) } @Test @@ -316,9 +308,6 @@ class Base32CrockfordUnitTest: BaseNEncodingTest() { fun givenEncodedDataWithCheckSymbol_whenDecodedWithCheckSymbolExpressed_returnsExpected() { for (symbol in validCheckSymbols) { this.symbol = symbol - useConstantTime = false - checkEncodeSuccessForDataSet(getEncodeSuccessDataSetWithCheckSymbolExpected(symbol)) - useConstantTime = true checkEncodeSuccessForDataSet(getEncodeSuccessDataSetWithCheckSymbolExpected(symbol)) } } @@ -327,9 +316,6 @@ class Base32CrockfordUnitTest: BaseNEncodingTest() { fun givenString_whenEncodedWithCheckSymbolExpressed_returnsExpected() { for (symbol in validCheckSymbols) { this.symbol = symbol - useConstantTime = false - checkDecodeSuccessForDataSet(getDecodeSuccessDataSetWithCheckSymbolExpected(symbol)) - useConstantTime = true checkDecodeSuccessForDataSet(getDecodeSuccessDataSetWithCheckSymbolExpected(symbol)) } } @@ -351,15 +337,11 @@ class Base32CrockfordUnitTest: BaseNEncodingTest() { @Test fun givenUniversalDecoderParameters_whenChecked_areSuccessful() { checkUniversalDecoderParameters() - useConstantTime = true - checkUniversalDecoderParameters() } @Test fun givenUniversalEncoderParameters_whenChecked_areSuccessful() { checkUniversalEncoderParameters() - useConstantTime = true - checkUniversalEncoderParameters() } @Test @@ -374,8 +356,6 @@ class Base32CrockfordUnitTest: BaseNEncodingTest() { } checkEncodeSuccessForDataSet(lowercaseData) - useConstantTime = true - checkEncodeSuccessForDataSet(lowercaseData) } @Test @@ -387,28 +367,21 @@ class Base32CrockfordUnitTest: BaseNEncodingTest() { @Test fun givenBase32Crockford_whenDecodeEncode_thenReturnsSameValue() { val expected = "AHM6A83HENMP6TS0C9S6YXVE41K6YY10D9TPTW3K41QQCSBJ41T6GS90DHGQMY90CHQPEBG" - listOf(false, true).forEach { ct -> - useConstantTime = ct - val encoder = base32() - val decoded = expected.decodeToByteArray(encoder) - val actual = decoded.encodeToString(encoder) - assertEquals(expected, actual) - } + val encoder = base32() + val decoded = expected.decodeToByteArray(encoder) + val actual = decoded.encodeToString(encoder) + assertEquals(expected, actual) } @Test fun givenBase32Crockford_whenEncodeDecodeRandomData_thenBytesMatch() { checkRandomData() - useConstantTime = true - checkRandomData() } @Test fun givenBase32CrockfordLowercase_whenEncodeDecodeRandomData_thenBytesMatch() { useLowercase = true checkRandomData() - useConstantTime = true - checkRandomData() } @Test diff --git a/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32DefaultUnitTest.kt b/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32DefaultUnitTest.kt index ec66954e..03115e12 100644 --- a/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32DefaultUnitTest.kt +++ b/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32DefaultUnitTest.kt @@ -26,12 +26,10 @@ import kotlin.test.assertEquals class Base32DefaultUnitTest: BaseNEncodingTest() { - private var useConstantTime = false private var useLowercase = false private var usePadding = true private fun base32(): Base32.Default = Base32Default { - isConstantTime = useConstantTime encodeToLowercase = useLowercase padEncoded = usePadding } @@ -139,36 +137,26 @@ class Base32DefaultUnitTest: BaseNEncodingTest() { @Test fun givenString_whenEncoded_MatchesRfc4648Spec() { checkEncodeSuccessForDataSet(encodeSuccessDataSet) - useConstantTime = true - checkEncodeSuccessForDataSet(encodeSuccessDataSet) } @Test fun givenBadEncoding_whenDecoded_ReturnsNull() { checkDecodeFailureForDataSet(decodeFailureDataSet) - useConstantTime = true - checkDecodeFailureForDataSet(decodeFailureDataSet) } @Test fun givenEncodedData_whenDecoded_MatchesRfc4648Spec() { checkDecodeSuccessForDataSet(decodeSuccessDataSet) - useConstantTime = true - checkDecodeSuccessForDataSet(decodeSuccessDataSet) } @Test fun givenUniversalDecoderParameters_whenChecked_areSuccessful() { checkUniversalDecoderParameters() - useConstantTime = true - checkUniversalDecoderParameters() } @Test fun givenUniversalEncoderParameters_whenChecked_areSuccessful() { checkUniversalEncoderParameters() - useConstantTime = true - checkUniversalEncoderParameters() } @Test @@ -183,8 +171,6 @@ class Base32DefaultUnitTest: BaseNEncodingTest() { } checkEncodeSuccessForDataSet(noPadData) - useConstantTime = true - checkEncodeSuccessForDataSet(noPadData) } @Test @@ -199,8 +185,6 @@ class Base32DefaultUnitTest: BaseNEncodingTest() { } checkEncodeSuccessForDataSet(lowercaseData) - useConstantTime = true - checkEncodeSuccessForDataSet(lowercaseData) } @Test @@ -212,28 +196,21 @@ class Base32DefaultUnitTest: BaseNEncodingTest() { @Test fun givenBase32Default_whenDecodeEncode_thenReturnsSameValue() { val expected = "OBTDFCGTEKTGXPVR23DA7YFDEB5IZGLEHJH5GIIVBKGL5S2HNNRQ====" - listOf(false, true).forEach { ct -> - useConstantTime = ct - val encoder = base32() - val decoded = expected.decodeToByteArray(encoder) - val actual = decoded.encodeToString(encoder) - assertEquals(expected, actual) - } + val encoder = base32() + val decoded = expected.decodeToByteArray(encoder) + val actual = decoded.encodeToString(encoder) + assertEquals(expected, actual) } @Test fun givenBase32Default_whenEncodeDecodeRandomData_thenBytesMatch() { checkRandomData() - useConstantTime = true - checkRandomData() } @Test fun givenBase32DefaultLowercase_whenEncodeDecodeRandomData_thenBytesMatch() { useLowercase = true checkRandomData() - useConstantTime = true - checkRandomData() } } diff --git a/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32HexUnitTest.kt b/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32HexUnitTest.kt index 917cbee8..c26e6e50 100644 --- a/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32HexUnitTest.kt +++ b/library/base32/src/commonTest/kotlin/io/matthewnelson/encoding/base32/Base32HexUnitTest.kt @@ -26,12 +26,10 @@ import kotlin.test.assertEquals class Base32HexUnitTest: BaseNEncodingTest() { - private var useConstantTime = false private var useLowercase = false private var usePadding = true private fun base32(): Base32.Hex = Base32Hex { - isConstantTime = useConstantTime encodeToLowercase = useLowercase padEncoded = usePadding } @@ -140,36 +138,26 @@ class Base32HexUnitTest: BaseNEncodingTest() { @Test fun givenString_whenEncoded_MatchesRfc4648Spec() { checkEncodeSuccessForDataSet(encodeSuccessDataSet) - useConstantTime = true - checkEncodeSuccessForDataSet(encodeSuccessDataSet) } @Test fun givenBadEncoding_whenDecoded_ReturnsNull() { checkDecodeFailureForDataSet(decodeFailureDataSet) - useConstantTime = true - checkDecodeFailureForDataSet(decodeFailureDataSet) } @Test fun givenEncodedData_whenDecoded_MatchesRfc4648Spec() { checkDecodeSuccessForDataSet(decodeSuccessDataSet) - useConstantTime = true - checkDecodeSuccessForDataSet(decodeSuccessDataSet) } @Test fun givenUniversalDecoderParameters_whenChecked_areSuccessful() { checkUniversalDecoderParameters() - useConstantTime = true - checkUniversalDecoderParameters() } @Test fun givenUniversalEncoderParameters_whenChecked_areSuccessful() { checkUniversalEncoderParameters() - useConstantTime = true - checkUniversalEncoderParameters() } @Test @@ -184,8 +172,6 @@ class Base32HexUnitTest: BaseNEncodingTest() { } checkEncodeSuccessForDataSet(noPadData) - useConstantTime = true - checkEncodeSuccessForDataSet(noPadData) } @Test @@ -200,8 +186,6 @@ class Base32HexUnitTest: BaseNEncodingTest() { } checkEncodeSuccessForDataSet(lowercaseData) - useConstantTime = true - checkEncodeSuccessForDataSet(lowercaseData) } @Test @@ -213,28 +197,21 @@ class Base32HexUnitTest: BaseNEncodingTest() { @Test fun givenBase32Hex_whenDecodeEncode_thenReturnsSameValue() { val expected = "AHK6A83HELKM6QP0C9P6UTRE41J6UU10D9QMQS3J41NNCPBI41Q6GP90DHGNKU90CHNMEBG=" - listOf(false, true).forEach { ct -> - useConstantTime = ct - val encoder = base32() - val decoded = expected.decodeToByteArray(encoder) - val actual = decoded.encodeToString(encoder) - assertEquals(expected, actual) - } + val encoder = base32() + val decoded = expected.decodeToByteArray(encoder) + val actual = decoded.encodeToString(encoder) + assertEquals(expected, actual) } @Test fun givenBase32Hex_whenEncodeDecodeRandomData_thenBytesMatch() { checkRandomData() - useConstantTime = true - checkRandomData() } @Test fun givenBase32HexLowercase_whenEncodeDecodeRandomData_thenBytesMatch() { useLowercase = true checkRandomData() - useConstantTime = true - checkRandomData() } } From f54b3c985329cbad5e61d33933ad47c53327ff77 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 14:33:14 -0500 Subject: [PATCH 7/9] Deprecate CTCase --- .../kotlin/io/matthewnelson/encoding/core/util/CTCase.kt | 8 +++++++- .../io/matthewnelson/encoding/core/util/CTCaseUnitTest.kt | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt index c66cf765..3fd125e8 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt @@ -15,7 +15,7 @@ **/ package io.matthewnelson.encoding.core.util -import io.matthewnelson.immutable.collections.toImmutableSet +import io.matthewnelson.preimmutable.collections.toImmutableSet import kotlin.jvm.JvmField /** @@ -50,19 +50,23 @@ import kotlin.jvm.JvmField * @param [table] The decoding table (e.g. `ABCDEFGHIJKLMNOPQRSTUVWXYZ234567`) * @throws [IllegalArgumentException] if table contains a letter that has no * corresponding lowercase value. + * @suppress * */ +@Deprecated("Implementation is incredibly slow. Diff ASCII values instead.") public class CTCase @Throws(IllegalArgumentException::class) public constructor(table: CharSequence) { /** * Uppercase letters + * @suppress * */ @JvmField public val uppers: Set /** * Lowercase letters corresponding to [uppers] + * @suppress * */ @JvmField public val lowers: Set @@ -70,6 +74,7 @@ public constructor(table: CharSequence) { /** * If the provided [char] exists within [lowers], its corresponding * uppercase value is returned. If nothing is found, `null` is returned. + * @suppress * */ public fun uppercase(char: Char): Char? { val iLower = lowers.iterator() @@ -89,6 +94,7 @@ public constructor(table: CharSequence) { /** * If the provided [char] exists within [uppers], its corresponding * lowercase value is returned. If nothing is found, `null` is returned. + * @suppress * */ public fun lowercase(char: Char): Char? { val iLower = lowers.iterator() diff --git a/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/CTCaseUnitTest.kt b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/CTCaseUnitTest.kt index 742f2b53..fd91c35b 100644 --- a/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/CTCaseUnitTest.kt +++ b/library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/CTCaseUnitTest.kt @@ -20,6 +20,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNull +@Suppress("DEPRECATION") class CTCaseUnitTest { @Test From 8e286a5723267c31dd5dbbc22dd800403d0275f2 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 14:33:31 -0500 Subject: [PATCH 8/9] Deprecate DecoderAction + Parser --- .../io/matthewnelson/encoding/core/util/DecoderAction.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt index 82699c96..4266f9e4 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt @@ -23,11 +23,14 @@ import kotlin.jvm.JvmField * An action for decoding * * @see [Parser] + * @suppress * */ +@Deprecated("Implementation is incredibly slow. Do not use.") public fun interface DecoderAction { /** * Convert decoder input character to its bitwise integer value. + * @suppress * */ public fun convert(input: Char): Int @@ -72,7 +75,10 @@ public fun interface DecoderAction { * } * * @param [action] Pairs of character ranges and their associated [DecoderAction] + * @suppress * */ + @Suppress("DEPRECATION") + @Deprecated("Implementation is incredibly slow. Do not use.") public class Parser(vararg action: Pair, DecoderAction>) { @JvmField @@ -87,6 +93,7 @@ public fun interface DecoderAction { * loops early in the event a match is found. * @return The result of [DecoderAction.convert], or `null` if no * match was found. + * @suppress * */ public fun parse(input: Char, isConstantTime: Boolean): Int? { var da: DecoderAction? = null From a2c856bbe1fd632fd5eb6c4cbf058ca9cc900f17 Mon Sep 17 00:00:00 2001 From: Matthew Nelson Date: Tue, 17 Dec 2024 14:38:52 -0500 Subject: [PATCH 9/9] Remove immutable:collections dependency --- .kotlin-js-store/yarn.lock | 55 ++++++++++++++----- README.md | 3 - benchmarks/build.gradle.kts | 1 - gradle/libs.versions.toml | 4 -- library/core/build.gradle.kts | 10 +--- .../encoding/core/util/CTCase.kt | 5 +- .../encoding/core/util/DecoderAction.kt | 6 +- .../core/src/jvmMain/java9/module-info.java | 1 - 8 files changed, 47 insertions(+), 38 deletions(-) diff --git a/.kotlin-js-store/yarn.lock b/.kotlin-js-store/yarn.lock index eda205e0..da67d2fd 100644 --- a/.kotlin-js-store/yarn.lock +++ b/.kotlin-js-store/yarn.lock @@ -428,7 +428,7 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== -call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: +call-bind-apply-helpers@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz#32e5892e6361b29b0b545ba6f7763378daca2840" integrity sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g== @@ -436,7 +436,7 @@ call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1: es-errors "^1.3.0" function-bind "^1.1.2" -call-bound@^1.0.2: +call-bound@^1.0.2, call-bound@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.3.tgz#41cfd032b593e39176a71533ab4f384aa04fd681" integrity sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA== @@ -658,11 +658,11 @@ dom-serialize@^2.2.1: void-elements "^2.0.0" dunder-proto@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.0.tgz#c2fce098b3c8f8899554905f4377b6d85dabaa80" - integrity sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A== + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== dependencies: - call-bind-apply-helpers "^1.0.0" + call-bind-apply-helpers "^1.0.1" es-errors "^1.3.0" gopd "^1.2.0" @@ -672,9 +672,9 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.5.73: - version "1.5.73" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz#f32956ce40947fa3c8606726a96cd8fb5bb5f720" - integrity sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg== + version "1.5.74" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz#cb886b504a6467e4c00bea3317edb38393c53413" + integrity sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw== emoji-regex@^8.0.0: version "8.0.0" @@ -716,11 +716,14 @@ enhanced-resolve@^5.13.0: tapable "^2.2.0" ent@~2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.1.tgz#68dc99a002f115792c26239baedaaea9e70c0ca2" - integrity sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A== + version "2.2.2" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.2.tgz#22a5ed2fd7ce0cbcff1d1474cf4909a44bdb6e85" + integrity sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw== dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" punycode "^1.4.1" + safe-regex-test "^1.1.0" envinfo@^7.7.3: version "7.14.0" @@ -976,11 +979,18 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.1.0: +has-symbols@^1.0.3, has-symbols@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -1101,6 +1111,16 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -1614,6 +1634,15 @@ safe-buffer@^5.1.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" diff --git a/README.md b/README.md index 1b7b424c..b553a99a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![badge-latest-release]][url-latest-release] [![badge-kotlin]][url-kotlin] -[![badge-immutable]][url-immutable] ![badge-platform-android] ![badge-platform-jvm] @@ -306,7 +305,6 @@ dependencies { [badge-kotlin]: https://img.shields.io/badge/kotlin-1.9.24-blue.svg?logo=kotlin -[badge-immutable]: https://img.shields.io/badge/immutable-0.1.4-blue.svg?style=flat [badge-platform-android]: http://img.shields.io/badge/-android-6EDB8D.svg?style=flat @@ -328,4 +326,3 @@ dependencies { [url-latest-release]: https://github.com/05nelsonm/encoding/releases/latest [url-license]: https://www.apache.org/licenses/LICENSE-2.0.txt [url-kotlin]: https://kotlinlang.org -[url-immutable]: https://github.com/05nelsonm/immutable diff --git a/benchmarks/build.gradle.kts b/benchmarks/build.gradle.kts index c4fbaa0d..1b361bb6 100644 --- a/benchmarks/build.gradle.kts +++ b/benchmarks/build.gradle.kts @@ -54,7 +54,6 @@ kmpConfiguration { sourceSetMain { dependencies { implementation(libs.benchmark.runtime) - implementation(libs.immutable.collections) implementation(project(":library:base16")) implementation(project(":library:base32")) implementation(project(":library:base64")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 159a8264..fe5e6a3a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,6 @@ gradle-kmp-configuration = "0.3.2" gradle-kotlin = "1.9.24" gradle-publish-maven = "0.29.0" -immutable = "0.1.4" - [libraries] benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "gradle-benchmark" } @@ -16,8 +14,6 @@ gradle-kmp-configuration = { module = "io.matthewnelson:gradle-kmp-configuration gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "gradle-kotlin" } gradle-publish-maven = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "gradle-publish-maven" } -immutable-collections = { module = "io.matthewnelson.immutable:collections", version.ref = "immutable" } - [plugins] benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "gradle-benchmark" } binary-compat = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "gradle-binary-compat" } diff --git a/library/core/build.gradle.kts b/library/core/build.gradle.kts index bd1dae69..c2995752 100644 --- a/library/core/build.gradle.kts +++ b/library/core/build.gradle.kts @@ -19,13 +19,5 @@ plugins { } kmpConfiguration { - configureShared(java9ModuleName = "io.matthewnelson.encoding.core", publish = true) { - common { - sourceSetMain { - dependencies { - implementation(libs.immutable.collections) - } - } - } - } + configureShared(java9ModuleName = "io.matthewnelson.encoding.core", publish = true) {} } diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt index 3fd125e8..c419821c 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/CTCase.kt @@ -15,7 +15,6 @@ **/ package io.matthewnelson.encoding.core.util -import io.matthewnelson.preimmutable.collections.toImmutableSet import kotlin.jvm.JvmField /** @@ -126,7 +125,7 @@ public constructor(table: CharSequence) { require(u.size == l.size) { "uppers.size[${u.size}] != lowers.size[${l.size}] (invalid character input)." } - uppers = u.toImmutableSet() - lowers = l.toImmutableSet() + uppers = u.toSet() + lowers = l.toSet() } } diff --git a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt index 4266f9e4..093eed3d 100644 --- a/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt +++ b/library/core/src/commonMain/kotlin/io/matthewnelson/encoding/core/util/DecoderAction.kt @@ -15,8 +15,6 @@ **/ package io.matthewnelson.encoding.core.util -import io.matthewnelson.immutable.collections.toImmutableList -import io.matthewnelson.immutable.collections.toImmutableSet import kotlin.jvm.JvmField /** @@ -117,10 +115,10 @@ public fun interface DecoderAction { iterable } else { iterable.mapTo(LinkedHashSet(1, 1.0f)) { it } - }.toImmutableSet() + }.toSet() set to action - }.toImmutableList() + }.toList() this.actions = converted } diff --git a/library/core/src/jvmMain/java9/module-info.java b/library/core/src/jvmMain/java9/module-info.java index 759178a8..473fae2c 100644 --- a/library/core/src/jvmMain/java9/module-info.java +++ b/library/core/src/jvmMain/java9/module-info.java @@ -1,6 +1,5 @@ module io.matthewnelson.encoding.core { requires transitive kotlin.stdlib; - requires io.matthewnelson.immutable.collections; exports io.matthewnelson.encoding.core; exports io.matthewnelson.encoding.core.util;