diff --git a/src/dorkbox/bytes/Base58.kt b/src/dorkbox/bytes/Base58.kt new file mode 100644 index 0000000..08d15a6 --- /dev/null +++ b/src/dorkbox/bytes/Base58.kt @@ -0,0 +1,191 @@ +/* + * Copyright 2011 Google Inc. + * Copyright 2018 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. + * + * Converted to kotlin by ligi + * https://github.com/komputing/KBase58/blob/master/kbase58/src/main/kotlin/org/komputing/kbase58/Base58.kt + */ +package dorkbox.bytes + +/** + * Base58 is a way to encode Bitcoin addresses (or arbitrary data) as alphanumeric strings. + *

+ * Note that this is not the same base58 as used by Flickr, which you may find referenced around the Internet. + *

+ * You may want to consider working with {@link PrefixedChecksummedBytes} instead, which + * adds support for testing the prefix and suffix bytes commonly found in addresses. + *

+ * Satoshi explains: why base-58 instead of standard base-64 encoding? + *

+ *

+ * However, note that the encoding/decoding runs in O(n²) time, so it is not useful for large data. + *

+ * The basic idea of the encoding is to treat the data bytes as a large number represented using + * base-256 digits, convert the number to be represented using base-58 digits, preserve the exact + * number of leading zeros (which are otherwise lost during the mathematical operations on the + * numbers), and finally represent the resulting base-58 digits as alphanumeric ASCII characters. + */ + +private const val ENCODED_ZERO = '1' +private const val CHECKSUM_SIZE = 4 + +private const val alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +private val alphabetIndices by lazy { + IntArray(128) { alphabet.indexOf(it.toChar()) } +} + + +/** + * Divides a number, represented as an array of bytes each containing a single digit + * in the specified base, by the given divisor. The given number is modified in-place + * to contain the quotient, and the return value is the remainder. + * + * @param number the number to divide + * @param firstDigit the index within the array of the first non-zero digit + * (this is used for optimization by skipping the leading zeros) + * @param base the base in which the number's digits are represented (up to 256) + * @param divisor the number to divide by (up to 256) + * @return the remainder of the division operation + */ +private fun divmod(number: ByteArray, firstDigit: UInt, base: UInt, divisor: UInt): UInt { + // this is just long division which accounts for the base of the input digits + var remainder = 0.toUInt() + for (i in firstDigit until number.size.toUInt()) { + val digit = number[i.toInt()].toUByte() + val temp = remainder * base + digit + number[i.toInt()] = (temp / divisor).toByte() + remainder = temp % divisor + } + return remainder +} + + +/** + * Encodes the bytes as a base58 string (no checksum is appended). + * + * @return the base58-encoded string + */ +fun ByteArray.encodeToBase58String(): String { + + val input = copyOf(size) // since we modify it in-place + if (input.isEmpty()) { + return "" + } + // Count leading zeros. + var zeros = 0 + while (zeros < input.size && input[zeros].toInt() == 0) { + ++zeros + } + // Convert base-256 digits to base-58 digits (plus conversion to ASCII characters) + val encoded = CharArray(input.size * 2) // upper bound + var outputStart = encoded.size + var inputStart = zeros + while (inputStart < input.size) { + encoded[--outputStart] = alphabet[divmod(input, inputStart.toUInt(), 256.toUInt(), 58.toUInt()).toInt()] + if (input[inputStart].toInt() == 0) { + ++inputStart // optimization - skip leading zeros + } + } + // Preserve exactly as many leading encoded zeros in output as there were leading zeros in data. + while (outputStart < encoded.size && encoded[outputStart] == ENCODED_ZERO) { + ++outputStart + } + while (--zeros >= 0) { + encoded[--outputStart] = ENCODED_ZERO + } + // Return encoded string (including encoded leading zeros). + return String(encoded, outputStart, encoded.size - outputStart) +} + +/** + * Decodes the base58 string into a [ByteArray] + * + * @return the decoded data bytes + * @throws NumberFormatException if the string is not a valid base58 string + */ +@Throws(NumberFormatException::class) +fun String.decodeBase58(): ByteArray { + if (isEmpty()) { + return ByteArray(0) + } + // Convert the base58-encoded ASCII chars to a base58 byte sequence (base58 digits). + val input58 = ByteArray(length) + for (i in 0 until length) { + val c = this[i] + val digit = if (c.toInt() < 128) alphabetIndices[c.toInt()] else -1 + if (digit < 0) { + throw NumberFormatException("Illegal character $c at position $i") + } + input58[i] = digit.toByte() + } + // Count leading zeros. + var zeros = 0 + while (zeros < input58.size && input58[zeros].toInt() == 0) { + ++zeros + } + // Convert base-58 digits to base-256 digits. + val decoded = ByteArray(length) + var outputStart = decoded.size + var inputStart = zeros + while (inputStart < input58.size) { + decoded[--outputStart] = divmod(input58, inputStart.toUInt(), 58.toUInt(), 256.toUInt()).toByte() + if (input58[inputStart].toInt() == 0) { + ++inputStart // optimization - skip leading zeros + } + } + // Ignore extra leading zeroes that were added during the calculation. + while (outputStart < decoded.size && decoded[outputStart].toInt() == 0) { + ++outputStart + } + // Return decoded data (including original number of leading zeros). + return decoded.copyOfRange(outputStart - zeros, decoded.size) +} + +/** + * Encodes the given bytes as a base58 string, a checksum is appended + * + * @return the base58-encoded string +*/ +fun ByteArray.encodeToBase58WithChecksum() = ByteArray(size + CHECKSUM_SIZE).apply { + System.arraycopy(this@encodeToBase58WithChecksum, 0, this, 0, this@encodeToBase58WithChecksum.size) + val checksum = this@encodeToBase58WithChecksum.sha256().sha256() + System.arraycopy(checksum, 0, this, this@encodeToBase58WithChecksum.size, CHECKSUM_SIZE) + +}.encodeToBase58String() + +fun String.decodeBase58WithChecksum(): ByteArray { + val rawBytes = decodeBase58() + if (rawBytes.size < CHECKSUM_SIZE) { + throw Exception("Too short for checksum: $this l: ${rawBytes.size}") + } + val checksum = rawBytes.copyOfRange(rawBytes.size - CHECKSUM_SIZE, rawBytes.size) + + val payload = rawBytes.copyOfRange(0, rawBytes.size - CHECKSUM_SIZE) + + val hash = payload.sha256().sha256() + val computedChecksum = hash.copyOfRange(0, CHECKSUM_SIZE) + + if (checksum.contentEquals(computedChecksum)) { + return payload + } else { + throw IllegalArgumentException("Checksum mismatch: $checksum is not computed checksum $computedChecksum") + } +} diff --git a/src/dorkbox/bytes/HashExtensions.kt b/src/dorkbox/bytes/HashExtensions.kt new file mode 100644 index 0000000..017df25 --- /dev/null +++ b/src/dorkbox/bytes/HashExtensions.kt @@ -0,0 +1,38 @@ +package dorkbox.bytes + +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + + +private val digest1 = ThreadLocal.withInitial { + try { + return@withInitial MessageDigest.getInstance("SHA1") + } catch (e: NoSuchAlgorithmException) { + throw RuntimeException("Unable to initialize hash algorithm. SHA1 digest doesn't exist?!? (This should not happen") + } +} + +private val digest256 = ThreadLocal.withInitial { + try { + return@withInitial MessageDigest.getInstance("SHA-256") + } catch (e: NoSuchAlgorithmException) { + throw RuntimeException("Unable to initialize hash algorithm. SHA256 digest doesn't exist?!? (This should not happen") + } +} + + +fun ByteArray.sha1(): ByteArray { + val digest: MessageDigest = digest256.get() + + digest.reset() + digest.update(this) + return digest.digest() +} + +fun ByteArray.sha256(): ByteArray { + val digest: MessageDigest = digest256.get() + + digest.reset() + digest.update(this) + return digest.digest() +} diff --git a/test/dorkbox/bytes/TestBase58.kt b/test/dorkbox/bytes/TestBase58.kt new file mode 100644 index 0000000..1af2192 --- /dev/null +++ b/test/dorkbox/bytes/TestBase58.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2021 dorkbox, llc + * + * 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. + */ +package dorkbox.bytes + +import org.junit.Assert.assertTrue +import org.junit.Test + +class TestBase58 { + // Tests from https://github.com/bitcoin/bitcoin/blob/master/src/test/data/base58_encode_decode.json + private val TEST_VECTORS = mapOf( + "" to "", + "61" to "2g", + "626262" to "a3gV", + "636363" to "aPEr", + "73696d706c792061206c6f6e6720737472696e67" to "2cFupjhnEsSn59qHXstmK2ffpLv2", + "00eb15231dfceb60925886b67d065299925915aeb172c06647" to "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L", + "516b6fcd0f" to "ABnLTmg", + "bf4f89001e670274dd" to "3SEo3LWLoPntC", + "572e4794" to "3EFU7m", + "ecac89cad93923c02321" to "EJDM8drfXA6uyA", + "10c8511e" to "Rt5zm", + "00000000000000000000" to "1111111111" + ) + + @Test + fun encodingToBase58Works() { + TEST_VECTORS.forEach { + assertTrue(it.key.hexToByteArray().encodeToBase58String().contentEquals(it.value)) + } + } + + @Test + fun decodingFromBase58Works() { + TEST_VECTORS.forEach { + assertTrue(it.value.decodeBase58().contentEquals(it.key.hexToByteArray())) + } + } +}