Add toBase64/fromBase64 to AesEncryptedData

This commit is contained in:
Torkel Rogstad 2019-06-25 11:38:20 +02:00
parent 77d056e5fa
commit 161db9ff92
3 changed files with 89 additions and 16 deletions

View File

@ -59,13 +59,14 @@ class AesCryptTest extends BitcoinSUnitTest {
case class TestVector(
plainText: ByteVector,
key: AesKey,
cipherText: AesEncryptedData)
cipherText: AesEncryptedData
)
def runTest(testVector: TestVector): Assertion = {
val TestVector(plainText, key, cipherText) =
testVector
val Right(encrypted) =
val encrypted =
AesCrypt.encryptWithIV(plainText, cipherText.iv, key)
assert(encrypted == cipherText)
@ -88,9 +89,10 @@ class AesCryptTest extends BitcoinSUnitTest {
val second = TestVector(
plainText = hex"3a4f73044d035017d91883ebfc113da7",
key = getKey(hex"5ce91f97ed28fd5d1172e23eb17b1baa"),
cipherText =
AesEncryptedData(cipherText = hex"f0ff04edc644388edb872237ac558367",
iv = getIV(hex"3f91d29f81d48174b25a3d0143eb833c"))
cipherText = AesEncryptedData(
cipherText = hex"f0ff04edc644388edb872237ac558367",
iv = getIV(hex"3f91d29f81d48174b25a3d0143eb833c")
)
)
runTest(second)
@ -118,7 +120,7 @@ class AesCryptTest extends BitcoinSUnitTest {
val iv = getIV(hex"455014871CD34F8DCFD7C1E387987BFF")
val expectedCipher = ByteVector.fromValidBase64("oE8HErg1lg==")
val Right(encrypted) = AesCrypt.encryptWithIV(plainbytes, iv, key)
val encrypted = AesCrypt.encryptWithIV(plainbytes, iv, key)
// for some reason we end up with different cipher texts
// decrypting works though...
@ -154,14 +156,15 @@ class AesCryptTest extends BitcoinSUnitTest {
val key = getKey(hex"12345678123456781234567812345678")
val iv = getIV(hex"87654321876543218765432187654321")
val expectedCipher = ByteVector.fromValidBase64(
"KKbLXDQUy7ajmuIJm7ZR7ugaRubqGl1JwG+x5C451JXIFofnselHVTy/u8u0Or9nV2d7Kjy0")
"KKbLXDQUy7ajmuIJm7ZR7ugaRubqGl1JwG+x5C451JXIFofnselHVTy/u8u0Or9nV2d7Kjy0"
)
val plaintext = "The quick brown fox jumps over the lazy dog. 👻 👻"
val Right(plainbytes) = ByteVector.encodeUtf8(plaintext)
// decrypt our own encrypted data
{
val Right(encrypted) = AesCrypt.encryptWithIV(plainbytes, iv, key)
val encrypted = AesCrypt.encryptWithIV(plainbytes, iv, key)
assert(encrypted.iv == iv)
assert(encrypted.cipherText == expectedCipher)
@ -274,7 +277,7 @@ class AesCryptTest extends BitcoinSUnitTest {
// test encrypting and decrypting ourselves
{
val Right(encrypted) = AesCrypt.encryptWithIV(plainbytes, iv, key)
val encrypted = AesCrypt.encryptWithIV(plainbytes, iv, key)
// assert(encrypted.cipherText == expectedCipher)
assert(encrypted.iv == iv)
@ -301,7 +304,7 @@ class AesCryptTest extends BitcoinSUnitTest {
it must "have encryption and decryption symmetry" in {
forAll(NumberGenerator.bytevector, CryptoGenerators.aesKey) {
(bytes, key) =>
val Right(encrypted) = AesCrypt.encrypt(bytes, key)
val encrypted = AesCrypt.encrypt(bytes, key)
AesCrypt.decrypt(encrypted, key) match {
case Right(decrypted) => assert(decrypted == bytes)
case Left(exc) => fail(exc)
@ -309,9 +312,16 @@ class AesCryptTest extends BitcoinSUnitTest {
}
}
it must "have toBase64/fromBase64 symmetry" in {
forAll(CryptoGenerators.aesEncryptedData) { enc =>
val base64 = enc.toBase64
assert(AesEncryptedData.fromValidBase64(base64) == enc)
}
}
it must "fail to decrypt with the wrong key" in {
forAll(NumberGenerator.bytevector.suchThat(_.nonEmpty)) { bytes =>
val encrypted = AesCrypt.encryptExc(bytes, aesKey)
val encrypted = AesCrypt.encrypt(bytes, aesKey)
val decryptedE = AesCrypt.decrypt(encrypted, badAesKey)
decryptedE match {
case Right(decrypted) =>

View File

@ -14,7 +14,57 @@ import org.bitcoins.core.protocol.NetworkElement
* initialization vector (IV). Both the cipher text and the IV
* is needed to decrypt the cipher text.
*/
final case class AesEncryptedData(cipherText: ByteVector, iv: AesIV)
final case class AesEncryptedData(cipherText: ByteVector, iv: AesIV) {
/**
* We serialize IV and ciphertext by prepending the IV
* to the ciphertext, and converting it to base64.
* Since the IV is of static length, deserializing is a matter
* of taking the first bytes as IV, and the rest as
* ciphertext.
*/
lazy val toBase64: String = {
val bytes = iv.bytes ++ cipherText
bytes.toBase64
}
}
object AesEncryptedData {
/**
* We serialize IV and ciphertext by prepending the IV
* to the ciphertext, and converting it to base64.
* Since the IV is of static length, deserializing is a matter
* of taking the first bytes as IV, and the rest as
* ciphertext.
*/
def fromBase64(base64: String): Option[AesEncryptedData] = {
ByteVector.fromBase64(base64) match {
case None => None
case Some(bytes) if bytes.length <= AesIV.length => None
case Some(bytes) =>
val (ivBytes, cipherText) = bytes.splitAt(AesIV.length)
val iv = AesIV.fromValidBytes(ivBytes)
Some(AesEncryptedData(cipherText, iv))
}
}
/**
* We serialize IV and ciphertext by prepending the IV
* to the ciphertext, and converting it to base64.
* Since the IV is of static length, deserializing is a matter
* of taking the first bytes as IV, and the rest as
* ciphertext.
*/
def fromValidBase64(base64: String): AesEncryptedData =
fromBase64(base64) match {
case None =>
throw new IllegalArgumentException(
s"$base64 was not valid as AesEncryptedData!"
)
case Some(enc) => enc
}
}
/** Represents a salt used to derive a AES key from
* a human-readable passphrase.
@ -185,14 +235,19 @@ object AesKey {
*/
final case class AesIV private (private val underlying: ByteVector)
extends NetworkElement {
require(underlying.length == 16,
s"AES salt must be 16 bytes long! Got: ${underlying.length}")
require(
underlying.length == 16,
s"AES salt must be 16 bytes long! Got: ${underlying.length}"
)
val bytes: ByteVector = underlying
}
object AesIV {
/** Length of IV in bytes (for CFB mode, other modes have different lengths) */
private[crypto] val length = 16
// this is here to remove apply constructor
private[AesIV] def apply(bytes: ByteVector): AesIV = new AesIV(bytes)
@ -201,7 +256,7 @@ object AesIV {
* (in CFB mode, which is what we use here).
*/
def fromBytes(bytes: ByteVector): Option[AesIV] =
if (bytes.length == 16) Some(AesIV(bytes)) else None
if (bytes.length == AesIV.length) Some(AesIV(bytes)) else None
/** Constructs an AES IV from the given bytes. Throws if the given bytes are invalid */
def fromValidBytes(bytes: ByteVector): AesIV =
@ -212,7 +267,7 @@ object AesIV {
/** Generates a random IV */
def random: AesIV = {
val random = new SecureRandom()
val bytes = new Array[Byte](16)
val bytes = new Array[Byte](AesIV.length)
random.nextBytes(bytes)
AesIV(ByteVector(bytes))
}

View File

@ -230,6 +230,14 @@ sealed abstract class CryptoGenerators {
def aesPassword: Gen[AesPassword] =
Gen.alphaStr.suchThat(_.nonEmpty).map(AesPassword.fromNonEmptyString(_))
def aesIV: Gen[AesIV] = Gen.delay(AesIV.random)
def aesEncryptedData: Gen[AesEncryptedData] =
for {
cipher <- NumberGenerator.bytevector.suchThat(_.nonEmpty)
iv <- aesIV
} yield AesEncryptedData(cipherText = cipher, iv)
}
object CryptoGenerators extends CryptoGenerators