Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions benchmark/src/main/scala/ApiBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bench

import java.util.concurrent.TimeUnit
import org.openjdk.jmh.annotations._
import cats.effect.unsafe.implicits.global

import com.ironcorelabs.recrypt

Expand Down
105 changes: 51 additions & 54 deletions benchmark/src/main/scala/Ed25519.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
*/
package bench

import org.abstractj.kalium.NaCl.Sodium.{ CRYPTO_SIGN_ED25519_PUBLICKEYBYTES, CRYPTO_SIGN_ED25519_BYTES, CRYPTO_SIGN_ED25519_SECRETKEYBYTES } //scalastyle:ignore
import org.abstractj.kalium.NaCl.sodium
import org.bouncycastle.crypto.params.{ Ed25519PrivateKeyParameters, Ed25519PublicKeyParameters }
import org.bouncycastle.crypto.signers.Ed25519Signer
import scodec.bits.ByteVector
import jnr.ffi.byref.LongLongByReference

/**
* Functions and types for working with the ed25519 signature.
Expand All @@ -19,23 +18,20 @@ object Ed25519 { //scalastyle:ignore
override def toString: String = s"Ed25519.PublicKey as Base64: '${bytes.toBase64}'"
}
final object PublicKey {
final val Length = CRYPTO_SIGN_ED25519_PUBLICKEYBYTES
final val Length = 32
final val LengthLong = Length.toLong
private[Ed25519] def create(bytes: ByteVector): PublicKey = new PublicKey(bytes) {}

def unsafeFromBytes(b: ByteVector): PublicKey =
fromBytes(b).getOrElse(throw new Exception(s"'$b' did not have $Length length."))

/**
* Verifies the length of the byte vector satisfies the invariant detailed in NaCl
*/
def fromBytes(bytes: ByteVector): Option[PublicKey] = Some(bytes)
.filter(_.length == Length)
.map(create)

/**
* Create a value safely by just taking and padding bytes. This isn't ideal, but because of the
* interaction with recrypt and lack of sizing guarentees (a la refined) it's needed.
* Create a value by truncating-or-left-padding to the required length. This isn't ideal, but
* because of the interaction with recrypt and lack of sizing guarantees (a la refined) it's needed.
*/
def fromPaddedBytes(bytes: ByteVector): PublicKey =
create(bytes
Expand All @@ -44,100 +40,101 @@ object Ed25519 { //scalastyle:ignore
}

/**
* An Ed25519 Private Key, which always has the correct length, can be used in `verify`
* An Ed25519 Private Key, which always has the correct length, can be used in `verify`.
*
* WARNING: bytes are interpreted in the NaCl layout `seed (32) || publicKey (32)`. The
* BouncyCastle backend only consumes the first 32 bytes (the seed); the trailing 32 bytes
* are carried for byte-compat with the prior kalium implementation and the recrypt
* `PrivateSigningKey` round-trip. If you construct a `PrivateKey` from bytes that do NOT
* follow this layout, `sign` will produce signatures from whatever 32-byte prefix is present
* and `verify` against the corresponding `PublicKey` will silently fail. Prefer
* `generateKeyPair` to obtain a well-formed key.
*/
sealed abstract case class PrivateKey(bytes: ByteVector) {
//A protection against logging Private keys.
override def toString: String = s"Ed25519.PrivateKey(<BYTES>)"

/** The 32-byte Ed25519 seed (first half of the NaCl layout). */
def seed: ByteVector = bytes.take(PrivateKey.SeedLength.toLong)

/** The 32-byte public key embedded in this private key (second half of the NaCl layout). */
def embeddedPublicKey: ByteVector = bytes.drop(PrivateKey.SeedLength.toLong)
}
final object PrivateKey {
final val Length = CRYPTO_SIGN_ED25519_SECRETKEYBYTES
/** Length of the Ed25519 seed in bytes. */
final val SeedLength = 32
/** Total length of a NaCl-style Ed25519 private key: seed (32) || public key (32). */
final val Length = SeedLength + PublicKey.Length
final val LengthLong = Length.toLong
private[Ed25519] def create(bytes: ByteVector): PrivateKey = new PrivateKey(bytes) {}

/**
* Verifies the length of the byte vector satisfies the invariant detailed in NaCl.
*/
def fromBytes(bytes: ByteVector): Option[PrivateKey] = Some(bytes)
.filter(_.length == Length)
.map(create)

/**
* Create a value safely by just taking and padding bytes. This isn't ideal, but because of the
* interaction with recrypt and lack of sizing guarentees (a la refined) it's needed.
* Create a value by truncating-or-left-padding to the required length. This isn't ideal, but
* because of the interaction with recrypt and lack of sizing guarantees (a la refined) it's needed.
*/
def fromPaddedBytes(bytes: ByteVector): PrivateKey =
create(bytes.take(LengthLong).padLeft(LengthLong))
}

/**
* An Ed25519 signature, which always has the correct length, can be checked via `verify`
*/
sealed abstract case class Signature(bytes: ByteVector) {
override def toString: String = s"Signature in Base64: '${bytes.toBase64}'"
}
final object Signature {
val Length = CRYPTO_SIGN_ED25519_BYTES
val Length = 64
val LengthLong = Length.toLong
private[Ed25519] def create(bytes: ByteVector): Signature = new Signature(bytes) {}

def unsafeFromBytes(b: ByteVector): Signature =
fromBytes(b).getOrElse(throw new Exception(s"'$b' did not have $Length length."))

/**
* Verifies the length of the byte vector satisfies the invariant detailed in NaCl
*/
def fromBytes(bytes: ByteVector): Option[Signature] = Some(bytes)
.filter(_.length == Length)
.map(create)

/**
* Create a value safely by just taking and padding bytes. This isn't ideal, but because of the
* interaction with recrypt and lack of sizing guarentees (a la refined) it's needed.
* Create a value by truncating-or-left-padding to the required length. This isn't ideal, but
* because of the interaction with recrypt and lack of sizing guarantees (a la refined) it's needed.
*/
def fromPaddedBytes(bytes: ByteVector): Signature =
create(bytes
.take(LengthLong)
.padLeft(LengthLong))

}

def verify(key: PublicKey, message: ByteVector, signature: Signature): Boolean = {
val mergedBytes = signature.bytes ++ message
val mergedByteArray = mergedBytes.toArray
val sodiumResult = sodium.crypto_sign_ed25519_open(
arrayOfZeros(mergedByteArray.length), //Out buffer which we don't need the result of
new LongLongByReference(0), // Out buffer length which we don't need the result of
mergedByteArray, //signature + message bytes
mergedByteArray.length, // signature + message bytes length
key.bytes.toArray //public key bytes
)
isValid(sodiumResult)
val verifier = new Ed25519Signer()
val pubKeyParams = new Ed25519PublicKeyParameters(key.bytes.toArray, 0)
verifier.init(false, pubKeyParams)
val msgBytes = message.toArray
verifier.update(msgBytes, 0, msgBytes.length)
verifier.verifySignature(signature.bytes.toArray)
}

def sign(key: PrivateKey, message: ByteVector): Signature = {
val messageLength = message.length.toInt
val privateKeyBytes = key.bytes.toArray

val outputByteArray = arrayOfZeros(messageLength + Signature.Length)
sodium.crypto_sign_ed25519(
outputByteArray,
new LongLongByReference(0), //Don't care.
message.toArray,
messageLength,
privateKeyBytes
)
Signature.create(ByteVector.view(outputByteArray).take(Signature.LengthLong))
// BouncyCastle's Ed25519 takes only the 32-byte seed. We pull it from the NaCl-layout
// private key via `key.seed`; see the WARNING on `PrivateKey` for what happens if the
// caller built one from non-NaCl-format bytes.
val signer = new Ed25519Signer()
val privKeyParams = new Ed25519PrivateKeyParameters(key.seed.toArray, 0)
signer.init(true, privKeyParams)
val msgBytes = message.toArray
signer.update(msgBytes, 0, msgBytes.length)
Signature.create(ByteVector.view(signer.generateSignature()))
}

def generateKeyPair(seed: ByteVector): (PublicKey, PrivateKey) = {
//Allocate the arrays, which will get filled out by crypto_sign_ed25519_seed_keypair
val privateKeyArray = arrayOfZeros(PrivateKey.Length)
val publicKeyArray = arrayOfZeros(PublicKey.Length)
sodium.crypto_sign_ed25519_seed_keypair(publicKeyArray, privateKeyArray, seed.toArray)
PublicKey.create(ByteVector.view(publicKeyArray)) -> PrivateKey.create(ByteVector.view(privateKeyArray))
val privKeyParams = new Ed25519PrivateKeyParameters(seed.toArray, 0)
val pubKeyParams = privKeyParams.generatePublicKey()
val publicKeyBytes = ByteVector.view(pubKeyParams.getEncoded)
// Reconstruct the NaCl-style 64-byte private key: seed || public key.
val privateKeyBytes = seed ++ publicKeyBytes
PublicKey.create(publicKeyBytes) -> PrivateKey.create(privateKeyBytes)
}

final private def isValid(code: Int): Boolean = code == 0

final private def arrayOfZeros(n: Int): Array[Byte] = new Array[Byte](n)
}
1 change: 1 addition & 0 deletions benchmark/src/main/scala/GenerateFp12Benchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.openjdk.jmh.annotations._
import com.ironcorelabs.recrypt.internal._
import cats.instances.list._
import cats.syntax.traverse._
import cats.effect.unsafe.implicits.global

