Commit 09113d41 authored by Malena López-Batista's avatar Malena López-Batista

Wallet

parent db90ab01
# This file is a template, and might need editing before it works on your project.
# This is the Gradle build system for JVM applications
# https://gradle.org/
# https://github.com/gradle/gradle
image: gradle:alpine
image: openjdk:8-jdk
# Disable the Gradle daemon for Continuous Integration servers as correctness
# is usually a priority over speed in CI environments. Using a fresh
......@@ -13,10 +12,13 @@ variables:
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle
# Commands to ensure that package repository listings are up to date, and it installs packages we'll be using later on
- apt-get --quiet update --yes
- apt-get install -yqq --no-install-recommends build-essential libsodium-dev
build:
stage: build
script: gradle --build-cache assemble
script: ./gradlew --build-cache assemble
cache:
key: "$CI_COMMIT_REF_NAME"
policy: push
......@@ -26,7 +28,7 @@ build:
test:
stage: test
script: gradle check
script: ./gradlew check
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
......
# JavaTezos
JavaTezos is a Kotlin SDK, compatible with Java, for operating with the [Tezos Blockchain](https://tezos.gitlab.io
/tezos/index.html).
/tezos/index.html). API design and documentation are identical to our [TezosKit](https://github.com/keefertaylor
/TezosKit) counterpart, so if you already have experience with it, it should be easier to use.
#### This project is under construction.
......@@ -17,6 +18,8 @@ Name | Version | License
*JSON* | `20190722` | [JSON License](https://www.json.org/license.html)
*Kotlin Futures JDK8* | `1.2.0` | [MIT License](https://github.com/vjames19/kotlin-futures/blob/master/LICENSE)
*BitcoinJ* | `0.15.6` | [Apache License 2.0](https://github.com/bitcoinj/bitcoinj/blob/master/COPYING)
*LazySodium* | `4.2.3` | [Mozilla Public License 2.0](https://github.com/terl/lazysodium-java/blob/master/LICENSE.md)
*Java Native Access* | `5.5.0` | [Apache License 2.0](https://github.com/java-native-access/jna/blob/master/LICENSE)
Since OKHttp is a standard in the community, we only support its major version to avoid dependency conflicts. You
can provide your own client implementation implementing the ``NetworkClient`` or `ClientWrapper` interfaces.
......
......@@ -10,6 +10,7 @@ version = "0.1.0"
repositories {
mavenCentral()
maven("https://jitpack.io")
jcenter()
}
object Versions {
......@@ -17,7 +18,10 @@ object Versions {
const val OKHTTP = "4.+"
const val JSON = "20190722"
const val KT_FUTURES = "1.2.0"
// Cryptography
const val BITCOINJ = "0.15.6"
const val LAZYSODIUM = "4.2.3"
const val JNA = "5.5.0"
// Testing
const val MOCKWEBSERVER = "4.+"
const val MOCKK = "1.9.+"
......@@ -29,8 +33,10 @@ dependencies {
implementation("com.squareup.okhttp3:okhttp:${Versions.OKHTTP}")
implementation("org.json:json:${Versions.JSON}")
implementation("com.github.vjames19.kotlin-futures:kotlin-futures-jdk8:${Versions.KT_FUTURES}")
implementation("org.bitcoinj:bitcoinj-core:${Versions.BITCOINJ}")
implementation("org.bitcoinj:bitcoinj-core:${Versions.BITCOINJ}")
implementation("com.goterl.lazycode:lazysodium-java:${Versions.LAZYSODIUM}")
implementation("net.java.dev.jna:jna:${Versions.JNA}") // Libsodium dependency
testImplementation("com.squareup.okhttp3:mockwebserver:${Versions.MOCKWEBSERVER}")
testImplementation("io.mockk:mockk:${Versions.MOCKK}")
......
......@@ -52,6 +52,8 @@ internal object Base58Utils {
* Common prefixes used across Tezos Cryptography.
*/
internal object Prefix {
//Watermark
internal val operation: ByteArray = byteArrayOf(3.toByte())
// Keys
internal val edpk: ByteArray = byteArrayOf(13.toByte(), 15.toByte(), 37.toByte(), 217.toByte())
internal val edsk: ByteArray = byteArrayOf(43.toByte(), 246.toByte(), 78.toByte(), 7.toByte())
......
......@@ -19,36 +19,22 @@
*/
package io.camlcase.javatezos.crypto
import java.security.Key
enum class EllipticalCurve {
ED25519
}
data class PrivateKey(
/**
* Underlying bytes.
*/
val bytes: ByteArray
){
/**
* Base58Check representation of the key, prefixed with 'espk'.
*/
val base58Representation: String
get() {
TODO()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PrivateKey
if (!bytes.contentEquals(other.bytes)) return false
return true
}
enum class KeyFormat {
BASE58
}
override fun hashCode(): Int {
return bytes.contentHashCode()
/**
* Prepare bytes for verification by applying a watermark and hashing.
*/
fun Key?.watermarkAndHash(bytes: ByteArray): ByteArray? {
return this?.let {
val watermark = Prefix.operation + bytes
SodiumUtils.hash(watermark, 32)
}
}
......@@ -20,7 +20,7 @@
package io.camlcase.javatezos.crypto
import io.camlcase.javatezos.crypto.Base58Utils.encodeCheckWithPrefix
import io.camlcase.javatezos.crypto.mnemonic.hexStringToByteArray
/**
* Encapsulation of a Public Key.
......@@ -29,14 +29,19 @@ import io.camlcase.javatezos.crypto.Base58Utils.encodeCheckWithPrefix
data class PublicKey(
val bytes: ByteArray,
val signingCurve: EllipticalCurve = EllipticalCurve.ED25519
) {
) : java.security.PublicKey {
/**
* Public key hash representation of the key.
* Will be null if hash couldn't be calculated.
*/
val hash: String
val hash: String?
get() {
TODO()
SodiumUtils.hash(bytes, 20)?.let {
// For EllipticalCurve.ED25519
return encodeCheckWithPrefix(it, Prefix.tz1)
}
return null
}
/**
* Base58Check representation of the key, prefixed with 'espk'.
......@@ -59,6 +64,18 @@ data class PublicKey(
return true
}
override fun getFormat(): String {
return KeyFormat.BASE58.name
}
override fun getAlgorithm(): String {
return EllipticalCurve.ED25519.name // Not supported in Java yet
}
override fun getEncoded(): ByteArray {
return bytes
}
override fun hashCode(): Int {
var result = bytes.contentHashCode()
result = 31 * result + signingCurve.hashCode()
......@@ -66,17 +83,43 @@ data class PublicKey(
return result
}
/**
* Verify that the given [signature] matches the given input [hex].
*/
fun verify(signature: ByteArray, hex: String): Boolean {
return verify(signature, hex.hexStringToByteArray())
}
/**
* Verify that the given [signature] matches the given input [bytes].
*
* @param bytes The bytes to check.
* @param signature The proposed signature of the bytes.
*/
fun verify(signature: ByteArray, bytes: ByteArray): Boolean {
return watermarkAndHash(bytes)?.let {
// For EllipticalCurve.ED25519
return SodiumUtils.verify(it, this.bytes, signature)
} ?: false
}
companion object {
/**
* Constructor: Initialize a key from the given secret key
*/
operator fun invoke(secretKey: ByteArray) = run {
val end = secretKey.size - 1
val byte: ByteArray = secretKey.sliceArray(32..end)
PublicKey(byte)
operator fun invoke(secretKey: SecretKey?) = run {
secretKey?.let {
PublicKey(secretKeyBytes = secretKey.bytes)
}
}
/**
* Constructor: Initialize a key from the given secret key bytes
*/
operator fun invoke(secretKeyBytes: ByteArray) = run {
val end = secretKeyBytes.size - 1
val byte: ByteArray = secretKeyBytes.sliceArray(32..end)
PublicKey(byte)
}
}
}
/**
* # Released under MIT License
*
* Copyright (c) 2019 camlCase
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
* the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.camlcase.javatezos.crypto
import io.camlcase.javatezos.crypto.mnemonic.Mnemonic
import io.camlcase.javatezos.crypto.mnemonic.hexStringToByteArray
import javax.crypto.SecretKey
/**
* Encapsulation of a secret key.
*/
data class SecretKey(
val bytes: ByteArray
) : SecretKey {
/**
* Base58Check representation of the key, prefixed with 'edsk'.
*/
val base58Representation: String
get() {
return Base58Utils.encodeCheckWithPrefix(bytes, Prefix.edsk)
}
override fun getAlgorithm(): String {
return EllipticalCurve.ED25519.name
}
override fun getEncoded(): ByteArray {
return bytes
}
override fun getFormat(): String {
return KeyFormat.BASE58.name
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as io.camlcase.javatezos.crypto.SecretKey
if (!bytes.contentEquals(other.bytes)) return false
if (base58Representation != other.base58Representation) return false
return true
}
override fun hashCode(): Int {
return bytes.contentHashCode()
}
/**
* Sign the given hexadecimal encoded string with this secret key.
* @param hex The string to sign.
* @return A signature from the input.
*/
fun sign(hex: String): ByteArray? {
return sign(hex.hexStringToByteArray())
}
/**
* Sign the given [bytes] with this secret key.
*/
fun sign(bytes: ByteArray): ByteArray? {
return watermarkAndHash(bytes)?.let { bytesToSign ->
SodiumUtils.sign(bytesToSign, this.bytes)
}
}
companion object {
/**
* Constructor: Initialize a key with the given mnemonic and passphrase.
*/
operator fun invoke(mnemonic: List<String>, passphrase: String = "") = run {
val seed = Mnemonic.seedString(mnemonic, passphrase)
seed?.let { SecretKey(it) }
}
/**
* Constructor: Initialize a key with the given hex seed string.
*/
operator fun invoke(seedString: String) = run {
try {
val seed = seedString.hexStringToByteArray()
SodiumUtils.createSecretKey(seed)?.let {
SecretKey(it)
}
} catch (e: NumberFormatException) {
return null
}
}
}
}
......@@ -28,5 +28,5 @@ class SigningService {
*/
interface SignatureProvider {
val publicKey: PublicKey
fun sign(hex: String): ByteArray
fun sign(hex: String): ByteArray?
}
/**
* # Released under MIT License
*
* Copyright (c) 2019 camlCase
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
* the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.camlcase.javatezos.crypto
import com.goterl.lazycode.lazysodium.LazySodium
import com.goterl.lazycode.lazysodium.Sodium
import com.goterl.lazycode.lazysodium.SodiumJava
import com.goterl.lazycode.lazysodium.exceptions.SodiumException
import com.goterl.lazycode.lazysodium.interfaces.Sign
import com.goterl.lazycode.lazysodium.utils.KeyPair
/**
* Wrapper for Libsodium cryptographic utils.
*/
object SodiumUtils {
private val sodiumProvider = object : LazySodium() {
private val sodium: SodiumJava = SodiumJava()
override fun getSodium(): Sodium {
return sodium
}
}
/**
* Create a pair of keys with the given hex seed string.
* @return null if error creating the pair
*/
fun keyPair(seed: ByteArray): KeyPair? {
return try {
sodiumProvider.cryptoSignSeedKeypair(seed)
} catch (e: SodiumException) {
null
}
}
/**
* Create a secret key with the given hex seed string.
* @return null if error creating the pair
*/
fun createSecretKey(seed: ByteArray): ByteArray? {
return keyPair(seed)?.secretKey?.asBytes
}
fun hexHash(bytes: ByteArray, outputLenght: Int): String? {
hash(bytes, 20)?.let {
return LazySodium.toHex(it)
}
return null
}
/**
* Hash a specific [bytes] array to an [outputLenght]
*/
fun hash(bytes: ByteArray, outputLenght: Int): ByteArray? {
val hash = sodiumProvider.randomBytesBuf(outputLenght)
val hashed = sodiumProvider.cryptoGenericHash(hash, hash.size, bytes, bytes.size.toLong())
return if (hashed) {
return hash
} else {
null
}
}
/**
* Sign the given [message] bytes with the given [secretKeyBytes].
*/
fun sign(message: ByteArray, secretKeyBytes: ByteArray): ByteArray? {
val signedMessage = sodiumProvider.randomBytesBuf(message.size + Sign.BYTES)
val signed = sodiumProvider.cryptoSign(signedMessage, message, message.size.toLong(), secretKeyBytes)
return if (signed) {
signedMessage
} else {
null
}
}
/**
* Verifies the detached signature of a message with the sender's public key.
*/
fun verify(message: ByteArray, publicKeyBytes: ByteArray, signature: ByteArray): Boolean {
return sodiumProvider.cryptoSignVerifyDetached(signature, message, message.size, publicKeyBytes)
}
}
......@@ -19,48 +19,49 @@
*/
package io.camlcase.javatezos.crypto.mnemonic
import org.bitcoinj.crypto.MnemonicCode
import org.bitcoinj.crypto.MnemonicException
import kotlin.random.Random
/**
* Wrapper for BIP39 tooling in Java/Kotlin. Supports English mnemonics only (for now).
*/
object Mnemonic {
/**
* Generate a mnemonic from the given hex string
*/
fun generateMnemonic(hexString: String): String? {
// TODO
return ""
}
/**
* Generate a mnemonic of the given strength in english
* Generate a mnemonic of the given strength in english.
* Process is heavyweight, recommended to launch this in a computation thread.
* @param strength This must be a multiple of 32.
* @return list of mnemonics, null if strenght is not valid
*/
fun generateMnemonic(strength: Int): String? {
if (strength % 32 != 0) {
return null
fun generateMnemonic(strength: Int): List<String>? {
MnemonicCode.INSTANCE = MnemonicCode(ENGLISH_MNEMONICS.toInputStream(), null)
val array = ByteArray(strength)
Random.Default.nextBytes(array)
return try {
MnemonicCode.INSTANCE.toMnemonic(array)
} catch (e: MnemonicException.MnemonicLengthException) {
null
}
val count = strength / 8
// TODO
return ""
}
/**
* Validate that the given string is a valid mnemonic.
* Validate that the given list of words is a valid mnemonic.
*/
fun validate(mnemonic: String): Boolean {
val sanitizedMnemonic: String = mnemonic.toLowerCase()
.trim()
.replace("\n", "")
val mnemonics = sanitizedMnemonic.split(" ")
if (mnemonics.isEmpty()) {
fun validate(mnemonic: List<String>): Boolean {
fun String.sanitize(): String {
return this.toLowerCase()
.trim()
.replace("\n", "")
}
if (mnemonic.isEmpty()) {
return false
}
if (ENGLISH_MNEMONICS.contains(mnemonics[0])) {
mnemonics.forEach {
if (!ENGLISH_MNEMONICS.contains(it)) {
if (ENGLISH_MNEMONICS.contains(mnemonic[0].sanitize())) {
mnemonic.forEach {
if (!ENGLISH_MNEMONICS.contains(it.sanitize())) {
return false
}
}
......@@ -68,4 +69,41 @@ object Mnemonic {
}
return false
}
/**
* Generate a seed from a given mnemonic.
* @param mnemonic A BIP39 mnemonic phrase.
* @param passphrase An optional passphrase used for encryption.
*/
fun seed(mnemonic: List<String>, passphrase: String = ""): ByteArray {
return MnemonicCode.toSeed(mnemonic, passphrase)
}
/**
* Generate a seed string from a given mnemonic.
* @param mnemonic A BIP39 mnemonic phrase.
* @param passphrase An optional passphrase used for encryption.
* @return 64 byte of seed hex string
*/
fun seedString(mnemonic: List<String>, passphrase: String = ""): String? {
return if (validate(mnemonic)) {
val seed: ByteArray = seed(mnemonic, passphrase)
seed.toHexString().substring(0, 64)
} else {
null
}
}
}
/**
* Convert the given input bytes to hex.
*/
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
/**
* Convert the given hex to binary.
*/
fun String.hexStringToByteArray() = ByteArray(this.length / 2) { this.substring(it * 2, it * 2 + 2).toInt(16).toByte() }
/**
* # Released under MIT License
*
* Copyright (c) 2019 camlCase
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
* the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
* THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE