CryptoRuntime abstraction (#2658)

* Add CryptoRuntime, extend it with CryptoUtil

* Remove direct usages of CryptoUtil in the core project, use CryptoTrait.cryptoRuntime

* Add JvmCryptoRuntime

* Take ben's suggestion so we don't need to modify anyting in core, h/t to ben

* Refactor ECPrivateKey.freshPrivateKey to use CryptoUtil.freshPrivateKey

* Remove CryptoTrait as it is no longer necessary
This commit is contained in:
Chris Stewart 2021-02-12 15:18:42 -06:00 committed by GitHub
parent bcade41326
commit a1bdbda039
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 324 additions and 184 deletions

View file

@ -24,4 +24,12 @@ object CryptoContext {
}
}
}
/** The platform specific cryptographic functions required to run bitcoin-s */
lazy val cryptoRuntime: CryptoRuntime = {
default match {
case LibSecp256k1 => JvmCryptoRuntime
case BouncyCastle => JvmCryptoRuntime
}
}
}

View file

@ -0,0 +1,139 @@
package org.bitcoins.crypto
import org.bouncycastle.math.ec.ECPoint
import scodec.bits.{BitVector, ByteVector}
import java.math.BigInteger
/** Trait that should be extended by specific runtimes like javascript
* or the JVM to support crypto functions needed for bitcoin-s
*/
trait CryptoRuntime {
/** Generates a 32 byte private key */
def freshPrivateKey: ECPrivateKey
/** Converts a private key -> public key
* @param privateKey the private key we want the corresponding public key for
* @param isCompressed whether the returned public key should be compressed or not
*/
def toPublicKey(privateKey: ECPrivateKey, isCompressed: Boolean): ECPublicKey
def ripeMd160(bytes: ByteVector): RipeMd160Digest
def sha256Hash160(bytes: ByteVector): Sha256Hash160Digest
def sha256(bytes: ByteVector): Sha256Digest
def sha256(str: String): Sha256Digest = {
sha256(serializeForHash(str))
}
def sha256(bitVector: BitVector): Sha256Digest = {
sha256(bitVector.toByteVector)
}
def taggedSha256(bytes: ByteVector, tag: String): Sha256Digest = {
val tagHash = sha256(tag)
val tagBytes = tagHash.bytes ++ tagHash.bytes
sha256(tagBytes ++ bytes)
}
/** Performs sha256(sha256(bytes)). */
def doubleSHA256(bytes: ByteVector): DoubleSha256Digest = {
val hash: ByteVector = sha256(sha256(bytes).bytes).bytes
DoubleSha256Digest(hash)
}
def sha1(bytes: ByteVector): Sha1Digest
def hmac512(key: ByteVector, data: ByteVector): ByteVector
def normalize(str: String): String
def serializeForHash(str: String): ByteVector = {
ByteVector(normalize(str).getBytes("UTF-8"))
}
// The tag "BIP0340/challenge"
private lazy val schnorrChallengeTagBytes = {
ByteVector
.fromValidHex(
"7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c"
)
}
// The tag "DLC/oracle/attestation/v0"
private val dlcAttestationTagBytes = {
ByteVector
.fromValidHex(
"0c2fa46216e6e460e5e3f78555b102c5ac6aecabbfb82b430cf36cdfe04421790c2fa46216e6e460e5e3f78555b102c5ac6aecabbfb82b430cf36cdfe0442179"
)
}
def sha256SchnorrChallenge(bytes: ByteVector): Sha256Digest = {
sha256(schnorrChallengeTagBytes ++ bytes)
}
def sha256DLCAttestation(bytes: ByteVector): Sha256Digest = {
sha256(dlcAttestationTagBytes ++ bytes)
}
def sha256DLCAttestation(str: String): Sha256Digest = {
sha256DLCAttestation(CryptoUtil.serializeForHash(str))
}
// The tag "DLC/oracle/announcement/v0"
private val dlcAnnouncementTagBytes = {
ByteVector
.fromValidHex(
"6378871e8c99d480fff016e178a371e7e058445eff3023fe158f05aa185ed0e16378871e8c99d480fff016e178a371e7e058445eff3023fe158f05aa185ed0e1"
)
}
def sha256DLCAnnouncement(bytes: ByteVector): Sha256Digest = {
sha256(dlcAnnouncementTagBytes ++ bytes)
}
/** @param x x coordinate
* @return a tuple (p1, p2) where p1 and p2 are points on the curve and p1.x = p2.x = x
* p1.y is even, p2.y is odd
*/
def recoverPoint(x: BigInteger): (ECPoint, ECPoint)
/** Recover public keys from a signature and the message that was signed. This method will return 2 public keys, and the signature
* can be verified with both, but only one of them matches that private key that was used to generate the signature.
*
* @param signature signature
* @param message message that was signed
* @return a (pub1, pub2) tuple where pub1 and pub2 are candidates public keys. If you have the recovery id then use
* pub1 if the recovery id is even and pub2 if it is odd
*/
def recoverPublicKey(
signature: ECDigitalSignature,
message: ByteVector): (ECPublicKey, ECPublicKey)
// The tag "BIP0340/aux"
private val schnorrAuxTagBytes = {
ByteVector
.fromValidHex(
"f1ef4e5ec063cada6d94cafa9d987ea069265839ecc11f972d77a52ed8c1cc90f1ef4e5ec063cada6d94cafa9d987ea069265839ecc11f972d77a52ed8c1cc90"
)
}
def sha256SchnorrAuxRand(bytes: ByteVector): Sha256Digest = {
sha256(schnorrAuxTagBytes ++ bytes)
}
// The tag "BIP0340/nonce"
private val schnorrNonceTagBytes = {
ByteVector
.fromValidHex(
"07497734a79bcb355b9b8c7d034f121cf434d73ef72dda19870061fb52bfeb2f07497734a79bcb355b9b8c7d034f121cf434d73ef72dda19870061fb52bfeb2f"
)
}
def sha256SchnorrNonce(bytes: ByteVector): Sha256Digest = {
sha256(schnorrNonceTagBytes ++ bytes)
}
}