@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.SECONDS)
Expand Down
1 change: 1 addition & 0 deletions benchmark/src/main/scala/HomogeneousPointBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bench

import java.util.concurrent.TimeUnit
import org.openjdk.jmh.annotations._
import cats.effect.unsafe.implicits.global

import com.ironcorelabs.recrypt.internal._
import point.HomogeneousPoint
Expand Down
1 change: 1 addition & 0 deletions benchmark/src/main/scala/InternalEncryptBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.openjdk.jmh.annotations._

import com.ironcorelabs.recrypt.internal._
import scodec.bits.ByteVector
import cats.effect.unsafe.implicits.global

@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.SECONDS)
Expand Down
82 changes: 82 additions & 0 deletions benchmark/src/test/scala/Ed25519Test.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2017-present IronCore Labs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package bench

import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scodec.bits.ByteVector

class Ed25519Test extends AnyWordSpec with Matchers {
// A fixed 32-byte seed so failures are reproducible.
private val seed = ByteVector.fromValidHex(
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
)
private val message = ByteVector.encodeUtf8("recrypt benchmark sanity check").toOption.get

"Ed25519.generateKeyPair" should {
"produce a 32-byte public key and a 64-byte NaCl-layout private key" in {
val (pub, priv) = Ed25519.generateKeyPair(seed)
pub.bytes.length shouldBe Ed25519.PublicKey.Length
priv.bytes.length shouldBe Ed25519.PrivateKey.Length
}

"embed the public key as the second half of the private key" in {
val (pub, priv) = Ed25519.generateKeyPair(seed)
priv.seed shouldBe seed
priv.embeddedPublicKey shouldBe pub.bytes
}

"be deterministic for a given seed" in {
val a = Ed25519.generateKeyPair(seed)
val b = Ed25519.generateKeyPair(seed)
a._1.bytes shouldBe b._1.bytes
a._2.bytes shouldBe b._2.bytes
}
}

"Ed25519.sign / verify" should {
"round-trip a signature produced from a freshly generated key" in {
val (pub, priv) = Ed25519.generateKeyPair(seed)
val sig = Ed25519.sign(priv, message)
sig.bytes.length shouldBe Ed25519.Signature.Length
Ed25519.verify(pub, message, sig) shouldBe true
}

"reject a signature whose bytes have been tampered with" in {
val (pub, priv) = Ed25519.generateKeyPair(seed)
val sig = Ed25519.sign(priv, message)
val tampered = Ed25519.Signature.unsafeFromBytes(sig.bytes.update(0, (sig.bytes(0) ^ 0x01).toByte))
Ed25519.verify(pub, message, tampered) shouldBe false
}

"reject a valid signature against a different message" in {
val (pub, priv) = Ed25519.generateKeyPair(seed)
val sig = Ed25519.sign(priv, message)
val otherMessage = message ++ ByteVector(0x00.toByte)
Ed25519.verify(pub, otherMessage, sig) shouldBe false
}

"reject a valid signature against an unrelated public key" in {
val (_, priv) = Ed25519.generateKeyPair(seed)
val otherSeed = ByteVector.fill(Ed25519.PrivateKey.SeedLength.toLong)(0x42.toByte)
val (otherPub, _) = Ed25519.generateKeyPair(otherSeed)
val sig = Ed25519.sign(priv, message)
Ed25519.verify(otherPub, message, sig) shouldBe false
}
}
}
6 changes: 3 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ lazy val benchmark = project.in(file("benchmark"))
.settings(recryptSettings: _*)
.settings(coverageEnabled := false)
.settings(noPublish: _*)
.settings( libraryDependencies ++= Seq(
"org.abstractj.kalium" % "kalium" % "0.8.0"
))
.settings(libraryDependencies ++= Seq(
"org.bouncycastle" % "bcprov-jdk18on" % "1.84"
))
.enablePlugins(JmhPlugin)
Loading