taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

Bech32.kt (11048B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2020 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 // Copyright (c) 2020 Figure Technologies Inc.
     18 // The contents of this file were derived from an implementation
     19 // by the btcsuite developers https://github.com/btcsuite/btcutil.
     20 
     21 // Copyright (c) 2017 The btcsuite developers
     22 // Use of this source code is governed by an ISC
     23 // license that can be found in the LICENSE file.
     24 
     25 // modified version of https://gist.github.com/iramiller/4ebfcdfbc332a9722c4a4abeb4e16454
     26 
     27 package net.taler.common
     28 
     29 import java.util.Locale.ROOT
     30 import kotlin.experimental.and
     31 import kotlin.experimental.or
     32 
     33 infix fun Int.min(b: Int): Int = b.takeIf { this > b } ?: this
     34 infix fun UByte.shl(bitCount: Int) = ((this.toInt() shl bitCount) and 0xff).toUByte()
     35 infix fun UByte.shr(bitCount: Int) = (this.toInt() shr bitCount).toUByte()
     36 
     37 /**
     38  * Bech32 Data encoding instance containing data for encoding as well as a human readable prefix
     39  */
     40 data class Bech32Data(val hrp: String, val fiveBitData: ByteArray) {
     41 
     42     /**
     43      * The encapsulated data as typical 8bit bytes.
     44      */
     45     val data = Bech32.convertBits(fiveBitData, 5, 8, false)
     46 
     47     /**
     48      * Address is the Bech32 encoded value of the data prefixed with the human readable portion and
     49      * protected by an appended checksum.
     50      */
     51     val address = Bech32.encode(hrp, fiveBitData)
     52 
     53     /**
     54      * Checksum for encapsulated data + hrp
     55      */
     56     val checksum = Bech32.checksum(this.hrp, this.fiveBitData.toTypedArray())
     57 
     58     /**
     59      * The Bech32 Address toString prints state information for debugging purposes.
     60      * @see address() for the bech32 encoded address string output.
     61      */
     62     override fun toString(): String {
     63         return "bech32 : ${this.address}\nhuman: ${this.hrp} \nbytes"
     64     }
     65 
     66     override fun equals(other: Any?): Boolean {
     67         if (this === other) return true
     68         if (javaClass != other?.javaClass) return false
     69 
     70         other as Bech32Data
     71 
     72         if (hrp != other.hrp) return false
     73         if (!fiveBitData.contentEquals(other.fiveBitData)) return false
     74         if (!data.contentEquals(other.data)) return false
     75         if (address != other.address) return false
     76         if (!checksum.contentEquals(other.checksum)) return false
     77 
     78         return true
     79     }
     80 
     81     override fun hashCode(): Int {
     82         var result = hrp.hashCode()
     83         result = 31 * result + fiveBitData.contentHashCode()
     84         result = 31 * result + data.contentHashCode()
     85         result = 31 * result + address.hashCode()
     86         result = 31 * result + checksum.contentHashCode()
     87         return result
     88     }
     89 }
     90 
     91 /**
     92  * BIP173 compliant processing functions for handling Bech32 encoding for addresses
     93  */
     94 class Bech32 {
     95 
     96     companion object {
     97         const val CHECKSUM_SIZE = 6
     98         private const val MIN_VALID_LENGTH = 8
     99         private const val MAX_VALID_LENGTH = 90
    100         const val MIN_VALID_CODEPOINT = 33
    101         private const val MAX_VALID_CODEPOINT = 126
    102 
    103         const val charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
    104         private val gen = intArrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
    105 
    106         fun generateFakeSegwitAddress(reservePub: String?, addr: String): List<String> {
    107             if (reservePub == null || reservePub.isEmpty()) return listOf()
    108             val pub = CyptoUtils.decodeCrock(reservePub)
    109             if (pub.size != 32) return listOf()
    110 
    111             val firstRnd = pub.copyOfRange(0, 4)
    112             val secondRnd = pub.copyOfRange(0, 4)
    113 
    114             firstRnd[0] = firstRnd[0].and(0b0111_1111)
    115             secondRnd[0] = secondRnd[0].or(0b1000_0000.toByte())
    116 
    117             val firstPart = ByteArray(20)
    118             firstRnd.copyInto(firstPart, 0, 0, 4)
    119             pub.copyInto(firstPart, 4, 0, 16)
    120 
    121             val secondPart = ByteArray(20)
    122             secondRnd.copyInto(secondPart, 0, 0, 4)
    123             pub.copyInto(secondPart, 4, 16, 32)
    124 
    125             val zero = ByteArray(1)
    126             zero[0] = 0
    127             val hrp = when {
    128                 addr[0] == 'b' && addr[1] == 'c' && addr[2] == 'r' && addr[3] == 't' -> "bcrt"
    129                 addr[0] == 't' && addr[1] == 'b' -> "tb"
    130                 addr[0] == 'b' && addr[1] == 'c' -> "bc"
    131                 else -> throw Error("unknown bitcoin net")
    132             }
    133 
    134             return listOf(
    135                 Bech32Data(hrp, zero + convertBits(firstPart, 8, 5, true)).address,
    136                 Bech32Data(hrp, zero + convertBits(secondPart, 8, 5, true)).address,
    137             )
    138         }
    139 
    140         /**
    141          * Decodes a Bech32 String
    142          */
    143         fun decode(bech32: String): Bech32Data {
    144             require(bech32.length in MIN_VALID_LENGTH..MAX_VALID_LENGTH) { "invalid bech32 string length" }
    145             require(bech32.toCharArray()
    146                 .none { c -> c.code < MIN_VALID_CODEPOINT || c.code > MAX_VALID_CODEPOINT })
    147             {
    148                 "invalid character in bech32: ${
    149                     bech32.toCharArray().map { c -> c.code }
    150                         .filter { c -> c < MIN_VALID_CODEPOINT || c > MAX_VALID_CODEPOINT }
    151                 }"
    152             }
    153 
    154             require(bech32 == bech32.lowercase(ROOT) || bech32 == bech32.uppercase(ROOT))
    155             { "bech32 must be either all upper or lower case" }
    156             require(bech32.substring(1).dropLast(CHECKSUM_SIZE)
    157                 .contains('1')) { "invalid index of '1'" }
    158 
    159             val hrp = bech32.substringBeforeLast('1').lowercase(ROOT)
    160             val dataString = bech32.substringAfterLast('1').lowercase(ROOT)
    161 
    162             require(dataString.toCharArray()
    163                 .all { c -> charset.contains(c) }) { "invalid data encoding character in bech32" }
    164 
    165             val dataBytes = dataString.map { c -> charset.indexOf(c).toByte() }.toByteArray()
    166             val checkBytes =
    167                 dataString.takeLast(CHECKSUM_SIZE).map { c -> charset.indexOf(c).toByte() }
    168                     .toByteArray()
    169 
    170             val actualSum = checksum(hrp, dataBytes.dropLast(CHECKSUM_SIZE).toTypedArray())
    171             require(1 == polymod(expandHrp(hrp).plus(dataBytes.map { d -> d.toInt() }))) { "checksum failed: $checkBytes != $actualSum" }
    172 
    173             return Bech32Data(hrp, dataBytes.dropLast(CHECKSUM_SIZE).toByteArray())
    174         }
    175 
    176         /**
    177          * ConvertBits regroups bytes with toBits set based on reading groups of bits as a continuous stream group by fromBits.
    178          * This process is used to convert from base64 (from 8) to base32 (to 5) or the inverse.
    179          */
    180         fun convertBits(data: ByteArray, fromBits: Int, toBits: Int, pad: Boolean): ByteArray {
    181             require(fromBits in 1..8 && toBits in 1..8) { "only bit groups between 1 and 8 are supported" }
    182 
    183             // resulting bytes with each containing the toBits bits from the input set.
    184             val regrouped = arrayListOf<Byte>()
    185 
    186             var nextByte = 0.toUByte()
    187             var filledBits = 0
    188 
    189             data.forEach { d ->
    190                 // discard unused bits.
    191                 var b = (d.toUByte() shl (8 - fromBits))
    192 
    193                 // How many bits remain to extract from input data.
    194                 var remainFromBits = fromBits
    195 
    196                 while (remainFromBits > 0) {
    197                     // How many bits remain to be copied in
    198                     val remainToBits = toBits - filledBits
    199 
    200                     // we extract the remaining bits unless that is more than we need.
    201                     val toExtract =
    202                         remainFromBits.takeUnless { remainToBits < remainFromBits } ?: remainToBits
    203                     check(toExtract >= 0) { "extract should be positive" }
    204 
    205                     // move existing bits to the left to make room for bits toExtract, copy in bits to extract
    206                     nextByte = (nextByte shl toExtract) or (b shr (8 - toExtract))
    207 
    208                     // discard extracted bits and update position counters
    209                     b = b shl toExtract
    210                     remainFromBits -= toExtract
    211                     filledBits += toExtract
    212 
    213                     // if we have a complete group then reset.
    214                     if (filledBits == toBits) {
    215                         regrouped.add(nextByte.toByte())
    216                         filledBits = 0
    217                         nextByte = 0.toUByte()
    218                     }
    219                 }
    220             }
    221 
    222             // pad any unfinished groups as required
    223             if (pad && filledBits > 0) {
    224                 nextByte = nextByte shl (toBits - filledBits)
    225                 regrouped.add(nextByte.toByte())
    226                 filledBits = 0
    227                 nextByte = 0.toUByte()
    228             }
    229 
    230             return regrouped.toByteArray()
    231         }
    232 
    233         /**
    234          * Encodes data 5-bit bytes (data) with a given human readable portion (hrp) into a bech32 string.
    235          * @see convertBits for conversion or ideally use the Bech32Data extension functions
    236          */
    237         fun encode(hrp: String, fiveBitData: ByteArray): String {
    238             return (fiveBitData.plus(checksum(hrp, fiveBitData.toTypedArray()))
    239                 .map { b -> charset[b.toInt()] }).joinToString("", hrp + "1")
    240         }
    241 
    242         /**
    243          * Calculates a bech32 checksum based on BIP 173 specification
    244          */
    245         fun checksum(hrp: String, data: Array<Byte>): ByteArray {
    246             val values = expandHrp(hrp)
    247                 .plus(data.map { d -> d.toInt() })
    248                 .plus(Array(6) { 0 }.toIntArray())
    249 
    250             val poly = polymod(values) xor 1
    251 
    252             return (0..5).map {
    253                 ((poly shr (5 * (5 - it))) and 31).toByte()
    254             }.toByteArray()
    255         }
    256 
    257         /**
    258          * Expands the human readable prefix per BIP173 for Checksum encoding
    259          */
    260         private fun expandHrp(hrp: String) =
    261             hrp.map { c -> c.code shr 5 }
    262                 .plus(0)
    263                 .plus(hrp.map { c -> c.code and 31 })
    264                 .toIntArray()
    265 
    266         /**
    267          * Polynomial division function for checksum calculation.  For details see BIP173
    268          */
    269         fun polymod(values: IntArray): Int {
    270             var chk = 1
    271             return values.map { num ->
    272                 val b = chk shr 25
    273                 chk = ((chk and 0x1ffffff) shl 5) xor num
    274                 (0..4).map {
    275                     if (((b shr it) and 1) == 1) {
    276                         chk = chk xor gen[it]
    277                     }
    278                 }
    279             }.let { chk }
    280         }
    281     }
    282 }