Skip to content

Commit

Permalink
Merge 8842e40 into f4279d1
Browse files Browse the repository at this point in the history
  • Loading branch information
astinz authored Aug 23, 2024
2 parents f4279d1 + 8842e40 commit c385328
Show file tree
Hide file tree
Showing 12 changed files with 445 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.komputing.kbech32

sealed class AddressFormatException(message: String) : IllegalArgumentException(message) {

/**
* This exception is thrown by [Bech32] when you try to decode data and a character isn't valid.
* You shouldn't allow the user to proceed in this case.
*/
class InvalidCharacter(character: Char, position: Int) :
AddressFormatException("Invalid character '$character' at position $position")

/**
* This exception is thrown by [Bech32] when you try to decode data and the data isn't of the
* right size. You shouldn't allow the user to proceed in this case.
*/
class InvalidDataLength(message: String) : AddressFormatException(message)

/**
* This exception is thrown by [Bech32] when you try to decode data and the checksum isn't valid.
* You shouldn't allow the user to proceed in this case.
*/
class InvalidChecksum : AddressFormatException("Checksum does not validate")

/**
* This exception is thrown by [Bech32] when you try and decode an
* address or private key with an invalid prefix (version header or human-readable part). You shouldn't allow the
* user to proceed in this case.
*/
class InvalidPrefix(message: String) : AddressFormatException(message)
}
153 changes: 153 additions & 0 deletions lib/src/commonMain/kotlin/org/komputing/kbech32/Bech32.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package org.komputing.kbech32

