diff --git a/libs/encryption/utils/build.gradle.kts b/libs/encryption/utils/build.gradle.kts index 510a30949e..ebc11adb14 100644 --- a/libs/encryption/utils/build.gradle.kts +++ b/libs/encryption/utils/build.gradle.kts @@ -1,17 +1,44 @@ plugins { - alias(libs.plugins.flipcash.android.library) + kotlin("multiplatform") + id("com.android.kotlin.multiplatform.library") } -android { - namespace = "${Gradle.codeNamespace}.encryption.utils" -} +kotlin { + android { + namespace = "com.getcode.encryption.utils" + compileSdk = 37 + minSdk = 29 + withHostTest {} + } -dependencies { - implementation(project(":libs:encryption:base58")) - implementation(libs.protobuf.kotlin.lite) - implementation(project(":libs:encryption:ed25519")) - implementation(libs.bundles.kotlinx.serialization) + iosArm64() + iosSimulatorArm64() + iosX64() - testImplementation(kotlin("test")) - testImplementation(libs.robolectric) + sourceSets { + commonMain { + dependencies { + implementation(project(":libs:encryption:base58")) + } + } + androidMain { + dependencies { + implementation(project(":libs:encryption:ed25519")) + implementation(libs.protobuf.kotlin.lite) + implementation(libs.bundles.kotlinx.serialization) + implementation(libs.timber) + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + } + } + getByName("androidHostTest") { + dependencies { + implementation(kotlin("test")) + implementation(libs.robolectric) + } + } + } } diff --git a/libs/encryption/utils/src/test/kotlin/com/getcode/utils/Base64ExtensionsTest.kt b/libs/encryption/utils/src/androidHostTest/kotlin/com/getcode/utils/Base64ExtensionsTest.kt similarity index 100% rename from libs/encryption/utils/src/test/kotlin/com/getcode/utils/Base64ExtensionsTest.kt rename to libs/encryption/utils/src/androidHostTest/kotlin/com/getcode/utils/Base64ExtensionsTest.kt diff --git a/libs/encryption/utils/src/androidHostTest/kotlin/com/getcode/utils/UrlExtensionsTest.kt b/libs/encryption/utils/src/androidHostTest/kotlin/com/getcode/utils/UrlExtensionsTest.kt new file mode 100644 index 0000000000..9ee80aee26 --- /dev/null +++ b/libs/encryption/utils/src/androidHostTest/kotlin/com/getcode/utils/UrlExtensionsTest.kt @@ -0,0 +1,18 @@ +package com.getcode.utils + +import kotlin.test.Test +import kotlin.test.assertEquals + +class UrlExtensionsTest { + + @Test + fun urlEncodeDecodeRoundtrip() { + val original = "hello world & foo=bar" + assertEquals(original, original.urlEncode().urlDecode()) + } + + @Test + fun urlEncodeSpaces() { + assertEquals("hello+world", "hello world".urlEncode()) + } +} diff --git a/libs/encryption/utils/src/test/kotlin/com/getcode/utils/UtilsTest.kt b/libs/encryption/utils/src/androidHostTest/kotlin/com/getcode/utils/UtilsTest.kt similarity index 100% rename from libs/encryption/utils/src/test/kotlin/com/getcode/utils/UtilsTest.kt rename to libs/encryption/utils/src/androidHostTest/kotlin/com/getcode/utils/UtilsTest.kt diff --git a/libs/encryption/utils/src/androidMain/kotlin/com/getcode/utils/Extensions.android.kt b/libs/encryption/utils/src/androidMain/kotlin/com/getcode/utils/Extensions.android.kt new file mode 100644 index 0000000000..6bbbd55e96 --- /dev/null +++ b/libs/encryption/utils/src/androidMain/kotlin/com/getcode/utils/Extensions.android.kt @@ -0,0 +1,48 @@ +package com.getcode.utils + +import android.util.Base64 +import com.getcode.ed25519.Ed25519 +import com.getcode.vendor.Base58 +import com.google.protobuf.ByteString +import java.net.URLDecoder +import java.net.URLEncoder +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +fun List.toByteString(): ByteString = ByteString.copyFrom(this.toByteArray()) +fun ByteArray.toByteString(): ByteString = ByteString.copyFrom(this) + +val List.base64: String + get() = Base64.encodeToString(toByteArray(), Base64.NO_WRAP) + +fun String.decodeBase64(): ByteArray = Base64.decode(this, Base64.NO_WRAP) + +fun String.decodeBase64UrlSafe(): ByteArray = Base64.decode(this, Base64.NO_WRAP or Base64.URL_SAFE) + +fun ByteArray.decodeBase64(): ByteArray = Base64.decode(this, Base64.NO_WRAP) + +val ByteArray.base64: String + get() = Base64.encodeToString(this, Base64.NO_WRAP) + +fun ByteArray.encodeBase64(urlSafe: Boolean = false): String { + val flags = if (urlSafe) Base64.NO_WRAP or Base64.URL_SAFE else Base64.NO_WRAP + return Base64.encodeToString(this, flags) +} + +fun ByteArray.encodeBase64ToArray(): ByteArray = Base64.encode(this, Base64.NO_WRAP) + +fun ByteArray.sha512(): ByteArray { + return try { + MessageDigest.getInstance("SHA-512") + .apply { update(this@sha512) } + .digest() + } catch (e: NoSuchAlgorithmException) { + throw RuntimeException("SHA-512 not implemented") + } +} + +fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8") + +fun String.urlDecode(): String = URLDecoder.decode(this, "UTF-8") + +fun Ed25519.KeyPair.getPublicKeyBase58(): String = Base58.encode(publicKeyBytes) diff --git a/libs/encryption/utils/src/androidMain/kotlin/com/getcode/utils/Utils.kt b/libs/encryption/utils/src/androidMain/kotlin/com/getcode/utils/Utils.kt new file mode 100644 index 0000000000..673631de11 --- /dev/null +++ b/libs/encryption/utils/src/androidMain/kotlin/com/getcode/utils/Utils.kt @@ -0,0 +1,412 @@ +package com.getcode.utils + +/* + * Copyright 2011 Google Inc. + * Copyright 2014 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.math.BigInteger +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import timber.log.Timber + +/** A collection of various utility methods that are helpful for working with the Bitcoin protocol. */ +object Utils { + const val TAG = "Api Utils" + + /** Hex encoder for use throughout the framework. */ + @JvmField + val HEX = HexEncoder() + + class HexEncoder { + private val hexChars = "0123456789abcdef".toCharArray() + + fun encode(data: ByteArray): String { + val result = CharArray(data.size * 2) + for (i in data.indices) { + result[i * 2] = hexChars[(data[i].toInt() shr 4) and 0xF] + result[i * 2 + 1] = hexChars[data[i].toInt() and 0xF] + } + return String(result) + } + } + + /** Joins an iterable of objects with a space separator. */ + @JvmStatic + fun spaceJoin(parts: Iterable<*>): String = parts.joinToString(" ") + + /** Max initial size of variable-length arrays to guard against memory exhaustion attacks. */ + const val MAX_INITIAL_ARRAY_LENGTH = 20 + + /** + * Encodes a positive BigInteger as a fixed-length big-endian byte array. + * This is the antagonist to [bytesToBigInteger]. + */ + @JvmStatic + fun bigIntegerToBytes(b: BigInteger, numBytes: Int): ByteArray { + require(b.signum() >= 0) { "b must be positive or zero" } + require(numBytes > 0) { "numBytes must be positive" } + val src = b.toByteArray() + val dest = ByteArray(numBytes) + val isFirstByteOnlyForSign = src[0] == 0.toByte() + val length = if (isFirstByteOnlyForSign) src.size - 1 else src.size + require(length <= numBytes) { "The given number does not fit in $numBytes" } + val srcPos = if (isFirstByteOnlyForSign) 1 else 0 + val destPos = numBytes - length + System.arraycopy(src, srcPos, dest, destPos, length) + return dest + } + + /** Converts a big-endian byte array to a positive BigInteger. Antagonist of [bigIntegerToBytes]. */ + @JvmStatic + fun bytesToBigInteger(bytes: ByteArray): BigInteger = BigInteger(1, bytes) + + /** Writes 2 bytes as an unsigned 16-bit integer in little-endian format. */ + @JvmStatic + fun uint16ToByteArrayLE(value: Int, out: ByteArray, offset: Int) { + out[offset] = (value and 0xFF).toByte() + out[offset + 1] = ((value shr 8) and 0xFF).toByte() + } + + /** Writes 4 bytes as an unsigned 32-bit integer in little-endian format. */ + @JvmStatic + fun uint32ToByteArrayLE(value: Long, out: ByteArray, offset: Int) { + out[offset] = (value and 0xFF).toByte() + out[offset + 1] = ((value shr 8) and 0xFF).toByte() + out[offset + 2] = ((value shr 16) and 0xFF).toByte() + out[offset + 3] = ((value shr 24) and 0xFF).toByte() + } + + /** Writes 4 bytes as an unsigned 32-bit integer in big-endian format. */ + @JvmStatic + fun uint32ToByteArrayBE(value: Long, out: ByteArray, offset: Int) { + out[offset] = ((value shr 24) and 0xFF).toByte() + out[offset + 1] = ((value shr 16) and 0xFF).toByte() + out[offset + 2] = ((value shr 8) and 0xFF).toByte() + out[offset + 3] = (value and 0xFF).toByte() + } + + /** Reads an unsigned 16-bit little-endian integer from [bytes] at [offset]. */ + @JvmStatic + fun readUint16(bytes: ByteArray, offset: Int): Int { + return (bytes[offset].toInt() and 0xFF) or + ((bytes[offset + 1].toInt() and 0xFF) shl 8) + } + + /** Reads an unsigned 32-bit little-endian integer from [bytes] at [offset]. */ + @JvmStatic + fun readUint32(bytes: ByteArray, offset: Int): Long { + return (bytes[offset].toLong() and 0xFF) or + ((bytes[offset + 1].toLong() and 0xFF) shl 8) or + ((bytes[offset + 2].toLong() and 0xFF) shl 16) or + ((bytes[offset + 3].toLong() and 0xFF) shl 24) + } + + /** Reads an unsigned 32-bit big-endian integer from [bytes] at [offset]. */ + @JvmStatic + fun readUint32BE(bytes: ByteArray, offset: Int): Long { + return ((bytes[offset].toLong() and 0xFF) shl 24) or + ((bytes[offset + 1].toLong() and 0xFF) shl 16) or + ((bytes[offset + 2].toLong() and 0xFF) shl 8) or + (bytes[offset + 3].toLong() and 0xFF) + } + + /** Reads a signed 64-bit little-endian integer from [bytes] at [offset]. */ + @JvmStatic + fun readInt64(bytes: ByteArray, offset: Int): Long { + return (bytes[offset].toLong() and 0xFF) or + ((bytes[offset + 1].toLong() and 0xFF) shl 8) or + ((bytes[offset + 2].toLong() and 0xFF) shl 16) or + ((bytes[offset + 3].toLong() and 0xFF) shl 24) or + ((bytes[offset + 4].toLong() and 0xFF) shl 32) or + ((bytes[offset + 5].toLong() and 0xFF) shl 40) or + ((bytes[offset + 6].toLong() and 0xFF) shl 48) or + ((bytes[offset + 7].toLong() and 0xFF) shl 56) + } + + /** Writes a signed 64-bit integer to [out] at [offset] in little-endian format. */ + @JvmStatic + fun int64ToByteArrayLE(value: Long, out: ByteArray, offset: Int) { + out[offset] = (value and 0xFF).toByte() + out[offset + 1] = ((value shr 8) and 0xFF).toByte() + out[offset + 2] = ((value shr 16) and 0xFF).toByte() + out[offset + 3] = ((value shr 24) and 0xFF).toByte() + out[offset + 4] = ((value shr 32) and 0xFF).toByte() + out[offset + 5] = ((value shr 40) and 0xFF).toByte() + out[offset + 6] = ((value shr 48) and 0xFF).toByte() + out[offset + 7] = ((value shr 56) and 0xFF).toByte() + } + + /** Writes a uint16 LE integer to an [OutputStream]. */ + @JvmStatic + @Throws(IOException::class) + fun uint16ToByteStreamLE(value: Int, stream: OutputStream) { + stream.write((value and 0xFF)) + stream.write(((value shr 8) and 0xFF)) + } + + /** Writes a uint32 LE integer to an [OutputStream]. */ + @JvmStatic + @Throws(IOException::class) + fun uint32ToByteStreamLE(value: Long, stream: OutputStream) { + stream.write((value and 0xFF).toInt()) + stream.write(((value shr 8) and 0xFF).toInt()) + stream.write(((value shr 16) and 0xFF).toInt()) + stream.write(((value shr 24) and 0xFF).toInt()) + } + + /** Writes a signed 64-bit LE integer to an [OutputStream]. */ + @JvmStatic + @Throws(IOException::class) + fun int64ToByteStreamLE(value: Long, stream: OutputStream) { + stream.write((value and 0xFF).toInt()) + stream.write(((value shr 8) and 0xFF).toInt()) + stream.write(((value shr 16) and 0xFF).toInt()) + stream.write(((value shr 24) and 0xFF).toInt()) + stream.write(((value shr 32) and 0xFF).toInt()) + stream.write(((value shr 40) and 0xFF).toInt()) + stream.write(((value shr 48) and 0xFF).toInt()) + stream.write(((value shr 56) and 0xFF).toInt()) + } + + /** Reads bytes from [stream] until [count] bytes have been read or an EOF occurs. */ + @JvmStatic + @Throws(IOException::class) + fun readBytesFromStream(stream: InputStream, count: Int): ByteArray { + val buffer = ByteArray(minOf(count, MAX_INITIAL_ARRAY_LENGTH)) + var offset = 0 + var remaining = count + while (remaining > 0) { + val read = stream.read(buffer, offset, remaining) + if (read < 0) throw IOException("Tried to read $count bytes, but only $offset bytes available before EOF") + remaining -= read + offset += read + } + return buffer + } + + /** Reverses the content of the given byte array. */ + @JvmStatic + fun reverseBytes(bytes: ByteArray): ByteArray { + val buf = ByteArray(bytes.size) + for (i in bytes.indices) { + buf[i] = bytes[bytes.size - 1 - i] + } + return buf + } + + /** + * Decodes an MPI-encoded number. MPI encoded numbers consist of a 4-byte big-endian length field + * followed by the number in big-endian format with a sign bit. + * @param hasLength if false, the 4-byte length prefix is absent + */ + @JvmStatic + fun decodeMPI(mpi: ByteArray, hasLength: Boolean): BigInteger { + val buf: ByteArray = if (hasLength) { + val length = readUint32BE(mpi, 0).toInt() + mpi.copyOfRange(4, 4 + length) + } else { + mpi + } + if (buf.isEmpty()) return BigInteger.ZERO + val isNegative = (buf[0].toInt() and 0x80) == 0x80 + if (isNegative) buf[0] = (buf[0].toInt() and 0x7F).toByte() + val result = BigInteger(buf) + return if (isNegative) result.negate() else result + } + + /** + * Encodes a BigInteger in MPI format. MPI encoded numbers consist of a 4-byte big-endian + * length field followed by the number in big-endian format with a sign bit. + * @param includeLength if true, prepends the 4-byte length field + */ + @JvmStatic + fun encodeMPI(value: BigInteger, includeLength: Boolean): ByteArray { + if (value == BigInteger.ZERO) { + return if (!includeLength) ByteArray(0) else byteArrayOf(0, 0, 0, 0) + } + val isNegative = value.signum() < 0 + var array = if (isNegative) value.negate().toByteArray() else value.toByteArray() + var length = array.size + if ((array[0].toInt() and 0x80) == 0x80) length++ + return if (includeLength) { + val result = ByteArray(length + 4) + System.arraycopy(array, 0, result, length - array.size + 3, array.size) + uint32ToByteArrayBE(length.toLong(), result, 0) + if (isNegative) result[4] = (result[4].toInt() or 0x80).toByte() + result + } else { + if (length != array.size) { + val result = ByteArray(length) + System.arraycopy(array, 0, result, 1, array.size) + array = result + } + if (isNegative) array[0] = (array[0].toInt() or 0x80).toByte() + array + } + } + + /** + * Decodes the Bitcoin "compact bits" difficulty encoding. + * @see encodeCompactBits + */ + @JvmStatic + fun decodeCompactBits(compact: Long): BigInteger { + val size = ((compact shr 24) and 0xFF).toInt() + val bytes = ByteArray(4 + size) + bytes[3] = size.toByte() + if (size >= 1) bytes[4] = ((compact shr 16) and 0xFF).toByte() + if (size >= 2) bytes[5] = ((compact shr 8) and 0xFF).toByte() + if (size >= 3) bytes[6] = (compact and 0xFF).toByte() + return decodeMPI(bytes, true) + } + + /** + * Encodes a BigInteger in Bitcoin's "compact bits" difficulty format. + * @see decodeCompactBits + */ + @JvmStatic + fun encodeCompactBits(value: BigInteger): Long { + var result: Long + var size = value.toByteArray().size + result = if (size <= 3) value.toLong() shl (8 * (3 - size)) + else value.shiftRight(8 * (size - 3)).toLong() + if ((result and 0x00800000L) != 0L) { + result = result shr 8 + size++ + } + result = result or (size.toLong() shl 24) + result = result or (if (value.signum() == -1) 0x00800000L else 0L) + return result + } + + /** If non-null, overrides the return value of [now]. */ + @Volatile + private var mockTime: Date? = null + + /** Advances (or rewinds) the mock clock by the given number of seconds. */ + @JvmStatic + fun rollMockClock(seconds: Int): Date = rollMockClockMillis(seconds * 1000L) + + /** Advances (or rewinds) the mock clock by the given number of milliseconds. */ + @JvmStatic + fun rollMockClockMillis(millis: Long): Date { + val current = mockTime ?: throw IllegalStateException("You need to use setMockClock() first.") + return Date(current.time + millis).also { mockTime = it } + } + + /** Sets the mock clock to the current time. */ + @JvmStatic + fun setMockClock() { + mockTime = Date() + } + + /** Sets the mock clock to the given time (in seconds since epoch). */ + @JvmStatic + fun setMockClock(mockClockSeconds: Long) { + mockTime = Date(mockClockSeconds * 1000) + } + + /** Clears the mock clock. */ + @JvmStatic + fun resetMocking() { + mockTime = null + } + + /** Returns the current time, or a mocked equivalent. */ + @JvmStatic + fun now(): Date = mockTime ?: Date() + + /** Returns the current time in milliseconds since the epoch, or a mocked equivalent. */ + @JvmStatic + fun currentTimeMillis(): Long = mockTime?.time ?: System.currentTimeMillis() + + /** Returns the current time in seconds since the epoch, or a mocked equivalent. */ + @JvmStatic + fun currentTimeSeconds(): Long = currentTimeMillis() / 1000 + + private val UTC = TimeZone.getTimeZone("UTC") + + /** Formats a date to an ISO 8601 string. */ + @JvmStatic + fun dateTimeFormat(dateTime: Date): String { + val iso8601 = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + iso8601.timeZone = UTC + return iso8601.format(dateTime) + } + + /** Formats a unix time in milliseconds to an ISO 8601 string. */ + @JvmStatic + fun dateTimeFormat(dateTime: Long): String { + val iso8601 = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + iso8601.timeZone = UTC + return iso8601.format(dateTime) + } + + private val bitMask = intArrayOf(0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80) + + /** Checks if the given bit is set in data using little-endian bit order. */ + @JvmStatic + fun checkBitLE(data: ByteArray, index: Int): Boolean = + (data[index ushr 3].toInt() and bitMask[7 and index]) != 0 + + /** Sets the given bit in data to 1 using little-endian bit order. */ + @JvmStatic + fun setBitLE(data: ByteArray, index: Int) { + data[index ushr 3] = (data[index ushr 3].toInt() or bitMask[7 and index]).toByte() + } + + private enum class Runtime { ANDROID, OPENJDK, ORACLE_JAVA } + private enum class OS { LINUX, WINDOWS, MAC_OS } + + private val runtime: Runtime? + private val os: OS? + + init { + val runtimeProp = (System.getProperty("java.runtime.name") ?: "").lowercase(Locale.US) + runtime = when { + runtimeProp.isEmpty() -> null + runtimeProp.contains("android") -> Runtime.ANDROID + runtimeProp.contains("openjdk") -> Runtime.OPENJDK + runtimeProp.contains("java(tm) se") -> Runtime.ORACLE_JAVA + else -> { Timber.i("Unknown java.runtime.name '$runtimeProp'"); null } + } + val osProp = (System.getProperty("os.name") ?: "").lowercase(Locale.US) + os = when { + osProp.isEmpty() -> null + osProp.contains("linux") -> OS.LINUX + osProp.contains("win") -> OS.WINDOWS + osProp.contains("mac") -> OS.MAC_OS + else -> { Timber.i("Unknown os.name '$osProp'"); null } + } + } + + @JvmStatic fun isAndroidRuntime(): Boolean = runtime == Runtime.ANDROID + @JvmStatic fun isOpenJDKRuntime(): Boolean = runtime == Runtime.OPENJDK + @JvmStatic fun isOracleJavaRuntime(): Boolean = runtime == Runtime.ORACLE_JAVA + @JvmStatic fun isLinux(): Boolean = os == OS.LINUX + @JvmStatic fun isWindows(): Boolean = os == OS.WINDOWS + @JvmStatic fun isMac(): Boolean = os == OS.MAC_OS + + /** Encodes a stack of byte arrays as a space-separated hex string surrounded by brackets. */ + @JvmStatic + fun toString(stack: List): String = + stack.joinToString(" ") { "[${HEX.encode(it)}]" } +} diff --git a/libs/encryption/utils/src/main/kotlin/com/getcode/utils/Extensions.kt b/libs/encryption/utils/src/commonMain/kotlin/com/getcode/utils/Extensions.kt similarity index 53% rename from libs/encryption/utils/src/main/kotlin/com/getcode/utils/Extensions.kt rename to libs/encryption/utils/src/commonMain/kotlin/com/getcode/utils/Extensions.kt index fd4ab24fd0..3b016d91f9 100644 --- a/libs/encryption/utils/src/main/kotlin/com/getcode/utils/Extensions.kt +++ b/libs/encryption/utils/src/commonMain/kotlin/com/getcode/utils/Extensions.kt @@ -1,71 +1,11 @@ package com.getcode.utils -import android.util.Base64 -import com.getcode.ed25519.Ed25519 import com.getcode.vendor.Base58 -import com.google.protobuf.ByteString -import java.net.URLDecoder -import java.net.URLEncoder -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException - -fun List.toByteString(): ByteString = ByteString.copyFrom(this.toByteArray()) -fun ByteArray.toByteString(): ByteString = ByteString.copyFrom(this) - - -val List.base58: String - get() = Base58.encode(toByteArray()) - -val ByteArray.base58: String - get() = Base58.encode(this) - -val List.base64: String - get() = Base64.encodeToString(toByteArray(), Base64.NO_WRAP) - -fun String.decodeBase58(): ByteArray { - return Base58.decode(this) -} - -fun String.decodeBase64(): ByteArray { - return Base64.decode(this, Base64.NO_WRAP) -} - -fun String.decodeBase64UrlSafe(): ByteArray { - return Base64.decode(this, Base64.NO_WRAP or Base64.URL_SAFE) -} - -fun ByteArray.decodeBase64(): ByteArray { - return Base64.decode(this, Base64.NO_WRAP) -} - -val ByteArray.base64: String - get() = Base64.encodeToString(this, Base64.NO_WRAP) - -fun ByteArray.encodeBase64(urlSafe: Boolean = false): String { - val flags = if (urlSafe) Base64.NO_WRAP or Base64.URL_SAFE else Base64.NO_WRAP - return Base64.encodeToString(this, flags) -} - -fun ByteArray.encodeBase64ToArray(): ByteArray { - return Base64.encode(this, Base64.NO_WRAP) -} - -fun ByteArray.sha512(): ByteArray { - return try { - MessageDigest.getInstance("SHA-512") - .apply { update(this@sha512) } - .digest() - - } catch (e: NoSuchAlgorithmException) { - throw RuntimeException("SHA-512 not implemented") - } -} - -fun List.toByteList(): List { - return this.map { it.toByte() } -} +/// Converts a list of integers to a list of bytes by truncating each value to a single byte. +fun List.toByteList(): List = map { it.toByte() } +/// Encodes this integer as a 4-byte little-endian byte array. fun Int.intToByteArray(): ByteArray = byteArrayOf( this.toByte(), @@ -74,10 +14,11 @@ fun Int.intToByteArray(): ByteArray = (this ushr 24).toByte() ) +/// Returns this integer as a little-endian byte list. val Int.bytes: List get() = intToByteArray().toList() - +/// Encodes this long as an 8-byte little-endian byte array. fun Long.toByteArray(): ByteArray = byteArrayOf( this.toByte(), @@ -90,30 +31,14 @@ fun Long.toByteArray(): ByteArray = (this ushr 56).toByte() ) +/// Returns this long as a little-endian byte list. val Long.bytes: List get() = toByteArray().toList() -fun String.urlEncode(): String { - return URLEncoder.encode(this, "UTF-8") -} - -fun String.urlDecode(): String { - return URLDecoder.decode(this, "UTF-8") -} - -fun String.replaceParam(vararg value: String?): String { - var result = this - value.forEachIndexed { index, s -> - result = result.replaceParam(index, s) - } - return result -} - -fun String.replaceParam(index: Int = 0, value: String?): String { - val param = "%${index + 1}\$s" - return this.replace(param, value.orEmpty()) -} +/// Alias for [Long.toByteArray]; encodes this long as an 8-byte little-endian byte array. +fun Long.longToByteArray(): ByteArray = toByteArray() +/// Decodes a little-endian byte array into a long value. fun ByteArray.byteArrayToLong(): Long { var result = 0L for (i in (size - 1) downTo 0) { @@ -122,6 +47,7 @@ fun ByteArray.byteArrayToLong(): Long { return result } +/// Decodes a little-endian byte array into an int value. fun ByteArray.byteArrayToInt(): Int { var result = 0 for (i in (size - 1) downTo 0) { @@ -130,18 +56,31 @@ fun ByteArray.byteArrayToInt(): Int { return result } -fun Long.longToByteArray(): ByteArray = toByteArray() - +/// Returns a sub-array of [count] bytes starting at [start]. fun ByteArray.subByteArray(start: Int, count: Int): ByteArray = copyOfRange(start, start + count) +/// Shifts this byte left by [bitCount] bits, truncating to a byte. infix fun Byte.shl(bitCount: Int): Byte = (toInt() shl bitCount).toByte() -fun String.toUTF8Bytes(): ByteArray = toByteArray(Charsets.UTF_8) +/// Returns this string encoded as UTF-8 bytes. +fun String.toUTF8Bytes(): ByteArray = encodeToByteArray() + +/// Substitutes indexed positional params (`%1$s`, `%2$s`, …) in this string with the given values. +fun String.replaceParam(vararg value: String?): String { + var result = this + value.forEachIndexed { index, s -> + result = result.replaceParam(index, s) + } + return result +} -fun Ed25519.KeyPair.getPublicKeyBase58(): String { - return Base58.encode(publicKeyBytes) +/// Substitutes the positional param at [index] (`%{index+1}$s`) with [value]. +fun String.replaceParam(index: Int = 0, value: String?): String { + val param = "%${index + 1}\$s" + return this.replace(param, value.orEmpty()) } +/// Returns a lowercase hex string for these bytes, or uppercase if [HexEncodingOptions.Uppercase] is given. fun List.hexEncodedString(options: Set = emptySet()): String { val hexDigits = if (options.contains(HexEncodingOptions.Uppercase)) "0123456789ABCDEF" @@ -156,10 +95,21 @@ fun List.hexEncodedString(options: Set = emptySet()): chars[index++] = hexDigits[byte.toInt() and 0xF] } - return String(chars) + return chars.concatToString() } +/// Options for [hexEncodedString]. sealed interface HexEncodingOptions { - data object Uppercase: HexEncodingOptions + data object Uppercase : HexEncodingOptions } +/// Returns the Base58-encoded string for this byte list. +val List.base58: String + get() = Base58.encode(toByteArray()) + +/// Returns the Base58-encoded string for this byte array. +val ByteArray.base58: String + get() = Base58.encode(this) + +/// Decodes this Base58-encoded string into bytes. +fun String.decodeBase58(): ByteArray = Base58.decode(this) diff --git a/libs/encryption/utils/src/test/kotlin/com/getcode/utils/ExtensionsTest.kt b/libs/encryption/utils/src/commonTest/kotlin/com/getcode/utils/ExtensionsTest.kt similarity index 92% rename from libs/encryption/utils/src/test/kotlin/com/getcode/utils/ExtensionsTest.kt rename to libs/encryption/utils/src/commonTest/kotlin/com/getcode/utils/ExtensionsTest.kt index 77e187f71b..42e93a5185 100644 --- a/libs/encryption/utils/src/test/kotlin/com/getcode/utils/ExtensionsTest.kt +++ b/libs/encryption/utils/src/commonTest/kotlin/com/getcode/utils/ExtensionsTest.kt @@ -120,19 +120,6 @@ class ExtensionsTest { assertEquals(4.toByte(), b shl 2) } - // --- String.urlEncode / urlDecode --- - - @Test - fun urlEncodeDecodeRoundtrip() { - val original = "hello world & foo=bar" - assertEquals(original, original.urlEncode().urlDecode()) - } - - @Test - fun urlEncodeSpaces() { - assertEquals("hello+world", "hello world".urlEncode()) - } - // --- String.replaceParam --- @Test diff --git a/libs/encryption/utils/src/main/java/com/getcode/utils/Utils.java b/libs/encryption/utils/src/main/java/com/getcode/utils/Utils.java deleted file mode 100644 index 8cca0226ad..0000000000 --- a/libs/encryption/utils/src/main/java/com/getcode/utils/Utils.java +++ /dev/null @@ -1,555 +0,0 @@ -package com.getcode.utils; - -/* - * Copyright 2011 Google Inc. - * Copyright 2014 Andreas Schildbach - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.math.BigInteger; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.TimeZone; -import java.util.regex.Pattern; - -import timber.log.Timber; - -/** - * A collection of various utility methods that are helpful for working with the Bitcoin protocol. - * To enable debug logging from the library, run with -Dbitcoinj.logging=true on your command line. - */ -public class Utils { - public static final String TAG = "Api Utils"; - - /** Hex encoder for use throughout the framework. */ - public static final HexEncoder HEX = new HexEncoder(); - - public static class HexEncoder { - private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray(); - - public String encode(byte[] data) { - char[] result = new char[data.length * 2]; - for (int i = 0; i < data.length; i++) { - result[i * 2] = HEX_CHARS[(data[i] >> 4) & 0xF]; - result[i * 2 + 1] = HEX_CHARS[data[i] & 0xF]; - } - return new String(result); - } - } - - /** Joins strings with a space. */ - public static String spaceJoin(Iterable parts) { - StringBuilder sb = new StringBuilder(); - boolean first = true; - for (Object part : parts) { - if (!first) sb.append(' '); - sb.append(part); - first = false; - } - return sb.toString(); - } - - /** - * Max initial size of variable length arrays and ArrayLists that could be attacked. - * Avoids this attack: Attacker sends a msg indicating it will contain a huge number (eg 2 billion) elements (eg transaction inputs) and - * forces bitcoinj to try to allocate a huge piece of the memory resulting in OutOfMemoryError. - */ - public static final int MAX_INITIAL_ARRAY_LENGTH = 20; - - /** - *

- * The regular {@link BigInteger#toByteArray()} includes the sign bit of the number and - * might result in an extra byte addition. This method removes this extra byte. - *

- *

- * Assuming only positive numbers, it's possible to discriminate if an extra byte - * is added by checking if the first element of the array is 0 (0000_0000). - * Due to the minimal representation provided by BigInteger, it means that the bit sign - * is the least significant bit 0000_0000 . - * Otherwise the representation is not minimal. - * For example, if the sign bit is 0000_0000, then the representation is not minimal due to the rightmost zero. - *

- * This is the antagonist to {@link #bytesToBigInteger(byte[])}. - * @param b the integer to format into a byte array - * @param numBytes the desired size of the resulting byte array - * @return numBytes byte long array. - */ - public static byte[] bigIntegerToBytes(BigInteger b, int numBytes) { - if (b.signum() < 0) throw new IllegalArgumentException("b must be positive or zero"); - if (numBytes <= 0) throw new IllegalArgumentException("numBytes must be positive"); - byte[] src = b.toByteArray(); - byte[] dest = new byte[numBytes]; - boolean isFirstByteOnlyForSign = src[0] == 0; - int length = isFirstByteOnlyForSign ? src.length - 1 : src.length; - if (length > numBytes) throw new IllegalArgumentException("The given number does not fit in " + numBytes); - int srcPos = isFirstByteOnlyForSign ? 1 : 0; - int destPos = numBytes - length; - System.arraycopy(src, srcPos, dest, destPos, length); - return dest; - } - - /** - * Converts an array of bytes into a positive BigInteger. This is the antagonist to - * {@link #bigIntegerToBytes(BigInteger, int)}. - * - * @param bytes to convert into a BigInteger - * @return the converted BigInteger - */ - public static BigInteger bytesToBigInteger(byte[] bytes) { - return new BigInteger(1, bytes); - } - - /** Write 2 bytes to the byte array (starting at the offset) as unsigned 16-bit integer in little endian format. */ - public static void uint16ToByteArrayLE(int val, byte[] out, int offset) { - out[offset] = (byte) (0xFF & val); - out[offset + 1] = (byte) (0xFF & (val >> 8)); - } - - /** Write 4 bytes to the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */ - public static void uint32ToByteArrayLE(long val, byte[] out, int offset) { - out[offset] = (byte) (0xFF & val); - out[offset + 1] = (byte) (0xFF & (val >> 8)); - out[offset + 2] = (byte) (0xFF & (val >> 16)); - out[offset + 3] = (byte) (0xFF & (val >> 24)); - } - - /** Write 4 bytes to the byte array (starting at the offset) as unsigned 32-bit integer in big endian format. */ - public static void uint32ToByteArrayBE(long val, byte[] out, int offset) { - out[offset] = (byte) (0xFF & (val >> 24)); - out[offset + 1] = (byte) (0xFF & (val >> 16)); - out[offset + 2] = (byte) (0xFF & (val >> 8)); - out[offset + 3] = (byte) (0xFF & val); - } - - /** Write 8 bytes to the byte array (starting at the offset) as signed 64-bit integer in little endian format. */ - public static void int64ToByteArrayLE(long val, byte[] out, int offset) { - out[offset] = (byte) (0xFF & val); - out[offset + 1] = (byte) (0xFF & (val >> 8)); - out[offset + 2] = (byte) (0xFF & (val >> 16)); - out[offset + 3] = (byte) (0xFF & (val >> 24)); - out[offset + 4] = (byte) (0xFF & (val >> 32)); - out[offset + 5] = (byte) (0xFF & (val >> 40)); - out[offset + 6] = (byte) (0xFF & (val >> 48)); - out[offset + 7] = (byte) (0xFF & (val >> 56)); - } - - /** Write 2 bytes to the output stream as unsigned 16-bit integer in little endian format. */ - public static void uint16ToByteStreamLE(int val, OutputStream stream) throws IOException { - stream.write((int) (0xFF & val)); - stream.write((int) (0xFF & (val >> 8))); - } - - /** Write 2 bytes to the output stream as unsigned 16-bit integer in big endian format. */ - public static void uint16ToByteStreamBE(int val, OutputStream stream) throws IOException { - stream.write((int) (0xFF & (val >> 8))); - stream.write((int) (0xFF & val)); - } - - /** Write 4 bytes to the output stream as unsigned 32-bit integer in little endian format. */ - public static void uint32ToByteStreamLE(long val, OutputStream stream) throws IOException { - stream.write((int) (0xFF & val)); - stream.write((int) (0xFF & (val >> 8))); - stream.write((int) (0xFF & (val >> 16))); - stream.write((int) (0xFF & (val >> 24))); - } - - /** Write 4 bytes to the output stream as unsigned 32-bit integer in big endian format. */ - public static void uint32ToByteStreamBE(long val, OutputStream stream) throws IOException { - stream.write((int) (0xFF & (val >> 24))); - stream.write((int) (0xFF & (val >> 16))); - stream.write((int) (0xFF & (val >> 8))); - stream.write((int) (0xFF & val)); - } - - /** Write 8 bytes to the output stream as signed 64-bit integer in little endian format. */ - public static void int64ToByteStreamLE(long val, OutputStream stream) throws IOException { - stream.write((int) (0xFF & val)); - stream.write((int) (0xFF & (val >> 8))); - stream.write((int) (0xFF & (val >> 16))); - stream.write((int) (0xFF & (val >> 24))); - stream.write((int) (0xFF & (val >> 32))); - stream.write((int) (0xFF & (val >> 40))); - stream.write((int) (0xFF & (val >> 48))); - stream.write((int) (0xFF & (val >> 56))); - } - - /** Write 8 bytes to the output stream as unsigned 64-bit integer in little endian format. */ - public static void uint64ToByteStreamLE(BigInteger val, OutputStream stream) throws IOException { - byte[] bytes = val.toByteArray(); - if (bytes.length > 8) { - throw new RuntimeException("Input too large to encode into a uint64"); - } - bytes = reverseBytes(bytes); - stream.write(bytes); - if (bytes.length < 8) { - for (int i = 0; i < 8 - bytes.length; i++) - stream.write(0); - } - } - - /** Parse 2 bytes from the byte array (starting at the offset) as unsigned 16-bit integer in little endian format. */ - public static int readUint16(byte[] bytes, int offset) { - return (bytes[offset] & 0xff) | - ((bytes[offset + 1] & 0xff) << 8); - } - - /** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in little endian format. */ - public static long readUint32(byte[] bytes, int offset) { - return (bytes[offset] & 0xffl) | - ((bytes[offset + 1] & 0xffl) << 8) | - ((bytes[offset + 2] & 0xffl) << 16) | - ((bytes[offset + 3] & 0xffl) << 24); - } - - /** Parse 8 bytes from the byte array (starting at the offset) as signed 64-bit integer in little endian format. */ - public static long readInt64(byte[] bytes, int offset) { - return (bytes[offset] & 0xffl) | - ((bytes[offset + 1] & 0xffl) << 8) | - ((bytes[offset + 2] & 0xffl) << 16) | - ((bytes[offset + 3] & 0xffl) << 24) | - ((bytes[offset + 4] & 0xffl) << 32) | - ((bytes[offset + 5] & 0xffl) << 40) | - ((bytes[offset + 6] & 0xffl) << 48) | - ((bytes[offset + 7] & 0xffl) << 56); - } - - /** Parse 4 bytes from the byte array (starting at the offset) as unsigned 32-bit integer in big endian format. */ - public static long readUint32BE(byte[] bytes, int offset) { - return ((bytes[offset] & 0xffl) << 24) | - ((bytes[offset + 1] & 0xffl) << 16) | - ((bytes[offset + 2] & 0xffl) << 8) | - (bytes[offset + 3] & 0xffl); - } - - /** Parse 2 bytes from the byte array (starting at the offset) as unsigned 16-bit integer in big endian format. */ - public static int readUint16BE(byte[] bytes, int offset) { - return ((bytes[offset] & 0xff) << 8) | - (bytes[offset + 1] & 0xff); - } - - /** Parse 2 bytes from the stream as unsigned 16-bit integer in little endian format. */ - public static int readUint16FromStream(InputStream is) { - try { - return (is.read() & 0xff) | - ((is.read() & 0xff) << 8); - } catch (IOException x) { - throw new RuntimeException(x); - } - } - - /** Parse 4 bytes from the stream as unsigned 32-bit integer in little endian format. */ - public static long readUint32FromStream(InputStream is) { - try { - return (is.read() & 0xffl) | - ((is.read() & 0xffl) << 8) | - ((is.read() & 0xffl) << 16) | - ((is.read() & 0xffl) << 24); - } catch (IOException x) { - throw new RuntimeException(x); - } - } - - /** - * Returns a copy of the given byte array in reverse order. - */ - public static byte[] reverseBytes(byte[] bytes) { - // We could use the XOR trick here but it's easier to understand if we don't. If we find this is really a - // performance issue the matter can be revisited. - byte[] buf = new byte[bytes.length]; - for (int i = 0; i < bytes.length; i++) - buf[i] = bytes[bytes.length - 1 - i]; - return buf; - } - - /** - * MPI encoded numbers are produced by the OpenSSL BN_bn2mpi function. They consist of - * a 4 byte big endian length field, followed by the stated number of bytes representing - * the number in big endian format (with a sign bit). - * @param hasLength can be set to false if the given array is missing the 4 byte length field - */ - public static BigInteger decodeMPI(byte[] mpi, boolean hasLength) { - byte[] buf; - if (hasLength) { - int length = (int) readUint32BE(mpi, 0); - buf = new byte[length]; - System.arraycopy(mpi, 4, buf, 0, length); - } else - buf = mpi; - if (buf.length == 0) - return BigInteger.ZERO; - boolean isNegative = (buf[0] & 0x80) == 0x80; - if (isNegative) - buf[0] &= 0x7f; - BigInteger result = new BigInteger(buf); - return isNegative ? result.negate() : result; - } - - /** - * MPI encoded numbers are produced by the OpenSSL BN_bn2mpi function. They consist of - * a 4 byte big endian length field, followed by the stated number of bytes representing - * the number in big endian format (with a sign bit). - * @param includeLength indicates whether the 4 byte length field should be included - */ - public static byte[] encodeMPI(BigInteger value, boolean includeLength) { - if (value.equals(BigInteger.ZERO)) { - if (!includeLength) - return new byte[] {}; - else - return new byte[] {0x00, 0x00, 0x00, 0x00}; - } - boolean isNegative = value.signum() < 0; - if (isNegative) - value = value.negate(); - byte[] array = value.toByteArray(); - int length = array.length; - if ((array[0] & 0x80) == 0x80) - length++; - if (includeLength) { - byte[] result = new byte[length + 4]; - System.arraycopy(array, 0, result, length - array.length + 3, array.length); - uint32ToByteArrayBE(length, result, 0); - if (isNegative) - result[4] |= 0x80; - return result; - } else { - byte[] result; - if (length != array.length) { - result = new byte[length]; - System.arraycopy(array, 0, result, 1, array.length); - }else - result = array; - if (isNegative) - result[0] |= 0x80; - return result; - } - } - - /** - *

The "compact" format is a representation of a whole number N using an unsigned 32 bit number similar to a - * floating point format. The most significant 8 bits are the unsigned exponent of base 256. This exponent can - * be thought of as "number of bytes of N". The lower 23 bits are the mantissa. Bit number 24 (0x800000) represents - * the sign of N. Therefore, N = (-1^sign) * mantissa * 256^(exponent-3).

- * - *

Satoshi's original implementation used BN_bn2mpi() and BN_mpi2bn(). MPI uses the most significant bit of the - * first byte as sign. Thus 0x1234560000 is compact 0x05123456 and 0xc0de000000 is compact 0x0600c0de. Compact - * 0x05c0de00 would be -0x40de000000.

- * - *

Bitcoin only uses this "compact" format for encoding difficulty targets, which are unsigned 256bit quantities. - * Thus, all the complexities of the sign bit and using base 256 are probably an implementation accident.

- */ - public static BigInteger decodeCompactBits(long compact) { - int size = ((int) (compact >> 24)) & 0xFF; - byte[] bytes = new byte[4 + size]; - bytes[3] = (byte) size; - if (size >= 1) bytes[4] = (byte) ((compact >> 16) & 0xFF); - if (size >= 2) bytes[5] = (byte) ((compact >> 8) & 0xFF); - if (size >= 3) bytes[6] = (byte) (compact & 0xFF); - return decodeMPI(bytes, true); - } - - /** - * @see Utils#decodeCompactBits(long) - */ - public static long encodeCompactBits(BigInteger value) { - long result; - int size = value.toByteArray().length; - if (size <= 3) - result = value.longValue() << 8 * (3 - size); - else - result = value.shiftRight(8 * (size - 3)).longValue(); - // The 0x00800000 bit denotes the sign. - // Thus, if it is already set, divide the mantissa by 256 and increase the exponent. - if ((result & 0x00800000L) != 0) { - result >>= 8; - size++; - } - result |= size << 24; - result |= value.signum() == -1 ? 0x00800000 : 0; - return result; - } - - /** - * If non-null, overrides the return value of now(). - */ - private static volatile Date mockTime; - - /** - * Advances (or rewinds) the mock clock by the given number of seconds. - */ - public static Date rollMockClock(int seconds) { - return rollMockClockMillis(seconds * 1000); - } - - /** - * Advances (or rewinds) the mock clock by the given number of milliseconds. - */ - public static Date rollMockClockMillis(long millis) { - if (mockTime == null) - throw new IllegalStateException("You need to use setMockClock() first."); - mockTime = new Date(mockTime.getTime() + millis); - return mockTime; - } - - /** - * Sets the mock clock to the current time. - */ - public static void setMockClock() { - mockTime = new Date(); - } - - /** - * Sets the mock clock to the given time (in seconds). - */ - public static void setMockClock(long mockClockSeconds) { - mockTime = new Date(mockClockSeconds * 1000); - } - - /** - * Clears the mock clock and sleep - */ - public static void resetMocking() { - mockTime = null; - } - - /** - * Returns the current time, or a mocked out equivalent. - */ - public static Date now() { - return mockTime != null ? mockTime : new Date(); - } - - /** - * Returns the current time in milliseconds since the epoch, or a mocked out equivalent. - */ - public static long currentTimeMillis() { - return mockTime != null ? mockTime.getTime() : System.currentTimeMillis(); - } - - /** - * Returns the current time in seconds since the epoch, or a mocked out equivalent. - */ - public static long currentTimeSeconds() { - return currentTimeMillis() / 1000; - } - - private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); - - /** - * Formats a given date+time value to an ISO 8601 string. - * @param dateTime value to format, as a Date - */ - public static String dateTimeFormat(Date dateTime) { - DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - iso8601.setTimeZone(UTC); - return iso8601.format(dateTime); - } - - /** - * Formats a given date+time value to an ISO 8601 string. - * @param dateTime value to format, unix time (ms) - */ - public static String dateTimeFormat(long dateTime) { - DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - iso8601.setTimeZone(UTC); - return iso8601.format(dateTime); - } - - // 00000001, 00000010, 00000100, 00001000, ... - private static final int[] bitMask = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; - - /** Checks if the given bit is set in data, using little endian (not the same as Java native big endian) */ - public static boolean checkBitLE(byte[] data, int index) { - return (data[index >>> 3] & bitMask[7 & index]) != 0; - } - - /** Sets the given bit in data to one, using little endian (not the same as Java native big endian) */ - public static void setBitLE(byte[] data, int index) { - data[index >>> 3] |= bitMask[7 & index]; - } - - private enum Runtime { - ANDROID, OPENJDK, ORACLE_JAVA - } - - private enum OS { - LINUX, WINDOWS, MAC_OS - } - - private static Runtime runtime = null; - private static OS os = null; - static { - String runtimeProp = System.getProperty("java.runtime.name", "").toLowerCase(Locale.US); - if (runtimeProp.equals("")) - runtime = null; - else if (runtimeProp.contains("android")) - runtime = Runtime.ANDROID; - else if (runtimeProp.contains("openjdk")) - runtime = Runtime.OPENJDK; - else if (runtimeProp.contains("java(tm) se")) - runtime = Runtime.ORACLE_JAVA; - else - Timber.i("Unknown java.runtime.name '{}' " + runtimeProp); - - String osProp = System.getProperty("os.name", "").toLowerCase(Locale.US); - if (osProp.equals("")) - os = null; - else if (osProp.contains("linux")) - os = OS.LINUX; - else if (osProp.contains("win")) - os = OS.WINDOWS; - else if (osProp.contains("mac")) - os = OS.MAC_OS; - else - Timber.i("Unknown os.name '{}' %s", runtimeProp); - } - - public static boolean isAndroidRuntime() { - return runtime == Runtime.ANDROID; - } - - public static boolean isOpenJDKRuntime() { - return runtime == Runtime.OPENJDK; - } - - public static boolean isOracleJavaRuntime() { - return runtime == Runtime.ORACLE_JAVA; - } - - public static boolean isLinux() { - return os == OS.LINUX; - } - - public static boolean isWindows() { - return os == OS.WINDOWS; - } - - public static boolean isMac() { - return os == OS.MAC_OS; - } - - public static String toString(List stack) { - List parts = new ArrayList<>(stack.size()); - for (byte[] push : stack) - parts.add('[' + HEX.encode(push) + ']'); - return spaceJoin(parts); - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index fc23b06914..b2c186780f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -252,6 +252,7 @@ val kmpUnitTestModules = setOf( ":libs:encryption:sha256", ":libs:encryption:sha512", ":libs:encryption:hmac", + ":libs:encryption:utils", ) val noUnitTestModules = setOf(":apps:flipcash:benchmark", ":kmp:shared-core") val unitTestCandidates = includedProjectPaths.filter { path ->