Skip to content

Commit

Permalink
Merge pull request #15 from mcxross/crypto
Browse files Browse the repository at this point in the history
feat: add account management
  • Loading branch information
astinz authored Aug 22, 2024
2 parents 6cd2736 + 9bbbab3 commit 3dcdd2e
Show file tree
Hide file tree
Showing 34 changed files with 988 additions and 635 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ graphql-multiplatform = "0.1.0-beta07"

[libraries]
bcs = { module = "xyz.mcxross.bcs:bcs", version = "0.1.2" }
bitcoinj-core = { module = "org.bitcoinj:bitcoinj-core", version = "0.16.1" }
graphql-multiplatform-client = { module = "xyz.mcxross.graphql.client:graphql-multiplatform-client", version.ref = "graphql-multiplatform" }
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
Expand Down
22 changes: 19 additions & 3 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,19 @@ kotlin {
applyDefaultHierarchyTemplate()

sourceSets {
val androidJvmMain by creating {
dependsOn(commonMain.get())
dependencies {
implementation(libs.bitcoinj.core)
}
}
appleMain.dependencies { implementation(libs.ktor.client.darwin) }
androidMain.dependencies { implementation(libs.ktor.client.okhttp) }
val androidMain by getting {
dependsOn(androidJvmMain)
dependencies {
implementation(libs.ktor.client.okhttp)
}
}
commonMain.dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
Expand All @@ -79,7 +90,12 @@ kotlin {
implementation(libs.kotlin.test)
}
jsMain.dependencies { implementation(libs.ktor.client.js) }
jvmMain.dependencies { implementation(libs.ktor.client.cio) }
val jvmMain by getting {
dependsOn(androidJvmMain)
dependencies {
implementation(libs.ktor.client.cio)
}
}
linuxMain.dependencies { implementation(libs.ktor.client.curl) }
mingwMain.dependencies { implementation(libs.ktor.client.winhttp) }
}
Expand Down Expand Up @@ -136,7 +152,7 @@ mavenPublishing {
javadocJar = JavadocJar.Dokka("dokkaHtml"),
sourcesJar = true,
androidVariantsToPublish = listOf("debug", "release"),
)
),
)