/**
* Bech32 Kotlin implementation.
*
* Taken from [Bitcoinj Bech32 Java implementation](https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/core/Bech32.java)
*/
object Bech32 {
/** The Bech32 character set for encoding. */
private const val CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

/** The Bech32 character set for decoding. */
private val CHARSET_REV = byteArrayOf(
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1,
-1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11,
28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18,
22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
)

/** Find the polynomial with value coefficients mod the generator as 30-bit. */
private fun polymod(values: ByteArray): Int {
var c = 1
for (v_i in values) {
val c0 = c.ushr(25) and 0xff
c = c and 0x1ffffff shl 5 xor (v_i.toInt() and 0xff)
if (c0 and 1 != 0) c = c xor 0x3b6a57b2
if (c0 and 2 != 0) c = c xor 0x26508e6d
if (c0 and 4 != 0) c = c xor 0x1ea119fa
if (c0 and 8 != 0) c = c xor 0x3d4233dd
if (c0 and 16 != 0) c = c xor 0x2a1462b3
}
return c
}

/** Expand a HRP for use in checksum computation. */
private fun expandHrp(hrp: String): ByteArray {
val hrpLength = hrp.length
val ret = ByteArray(hrpLength * 2 + 1)
for (i in 0 until hrpLength) {
val c = hrp[i].toInt() and 0x7f // Limit to standard 7-bit ASCII
ret[i] = (c.ushr(5) and 0x07).toByte()
ret[i + hrpLength + 1] = (c and 0x1f).toByte()
}
ret[hrpLength] = 0
return ret
}

/** Verify a checksum. */
private fun verifyChecksum(hrp: String, values: ByteArray): Boolean {
val hrpExpanded = expandHrp(hrp)
val combined = ByteArray(hrpExpanded.size + values.size)
hrpExpanded.copyInto(combined)
values.copyInto(combined, destinationOffset = hrpExpanded.size)
return polymod(combined) == 1
}

/** Create a checksum. */
private fun createChecksum(hrp: String, values: ByteArray): ByteArray {
val hrpExpanded = expandHrp(hrp)
val enc = ByteArray(hrpExpanded.size + values.size + 6)
hrpExpanded.copyInto(enc)
values.copyInto(enc, startIndex = 0, destinationOffset = hrpExpanded.size)

val mod = polymod(enc) xor 1
val ret = ByteArray(6)
for (i in 0..5) {
ret[i] = (mod.ushr(5 * (5 - i)) and 31).toByte()
}
return ret
}

/**
* Encodes a Bech32 string.
*/
fun encode(bech32: Bech32Data): String {
return encode(bech32.humanReadablePart, bech32.data)
}

/**
* Encodes a Bech32 string.
*/
fun encode(humanReadablePart: String, data: ByteArray): String {
var hrp = humanReadablePart

check(hrp.isNotEmpty()) { "Human-readable part is too short" }
check(hrp.length <= 83) { "Human-readable part is too long" }

hrp = hrp.toLowerCase()
val checksum = createChecksum(hrp, data)
val combined = ByteArray(data.size + checksum.size)
data.copyInto(combined)
checksum.copyInto(combined, startIndex = 0, destinationOffset = data.size)

val sb = StringBuilder(hrp.length + 1 + combined.size)
sb.append(hrp)
sb.append('1')
for (b in combined) {
sb.append(CHARSET.get(b.toInt()))
}
return sb.toString()
}

/**
* Decodes a Bech32 string.
*/
fun decode(str: String): Bech32Data {
var lower = false
var upper = false
if (str.length < 8)
throw AddressFormatException.InvalidDataLength("Input too short: " + str.length)
if (str.length > 90)
throw AddressFormatException.InvalidDataLength("Input too long: " + str.length)
for (i in 0 until str.length) {
val c = str[i]
if (c.toInt() < 33 || c.toInt() > 126) throw AddressFormatException.InvalidCharacter(
c,
i
)
if (c in 'a'..'z') {
if (upper)
throw AddressFormatException.InvalidCharacter(c, i)
lower = true
}
if (c in 'A'..'Z') {
if (lower)
throw AddressFormatException.InvalidCharacter(c, i)
upper = true
}
}
val pos = str.lastIndexOf('1')
if (pos < 1) throw AddressFormatException.InvalidPrefix("Missing human-readable part")
val dataPartLength = str.length - 1 - pos
if (dataPartLength < 6) throw AddressFormatException.InvalidDataLength("Data part too short: $dataPartLength")
val values = ByteArray(dataPartLength)
for (i in 0 until dataPartLength) {
val c = str[i + pos + 1]
if (CHARSET_REV[c.toInt()].toInt() == -1) throw AddressFormatException.InvalidCharacter(
c,
i + pos + 1
)
values[i] = CHARSET_REV[c.toInt()]
}
val hrp = str.substring(0, pos).lowercase()
if (!verifyChecksum(
hrp,
values
)
) throw AddressFormatException.InvalidChecksum()
return Bech32Data(hrp, values.copyOfRange(0, values.size - 6))
}
}
21 changes: 21 additions & 0 deletions lib/src/commonMain/kotlin/org/komputing/kbech32/Bech32Data.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.komputing.kbech32

data class Bech32Data(val humanReadablePart: String, val data: ByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as Bech32Data

if (humanReadablePart != other.humanReadablePart) return false
if (!data.contentEquals(other.data)) return false

return true
}

override fun hashCode(): Int {
var result = humanReadablePart.hashCode()
result = 31 * result + data.contentHashCode()
return result
}
}
35 changes: 28 additions & 7 deletions lib/src/commonMain/kotlin/xyz/mcxross/ksui/account/Account.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package xyz.mcxross.ksui.account