View file

@ -1,40 +1,43 @@
package org.bitcoins.crypto
import java.math.BigInteger
import java.security.MessageDigest
import org.bouncycastle.crypto.digests.{RIPEMD160Digest, SHA512Digest}
import org.bouncycastle.crypto.macs.HMac
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.math.ec.ECPoint
import scodec.bits.{BitVector, ByteVector}
import scodec.bits.ByteVector
import java.math.BigInteger
/** Utility cryptographic functions
* This is a proxy for the underlying implementation of [[CryptoRuntime]]
* such as [[JvmCryptoRuntime]].
*
* This is necessary so that the core module doesn't need to be refactored
* to add support for multiple platforms, it can keep referencing CryptoUtil
*/
trait CryptoUtil {
trait CryptoUtil extends CryptoRuntime {
def normalize(str: String): String = {
java.text.Normalizer.normalize(str, java.text.Normalizer.Form.NFC)
/** The underlying runtime for the specific platform we are running on */
private lazy val cryptoRuntime: CryptoRuntime = CryptoContext.cryptoRuntime
override def freshPrivateKey: ECPrivateKey = {
cryptoRuntime.freshPrivateKey
}
def serializeForHash(str: String): ByteVector = {
ByteVector(normalize(str).getBytes("UTF-8"))
override def toPublicKey(
privateKey: ECPrivateKey,
isCompressed: Boolean): ECPublicKey = {
cryptoRuntime.toPublicKey(privateKey, isCompressed)
}
override def normalize(str: String): String = {
cryptoRuntime.normalize(str)
}
/** Does the following computation: RIPEMD160(SHA256(hex)). */
def sha256Hash160(bytes: ByteVector): Sha256Hash160Digest = {
val hash = ripeMd160(sha256(bytes).bytes).bytes
Sha256Hash160Digest(hash)
override def sha256Hash160(bytes: ByteVector): Sha256Hash160Digest = {
cryptoRuntime.sha256Hash160(bytes)
}
def sha256Hash160(str: String): Sha256Hash160Digest = {
sha256Hash160(serializeForHash(str))
}
/** Performs sha256(sha256(bytes)). */
def doubleSHA256(bytes: ByteVector): DoubleSha256Digest = {
val hash: ByteVector = sha256(sha256(bytes).bytes).bytes
DoubleSha256Digest(hash)
cryptoRuntime.sha256Hash160(serializeForHash(str))
}
def doubleSHA256(str: String): DoubleSha256Digest = {
@ -42,98 +45,17 @@ trait CryptoUtil {
}
/** Takes sha256(bytes). */
def sha256(bytes: ByteVector): Sha256Digest = {
val hash = MessageDigest.getInstance("SHA-256").digest(bytes.toArray)
Sha256Digest(ByteVector(hash))
}
/** Takes sha256(bits). */
def sha256(bits: BitVector): Sha256Digest = {
sha256(bits.toByteVector)
}
def sha256(str: String): Sha256Digest = {
sha256(serializeForHash(str))
}
def taggedSha256(bytes: ByteVector, tag: String): Sha256Digest = {
val tagHash = sha256(tag)
val tagBytes = tagHash.bytes ++ tagHash.bytes
sha256(tagBytes ++ bytes)
override def sha256(bytes: ByteVector): Sha256Digest = {
cryptoRuntime.sha256(bytes)
}
def taggedSha256(str: String, tag: String): Sha256Digest = {
taggedSha256(serializeForHash(str), tag)
}
// The tag "BIP0340/challenge"
private val schnorrChallengeTagBytes = {
ByteVector
.fromValidHex(
"7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c7bb52d7a9fef58323eb1bf7a407db382d2f3f2d81bb1224f49fe518f6d48d37c"
)
}
def sha256SchnorrChallenge(bytes: ByteVector): Sha256Digest = {
sha256(schnorrChallengeTagBytes ++ bytes)
}
// The tag "BIP0340/nonce"
private val schnorrNonceTagBytes = {
ByteVector
.fromValidHex(
"07497734a79bcb355b9b8c7d034f121cf434d73ef72dda19870061fb52bfeb2f07497734a79bcb355b9b8c7d034f121cf434d73ef72dda19870061fb52bfeb2f"
)
}
def sha256SchnorrNonce(bytes: ByteVector): Sha256Digest = {
sha256(schnorrNonceTagBytes ++ bytes)
}
// The tag "BIP0340/aux"
private val schnorrAuxTagBytes = {
ByteVector
.fromValidHex(
"f1ef4e5ec063cada6d94cafa9d987ea069265839ecc11f972d77a52ed8c1cc90f1ef4e5ec063cada6d94cafa9d987ea069265839ecc11f972d77a52ed8c1cc90"
)
}
def sha256SchnorrAuxRand(bytes: ByteVector): Sha256Digest = {
sha256(schnorrAuxTagBytes ++ bytes)
}
// The tag "DLC/oracle/attestation/v0"
private val dlcAttestationTagBytes = {
ByteVector
.fromValidHex(
"0c2fa46216e6e460e5e3f78555b102c5ac6aecabbfb82b430cf36cdfe04421790c2fa46216e6e460e5e3f78555b102c5ac6aecabbfb82b430cf36cdfe0442179"
)
}
def sha256DLCAttestation(bytes: ByteVector): Sha256Digest = {
sha256(dlcAttestationTagBytes ++ bytes)
}
def sha256DLCAttestation(str: String): Sha256Digest = {
sha256DLCAttestation(CryptoUtil.serializeForHash(str))
}
// The tag "DLC/oracle/announcement/v0"
private val dlcAnnouncementTagBytes = {
ByteVector
.fromValidHex(
"6378871e8c99d480fff016e178a371e7e058445eff3023fe158f05aa185ed0e16378871e8c99d480fff016e178a371e7e058445eff3023fe158f05aa185ed0e1"
)
}
def sha256DLCAnnouncement(bytes: ByteVector): Sha256Digest = {
sha256(dlcAnnouncementTagBytes ++ bytes)
}
/** Performs SHA1(bytes). */
def sha1(bytes: ByteVector): Sha1Digest = {
val hash = MessageDigest.getInstance("SHA-1").digest(bytes.toArray).toList
Sha1Digest(ByteVector(hash))
override def sha1(bytes: ByteVector): Sha1Digest = {
cryptoRuntime.sha1(bytes)
}
def sha1(str: String): Sha1Digest = {
@ -141,14 +63,8 @@ trait CryptoUtil {
}
/** Performs RIPEMD160(bytes). */
def ripeMd160(bytes: ByteVector): RipeMd160Digest = {
//from this tutorial http://rosettacode.org/wiki/RIPEMD-160#Scala
val messageDigest = new RIPEMD160Digest
val raw = bytes.toArray
messageDigest.update(raw, 0, raw.length)
val out = Array.fill[Byte](messageDigest.getDigestSize)(0)
messageDigest.doFinal(out, 0)
RipeMd160Digest(ByteVector(out))
override def ripeMd160(bytes: ByteVector): RipeMd160Digest = {
cryptoRuntime.ripeMd160(bytes)
}
def ripeMd160(str: String): RipeMd160Digest = {
@ -157,13 +73,8 @@ trait CryptoUtil {
/** Calculates `HMAC-SHA512(key, data)`
*/
def hmac512(key: ByteVector, data: ByteVector): ByteVector = {
val hmac512 = new HMac(new SHA512Digest())
hmac512.init(new KeyParameter(key.toArray))
hmac512.update(data.toArray, 0, data.intSize.get)
val output = new Array[Byte](64)
hmac512.doFinal(output, 0)
ByteVector(output)
override def hmac512(key: ByteVector, data: ByteVector): ByteVector = {
cryptoRuntime.hmac512(key, data)
}
/** @param x x coordinate
@ -171,54 +82,13 @@ trait CryptoUtil {
* p1.y is even, p2.y is odd
*/
def recoverPoint(x: BigInteger): (ECPoint, ECPoint) = {
val bytes = ByteVector(x.toByteArray)
val bytes32 = if (bytes.length < 32) {
bytes.padLeft(32)
} else if (bytes.length == 32) {
bytes
} else if (bytes.length == 33 && bytes.head == 0.toByte) {
bytes.tail
} else {
throw new IllegalArgumentException(
s"Field element cannot have more than 32 bytes, got $bytes from $x")
}
(ECPublicKey(0x02.toByte +: bytes32).toPoint,
ECPublicKey(0x03.toByte +: bytes32).toPoint)
cryptoRuntime.recoverPoint(x)
}
/** Recover public keys from a signature and the message that was signed. This method will return 2 public keys, and the signature
* can be verified with both, but only one of them matches that private key that was used to generate the signature.
*
* @param signature signature
* @param message message that was signed
* @return a (pub1, pub2) tuple where pub1 and pub2 are candidates public keys. If you have the recovery id then use
* pub1 if the recovery id is even and pub2 if it is odd
*/
def recoverPublicKey(
override def recoverPublicKey(
signature: ECDigitalSignature,
message: ByteVector): (ECPublicKey, ECPublicKey) = {
val curve = CryptoParams.curve
val (r, s) = (signature.r.bigInteger, signature.s.bigInteger)
val m = new BigInteger(1, message.toArray)
val (p1, p2) = recoverPoint(r)
val Q1 = p1
.multiply(s)
.subtract(curve.getG.multiply(m))
.multiply(r.modInverse(curve.getN))
val Q2 = p2
.multiply(s)
.subtract(curve.getG.multiply(m))
.multiply(r.modInverse(curve.getN))
val pub1 = ECPublicKey.fromPoint(Q1)
val pub2 = ECPublicKey.fromPoint(Q2)
(pub1, pub2)
cryptoRuntime.recoverPublicKey(signature, message)
}
}

View file

@ -1,15 +1,8 @@
package org.bitcoins.crypto
import java.math.BigInteger
import java.security.SecureRandom
import org.bitcoin.NativeSecp256k1
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
import org.bouncycastle.crypto.generators.ECKeyPairGenerator
import org.bouncycastle.crypto.params.{
ECKeyGenerationParameters,
ECPrivateKeyParameters
}
import org.bouncycastle.math.ec.ECPoint
import scodec.bits.ByteVector
@ -358,17 +351,8 @@ object ECPrivateKey extends Factory[ECPrivateKey] {
def freshPrivateKey: ECPrivateKey = freshPrivateKey(true)
def freshPrivateKey(isCompressed: Boolean): ECPrivateKey = {
val secureRandom = new SecureRandom
val generator: ECKeyPairGenerator = new ECKeyPairGenerator
val keyGenParams: ECKeyGenerationParameters =
new ECKeyGenerationParameters(CryptoParams.curve, secureRandom)
generator.init(keyGenParams)
val keypair: AsymmetricCipherKeyPair = generator.generateKeyPair
val privParams: ECPrivateKeyParameters =
keypair.getPrivate.asInstanceOf[ECPrivateKeyParameters]
val priv: BigInteger = privParams.getD
val bytes = ByteVector(priv.toByteArray)
ECPrivateKey.fromBytes(bytes, isCompressed)
val priv = CryptoUtil.freshPrivateKey
ECPrivateKey.fromBytes(priv.bytes, isCompressed)
}
}

View file

@ -0,0 +1,139 @@
package org.bitcoins.crypto
import org.bitcoin.NativeSecp256k1
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
import org.bouncycastle.crypto.digests.{RIPEMD160Digest, SHA512Digest}
import org.bouncycastle.crypto.generators.ECKeyPairGenerator
import org.bouncycastle.crypto.macs.HMac
import org.bouncycastle.crypto.params.{
ECKeyGenerationParameters,
ECPrivateKeyParameters,
KeyParameter
}
import org.bouncycastle.math.ec.ECPoint
import scodec.bits.ByteVector
import java.math.BigInteger
import java.security.{MessageDigest, SecureRandom}
trait JvmCryptoRuntime extends CryptoRuntime {
private[this] lazy val secureRandom = new SecureRandom()
override def freshPrivateKey: ECPrivateKey = {
val generator: ECKeyPairGenerator = new ECKeyPairGenerator
val keyGenParams: ECKeyGenerationParameters =
new ECKeyGenerationParameters(CryptoParams.curve, secureRandom)
generator.init(keyGenParams)
val keypair: AsymmetricCipherKeyPair = generator.generateKeyPair
val privParams: ECPrivateKeyParameters =
keypair.getPrivate.asInstanceOf[ECPrivateKeyParameters]
val priv: BigInteger = privParams.getD
val bytes = ByteVector(priv.toByteArray)
ECPrivateKey.fromBytes(bytes)
}
/** @param x x coordinate
* @return a tuple (p1, p2) where p1 and p2 are points on the curve and p1.x = p2.x = x
* p1.y is even, p2.y is odd
*/
override def recoverPoint(x: BigInteger): (ECPoint, ECPoint) = {
val bytes = ByteVector(x.toByteArray)
val bytes32 = if (bytes.length < 32) {
bytes.padLeft(32)
} else if (bytes.length == 32) {
bytes
} else if (bytes.length == 33 && bytes.head == 0.toByte) {
bytes.tail
} else {
throw new IllegalArgumentException(
s"Field element cannot have more than 32 bytes, got $bytes from $x")
}
(ECPublicKey(0x02.toByte +: bytes32).toPoint,
ECPublicKey(0x03.toByte +: bytes32).toPoint)
}
override def recoverPublicKey(
signature: ECDigitalSignature,
message: ByteVector): (ECPublicKey, ECPublicKey) = {
val curve = CryptoParams.curve
val (r, s) = (signature.r.bigInteger, signature.s.bigInteger)
val m = new BigInteger(1, message.toArray)
val (p1, p2) = recoverPoint(r)
val Q1 = p1
.multiply(s)
.subtract(curve.getG.multiply(m))
.multiply(r.modInverse(curve.getN))
val Q2 = p2
.multiply(s)
.subtract(curve.getG.multiply(m))
.multiply(r.modInverse(curve.getN))
val pub1 = ECPublicKey.fromPoint(Q1)
val pub2 = ECPublicKey.fromPoint(Q2)
(pub1, pub2)
}
override def hmac512(key: ByteVector, data: ByteVector): ByteVector = {
val hmac512 = new HMac(new SHA512Digest())
hmac512.init(new KeyParameter(key.toArray))
hmac512.update(data.toArray, 0, data.intSize.get)
val output = new Array[Byte](64)
hmac512.doFinal(output, 0)
ByteVector(output)
}
override def ripeMd160(bytes: ByteVector): RipeMd160Digest = {
//from this tutorial http://rosettacode.org/wiki/RIPEMD-160#Scala
val messageDigest = new RIPEMD160Digest
val raw = bytes.toArray
messageDigest.update(raw, 0, raw.length)
val out = Array.fill[Byte](messageDigest.getDigestSize)(0)
messageDigest.doFinal(out, 0)
RipeMd160Digest(ByteVector(out))
}
override def sha256(bytes: ByteVector): Sha256Digest = {
val hash = MessageDigest.getInstance("SHA-256").digest(bytes.toArray)
Sha256Digest(ByteVector(hash))
}
override def sha1(bytes: ByteVector): Sha1Digest = {
val hash = MessageDigest.getInstance("SHA-1").digest(bytes.toArray).toList
Sha1Digest(ByteVector(hash))
}
override def normalize(str: String): String = {
java.text.Normalizer.normalize(str, java.text.Normalizer.Form.NFC)
}
override def sha256Hash160(bytes: ByteVector): Sha256Hash160Digest = {
val hash = ripeMd160(sha256(bytes).bytes).bytes
Sha256Hash160Digest(hash)
}
override def toPublicKey(
privateKey: ECPrivateKey,
isCompressed: Boolean): ECPublicKey = {
CryptoContext.default match {
case CryptoContext.BouncyCastle =>
BouncyCastleUtil.computePublicKey(privateKey)
case CryptoContext.LibSecp256k1 =>
val pubKeyBytes: Array[Byte] =
NativeSecp256k1.computePubkey(privateKey.bytes.toArray, isCompressed)
val pubBytes = ByteVector(pubKeyBytes)
require(
ECPublicKey.isFullyValid(pubBytes),
s"secp256k1 failed to generate a valid public key, got: ${CryptoBytesUtil
.encodeHex(pubBytes)}")
ECPublicKey(pubBytes)
}
}
}
object JvmCryptoRuntime extends JvmCryptoRuntime