pom {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2024 McXross
*
* 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 xyz.mcxross.ksui.core.crypto

import com.google.common.primitives.Bytes
import java.nio.ByteBuffer
import java.nio.charset.Charset
import java.util.*
import org.bitcoinj.crypto.ChildNumber
import org.bitcoinj.crypto.HDPath
import org.bitcoinj.crypto.HDUtils

class ED25519KeyDerive(val key: ByteArray, val chaincode: ByteArray) {

fun derive(index: Int): ED25519KeyDerive {
if (!hasHardenedBit(index)) {
// todo: create an exception
throw RuntimeException()
}

val indexBytes = ByteArray(4)
ByteBuffer.wrap(indexBytes).putInt(index)

val data = Bytes.concat(byteArrayOf(0x00), this.key, indexBytes)

val i = HDUtils.hmacSha512(this.chaincode, data)
val il = Arrays.copyOfRange(i, 0, 32)
val ir = Arrays.copyOfRange(i, 32, 64)

return ED25519KeyDerive(il, ir)
}

fun deriveFromPath(path: String = DEFAULT_DERIVE_PATH): ED25519KeyDerive {
require(path.isNotBlank()) { "Path cannot be blank" }
val hdPath = HDPath.parsePath(path)
val it: Iterator<ChildNumber> = hdPath.iterator()
var current = this
while (it.hasNext()) {
current = current.derive(it.next().i)
}
return current
}

private fun hasHardenedBit(a: Int): Boolean {
return (a and ChildNumber.HARDENED_BIT) != 0
}

companion object {
private const val DEFAULT_DERIVE_PATH = "m/44H/784H/0H/0H/0H"

fun createKeyByDefaultPath(seed: ByteArray): ED25519KeyDerive {
return createMasterKey(seed).deriveFromPath()
}

fun createMasterKey(seed: ByteArray): ED25519KeyDerive {
val i = HDUtils.hmacSha512("ed25519 seed".toByteArray(Charset.defaultCharset()), seed)
val il = Arrays.copyOfRange(i, 0, 32)
val ir = Arrays.copyOfRange(i, 32, 64)
return ED25519KeyDerive(il, ir)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2024 McXross
*
* 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 xyz.mcxross.ksui.core.crypto

import java.security.SecureRandom
import java.util.ArrayList
import org.bitcoinj.crypto.MnemonicCode
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
import org.bouncycastle.jcajce.provider.digest.Blake2b
import xyz.mcxross.ksui.exception.SignatureSchemeNotSupportedException

actual fun hash(hash: Hash, data: ByteArray): ByteArray {
return when (hash) {
Hash.BLAKE2B256 -> Blake2b.Blake2b256().digest(data)
}
}

actual fun generateMnemonic(): String {
val secureRandom = SecureRandom()
val entropy = ByteArray(16)
secureRandom.nextBytes(entropy)
var mnemonic: List<String> = ArrayList()

try {
mnemonic = MnemonicCode.INSTANCE.toMnemonic(entropy)
} catch (e: java.lang.Exception) {
// MnemonicLengthException won't happen
}
return mnemonic.joinToString(" ")
}

actual fun generateSeed(mnemonic: List<String>): ByteArray {
return MnemonicCode.toSeed(mnemonic, "")
}

@Throws(SignatureSchemeNotSupportedException::class)
actual fun generateKeyPair(seed: ByteArray, scheme: SignatureScheme): KeyPair {
return when (scheme) {
SignatureScheme.ED25519 -> {
val key: ED25519KeyDerive = ED25519KeyDerive.createKeyByDefaultPath(seed)
val parameters = Ed25519PrivateKeyParameters(key.key)
val publicKeyParameters = parameters.generatePublicKey()
KeyPair(parameters.encoded, publicKeyParameters.encoded)
}
else -> throw SignatureSchemeNotSupportedException()
}
}

actual fun derivePublicKey(privateKey: PrivateKey, schema: SignatureScheme): PublicKey {
return when (schema) {
SignatureScheme.ED25519 -> {
val privateKeyParameters = Ed25519PrivateKeyParameters(privateKey.data)
val publicKeyParameters = privateKeyParameters.generatePublicKey()
Ed25519PublicKey(publicKeyParameters.encoded)
}
else -> throw SignatureSchemeNotSupportedException()
}
}

@Throws(SignatureSchemeNotSupportedException::class)
actual fun importFromMnemonic(mnemonic: String): KeyPair {
return importFromMnemonic(mnemonic.split(" "))
}

actual fun importFromMnemonic(mnemonic: List<String>): KeyPair {
val seed = MnemonicCode.toSeed(mnemonic, "")
return generateKeyPair(seed, SignatureScheme.ED25519)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2024 McXross
*
* 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 xyz.mcxross.ksui.core.crypto

import xyz.mcxross.ksui.exception.SignatureSchemeNotSupportedException

actual fun hash(hash: Hash, data: ByteArray): ByteArray {
TODO("Not yet implemented")
}

actual fun generateMnemonic(): String {
TODO("Not yet implemented")
}

actual fun generateSeed(mnemonic: List<String>): ByteArray {
TODO("Not yet implemented")
}

@Throws(SignatureSchemeNotSupportedException::class)
actual fun generateKeyPair(
seed: ByteArray,
scheme: SignatureScheme
): KeyPair {
TODO("Not yet implemented")
}

actual fun derivePublicKey(
privateKey: PrivateKey,
schema: SignatureScheme
): PublicKey {
TODO("Not yet implemented")
}

actual fun importFromMnemonic(mnemonic: String): KeyPair {
TODO("Not yet implemented")
}

actual fun importFromMnemonic(mnemonic: List<String>): KeyPair {
TODO("Not yet implemented")
}
125 changes: 125 additions & 0 deletions lib/src/commonMain/kotlin/xyz/mcxross/ksui/account/Account.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2024 McXross
*
* 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 xyz.mcxross.ksui.account

import xyz.mcxross.ksui.core.crypto.Ed25519PrivateKey
import xyz.mcxross.ksui.core.crypto.PublicKey
import xyz.mcxross.ksui.core.crypto.SignatureScheme
import xyz.mcxross.ksui.core.crypto.importFromMnemonic
import xyz.mcxross.ksui.exception.SignatureSchemeNotSupportedException
import xyz.mcxross.ksui.model.AccountAddress

/**
* This file defines the `Account` abstract class and its companion object, which provides methods
* for creating and importing accounts using different signature schemes.
*
* The `[Account]` class has the following abstract properties:
* - `[publicKey]`: The public key of the account.
* - `[address]`: The account address.
* - `[scheme]`: The signature scheme used by the account.
*
* The companion object provides the following methods:
* - `create(scheme: SignatureScheme = SignatureScheme.ED25519)`: Creates a new account using the
* specified signature scheme. Defaults to ED25519.
* - `import(privateKey: ByteArray, scheme: SignatureScheme = SignatureScheme.ED25519)`: Imports an
* account using the provided private key and signature scheme. Defaults to ED25519.
* - `import(phrase: String, scheme: SignatureScheme = SignatureScheme.ED25519)`: Imports an account
* using the provided mnemonic phrase and signature scheme. Defaults to ED25519.
* - `import(phrases: List<String>, scheme: SignatureScheme = SignatureScheme.ED25519)`: Imports an
* account using the provided list of mnemonic phrases and signature scheme. Defaults to ED25519.
*
* The `[create]` and `[import]` methods throw a `[SignatureSchemeNotSupportedException]` if the
* specified signature scheme is not supported.
*/
abstract class Account {

abstract val publicKey: PublicKey

abstract val address: AccountAddress

abstract val scheme: SignatureScheme

companion object {

/**
* Creates a new account using the specified signature scheme.
*
* @param scheme The signature scheme to use. Defaults to ED25519.
* @return The new account.
* @throws SignatureSchemeNotSupportedException If the specified signature scheme is not
* supported.
*/
fun create(scheme: SignatureScheme = SignatureScheme.ED25519): Account {
return when (scheme) {
SignatureScheme.ED25519 -> Ed25519Account.generate()
else -> throw SignatureSchemeNotSupportedException()
}
}

/**
* Imports an account using the provided private key and signature scheme.
*
* @param privateKey The private key of the account.
* @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(privateKey: ByteArray, scheme: SignatureScheme = SignatureScheme.ED25519): Account {
return when (scheme) {
SignatureScheme.ED25519 -> Ed25519Account(Ed25519PrivateKey(privateKey))
else -> throw SignatureSchemeNotSupportedException()
}
}

/**
* Imports an account using the provided mnemonic phrase and signature scheme.
*
* @param phrase The mnemonic phrase of the account.
* @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)
}
else -> throw SignatureSchemeNotSupportedException()
}
}

/**
* Imports an account using the provided list of mnemonic phrases and signature scheme.
*
* @param phrases The list of mnemonic phrases of the account.
* @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(phrases: List<String>, scheme: SignatureScheme = SignatureScheme.ED25519): Account {
return when (scheme) {
SignatureScheme.ED25519 -> {
import(phrases.joinToString(" "))
}
else -> throw SignatureSchemeNotSupportedException()
}
}
}
}
Loading

0 comments on commit 3dcdd2e

Please sign in to comment.