import xyz.mcxross.ksui.core.crypto.Ed25519PrivateKey
import xyz.mcxross.ksui.core.crypto.PrivateKey
import xyz.mcxross.ksui.core.crypto.PublicKey
import xyz.mcxross.ksui.core.crypto.SignatureScheme
import xyz.mcxross.ksui.core.crypto.importFromMnemonic
Expand Down Expand Up @@ -88,20 +89,25 @@ abstract class Account {
/**
* Imports an account using the provided mnemonic phrase and signature scheme.
*
* @param phrase The mnemonic phrase of the account.
* @param str The mnemonic phrase of the account or the private key. And in which case, the
* private key is expected to be in the format of a Bech32 encoded string.
* @param scheme The signature scheme to use. Defaults to ED25519.
* @return The imported account.
* @throws SignatureSchemeNotSupportedException If the specified signature scheme is not
* supported.
*/
fun import(phrase: String, scheme: SignatureScheme = SignatureScheme.ED25519): Account {
return when (scheme) {
SignatureScheme.ED25519 -> {
val keyPair = importFromMnemonic(phrase)
Ed25519Account(Ed25519PrivateKey(keyPair.privateKey), phrase)
fun import(str: String, scheme: SignatureScheme = SignatureScheme.ED25519): Account {
if (str.contains(" ")) {
return when (scheme) {
SignatureScheme.ED25519 -> {
val keyPair = importFromMnemonic(str)
Ed25519Account(Ed25519PrivateKey(keyPair.privateKey), str)
}
else -> throw SignatureSchemeNotSupportedException()
}
else -> throw SignatureSchemeNotSupportedException()
}

return import(PrivateKey.fromEncoded(str))
}

/**
Expand All @@ -121,5 +127,20 @@ abstract class Account {
else -> throw SignatureSchemeNotSupportedException()
}
}

/**
* Imports an account using the provided private key.
*
* @param privateKey The private key of the account.
* @return The imported account.
* @throws SignatureSchemeNotSupportedException If the specified signature scheme is not
* supported.
*/
fun import(privateKey: PrivateKey): Account {
return when (privateKey) {
is Ed25519PrivateKey -> Ed25519Account(privateKey)
else -> throw SignatureSchemeNotSupportedException()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,44 @@
*/
package xyz.mcxross.ksui.core.crypto

import xyz.mcxross.ksui.account.Account
import xyz.mcxross.ksui.core.Hex


/**
* This class represents an Ed25519 private key.
*
* Creating a private key instance by either generating or _importing_ will not generate a
* passphrase. If you want to generate a passphrase, you can use the [Account.create] method.
*
* @property privateKey The private key data.
*/
class Ed25519PrivateKey(private val privateKey: ByteArray) : PrivateKey {

override val data: ByteArray
get() = privateKey

override val publicKey: Ed25519PublicKey
get() = derivePublicKey(this, SignatureScheme.ED25519) as Ed25519PublicKey

/**
* Creates a new [Ed25519PrivateKey] with a randomly generated private key.
*
* Default signature scheme is [SignatureScheme.ED25519].
*
* @param scheme The signature scheme to use.
* @throws IllegalArgumentException If the signature scheme is not supported.
*/
constructor(scheme: SignatureScheme = SignatureScheme.ED25519) : this(generatePrivateKey(scheme))

/**
* Creates a new [Ed25519PrivateKey] from an encoded private key.
*
* The expected format is a Bech32 encoded private key.
*
* @param privateKey The encoded private key.
* @throws IllegalArgumentException If the private key is invalid.
*/
constructor(privateKey: String) : this(PrivateKey.fromEncoded(privateKey).data)
}

class Ed25519PublicKey(val data: ByteArray) : PublicKey {
Expand Down
15 changes: 15 additions & 0 deletions lib/src/commonMain/kotlin/xyz/mcxross/ksui/core/crypto/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package xyz.mcxross.ksui.core.crypto

import xyz.mcxross.ksui.exception.SignatureSchemeNotSupportedException

expect fun hash(hash: Hash, data: ByteArray): ByteArray

expect fun generateMnemonic(): String
Expand All @@ -28,3 +30,16 @@ expect fun derivePublicKey(privateKey: PrivateKey, schema: SignatureScheme): Pub
expect fun importFromMnemonic(mnemonic: String): KeyPair

expect fun importFromMnemonic(mnemonic: List<String>): KeyPair

fun generatePrivateKey(scheme: SignatureScheme): ByteArray {
return when (scheme) {
SignatureScheme.ED25519 -> {
val seedPhrase = generateMnemonic().split(" ")
val seed = generateSeed(seedPhrase)
generateKeyPair(seed, SignatureScheme.ED25519).privateKey
}
else -> {
throw SignatureSchemeNotSupportedException()
}
}
}
Loading

0 comments on commit c385328

Please sign in to comment.