diff options
Diffstat (limited to 'taler-kotlin-android/src/main')
-rw-r--r-- | taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt | 133 |
1 files changed, 79 insertions, 54 deletions
diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt index 32885df..4b77f85 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt @@ -13,6 +13,7 @@ * You should have received a copy of the GNU General Public License along with * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ + // Copyright (c) 2020 Figure Technologies Inc. // The contents of this file were derived from an implementation // by the btcsuite developers https://github.com/btcsuite/btcutil. @@ -23,27 +24,17 @@ // modified version of https://gist.github.com/iramiller/4ebfcdfbc332a9722c4a4abeb4e16454 -import net.taler.common.CyptoUtils +package net.taler.common + +import java.util.Locale.ROOT import kotlin.experimental.and import kotlin.experimental.or - infix fun Int.min(b: Int): Int = b.takeIf { this > b } ?: this infix fun UByte.shl(bitCount: Int) = ((this.toInt() shl bitCount) and 0xff).toUByte() infix fun UByte.shr(bitCount: Int) = (this.toInt() shr bitCount).toUByte() /** - * Given an array of bytes, associate an HRP and return a Bech32Data instance. - */ -fun ByteArray.toBech32Data(hrp: String) = - Bech32Data(hrp, Bech32.convertBits(this, 8, 5, true)) - -/** - * Using a string in bech32 encoded address format, parses out and returns a Bech32Data instance - */ -fun String.toBech32Data() = Bech32.decode(this) - -/** * Bech32 Data encoding instance containing data for encoding as well as a human readable prefix */ data class Bech32Data(val hrp: String, val fiveBitData: ByteArray) { @@ -71,6 +62,30 @@ data class Bech32Data(val hrp: String, val fiveBitData: ByteArray) { override fun toString(): String { return "bech32 : ${this.address}\nhuman: ${this.hrp} \nbytes" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Bech32Data + + if (hrp != other.hrp) return false + if (!fiveBitData.contentEquals(other.fiveBitData)) return false + if (!data.contentEquals(other.data)) return false + if (address != other.address) return false + if (!checksum.contentEquals(other.checksum)) return false + + return true + } + + override fun hashCode(): Int { + var result = hrp.hashCode() + result = 31 * result + fiveBitData.contentHashCode() + result = 31 * result + data.contentHashCode() + result = 31 * result + address.hashCode() + result = 31 * result + checksum.contentHashCode() + return result + } } /** @@ -80,32 +95,32 @@ class Bech32 { companion object { const val CHECKSUM_SIZE = 6 - const val MIN_VALID_LENGTH = 8 - const val MAX_VALID_LENGTH = 90 + private const val MIN_VALID_LENGTH = 8 + private const val MAX_VALID_LENGTH = 90 const val MIN_VALID_CODEPOINT = 33 - const val MAX_VALID_CODEPOINT = 126 + private const val MAX_VALID_CODEPOINT = 126 const val charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" - val gen = intArrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3) + private val gen = intArrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3) fun generateFakeSegwitAddress(reservePub: String?, addr: String): List<String> { - if (reservePub == null || reservePub.isEmpty()) return listOf<String>() + if (reservePub == null || reservePub.isEmpty()) return listOf() val pub = CyptoUtils.decodeCrock(reservePub) if (pub.size != 32) return listOf() - val first_rnd = pub.copyOfRange(0,4) - val second_rnd = pub.copyOfRange(0,4) + val firstRnd = pub.copyOfRange(0, 4) + val secondRnd = pub.copyOfRange(0, 4) - first_rnd[0] = first_rnd[0].and(0b0111_1111); - second_rnd[0] = second_rnd[0].or(0b1000_0000.toByte()); + firstRnd[0] = firstRnd[0].and(0b0111_1111) + secondRnd[0] = secondRnd[0].or(0b1000_0000.toByte()) - val first_part = ByteArray(20); - first_rnd.copyInto( first_part, 0, 0, 4) - pub.copyInto( first_part, 4, 0, 16) + val firstPart = ByteArray(20) + firstRnd.copyInto(firstPart, 0, 0, 4) + pub.copyInto(firstPart, 4, 0, 16) - val second_part = ByteArray(20); - second_rnd.copyInto( second_part, 0, 0, 4) - pub.copyInto( second_part, 4, 16, 32) + val secondPart = ByteArray(20) + secondRnd.copyInto(secondPart, 0, 0, 4) + pub.copyInto(secondPart, 4, 16, 32) val zero = ByteArray(1) zero[0] = 0 @@ -117,8 +132,8 @@ class Bech32 { } return listOf( - Bech32Data(hrp, zero + convertBits(first_part, 8, 5, true)).address, - Bech32Data(hrp, zero + convertBits(second_part, 8, 5, true)).address, + Bech32Data(hrp, zero + convertBits(firstPart, 8, 5, true)).address, + Bech32Data(hrp, zero + convertBits(secondPart, 8, 5, true)).address, ) } @@ -126,22 +141,31 @@ class Bech32 { * Decodes a Bech32 String */ fun decode(bech32: String): Bech32Data { - require(bech32.length >= MIN_VALID_LENGTH && bech32.length <= MAX_VALID_LENGTH) { "invalid bech32 string length" } - require(bech32.toCharArray().none { c -> c.toInt() < MIN_VALID_CODEPOINT || c.toInt() > MAX_VALID_CODEPOINT }) - { "invalid character in bech32: ${bech32.toCharArray().map { c -> c.toInt() } - .filter { c -> c.toInt() < MIN_VALID_CODEPOINT || c.toInt() > MAX_VALID_CODEPOINT }}" } + require(bech32.length in MIN_VALID_LENGTH..MAX_VALID_LENGTH) { "invalid bech32 string length" } + require(bech32.toCharArray() + .none { c -> c.code < MIN_VALID_CODEPOINT || c.code > MAX_VALID_CODEPOINT }) + { + "invalid character in bech32: ${ + bech32.toCharArray().map { c -> c.code } + .filter { c -> c < MIN_VALID_CODEPOINT || c > MAX_VALID_CODEPOINT } + }" + } - require(bech32.equals(bech32.toLowerCase()) || bech32.equals(bech32.toUpperCase())) + require(bech32 == bech32.lowercase(ROOT) || bech32 == bech32.uppercase(ROOT)) { "bech32 must be either all upper or lower case" } - require(bech32.substring(1).dropLast(CHECKSUM_SIZE).contains('1')) { "invalid index of '1'" } + require(bech32.substring(1).dropLast(CHECKSUM_SIZE) + .contains('1')) { "invalid index of '1'" } - val hrp = bech32.substringBeforeLast('1').toLowerCase() - val dataString = bech32.substringAfterLast('1').toLowerCase() + val hrp = bech32.substringBeforeLast('1').lowercase(ROOT) + val dataString = bech32.substringAfterLast('1').lowercase(ROOT) - require(dataString.toCharArray().all { c -> charset.contains(c) }) { "invalid data encoding character in bech32"} + require(dataString.toCharArray() + .all { c -> charset.contains(c) }) { "invalid data encoding character in bech32" } val dataBytes = dataString.map { c -> charset.indexOf(c).toByte() }.toByteArray() - val checkBytes = dataString.takeLast(CHECKSUM_SIZE).map { c -> charset.indexOf(c).toByte() }.toByteArray() + val checkBytes = + dataString.takeLast(CHECKSUM_SIZE).map { c -> charset.indexOf(c).toByte() } + .toByteArray() val actualSum = checksum(hrp, dataBytes.dropLast(CHECKSUM_SIZE).toTypedArray()) require(1 == polymod(expandHrp(hrp).plus(dataBytes.map { d -> d.toInt() }))) { "checksum failed: $checkBytes != $actualSum" } @@ -154,10 +178,10 @@ class Bech32 { * This process is used to convert from base64 (from 8) to base32 (to 5) or the inverse. */ fun convertBits(data: ByteArray, fromBits: Int, toBits: Int, pad: Boolean): ByteArray { - require (fromBits in 1..8 && toBits in 1..8) { "only bit groups between 1 and 8 are supported"} + require(fromBits in 1..8 && toBits in 1..8) { "only bit groups between 1 and 8 are supported" } // resulting bytes with each containing the toBits bits from the input set. - var regrouped = arrayListOf<Byte>() + val regrouped = arrayListOf<Byte>() var nextByte = 0.toUByte() var filledBits = 0 @@ -174,8 +198,9 @@ class Bech32 { val remainToBits = toBits - filledBits // we extract the remaining bits unless that is more than we need. - val toExtract = remainFromBits.takeUnless { remainToBits < remainFromBits } ?: remainToBits - check(toExtract >= 0) { "extract should be positive"} + val toExtract = + remainFromBits.takeUnless { remainToBits < remainFromBits } ?: remainToBits + check(toExtract >= 0) { "extract should be positive" } // move existing bits to the left to make room for bits toExtract, copy in bits to extract nextByte = (nextByte shl toExtract) or (b shr (8 - toExtract)) @@ -218,24 +243,24 @@ class Bech32 { * Calculates a bech32 checksum based on BIP 173 specification */ fun checksum(hrp: String, data: Array<Byte>): ByteArray { - var values = expandHrp(hrp) + val values = expandHrp(hrp) .plus(data.map { d -> d.toInt() }) - .plus(Array<Int>(6){ _ -> 0}.toIntArray()) + .plus(Array(6) { 0 }.toIntArray()) - var poly = polymod(values) xor 1 + val poly = polymod(values) xor 1 return (0..5).map { - ((poly shr (5 * (5-it))) and 31).toByte() + ((poly shr (5 * (5 - it))) and 31).toByte() }.toByteArray() } /** * Expands the human readable prefix per BIP173 for Checksum encoding */ - fun expandHrp(hrp: String) = - hrp.map { c -> c.toInt() shr 5 } + private fun expandHrp(hrp: String) = + hrp.map { c -> c.code shr 5 } .plus(0) - .plus(hrp.map { c -> c.toInt() and 31 }) + .plus(hrp.map { c -> c.code and 31 }) .toIntArray() /** @@ -243,9 +268,9 @@ class Bech32 { */ fun polymod(values: IntArray): Int { var chk = 1 - return values.map { - var b = chk shr 25 - chk = ((chk and 0x1ffffff) shl 5) xor it + return values.map { num -> + val b = chk shr 25 + chk = ((chk and 0x1ffffff) shl 5) xor num (0..4).map { if (((b shr it) and 1) == 1) { chk = chk xor gen[it] |