From 161db9ff92bcb28a884b67ccd09e15a662939f9f Mon Sep 17 00:00:00 2001 From: Torkel Rogstad Date: Tue, 25 Jun 2019 11:38:20 +0200 Subject: [PATCH] Add toBase64/fromBase64 to AesEncryptedData --- .../bitcoins/core/crypto/AesCryptTest.scala | 32 +++++---- .../org/bitcoins/core/crypto/AesCrypt.scala | 65 +++++++++++++++++-- .../testkit/core/gen/CryptoGenerators.scala | 8 +++ 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/core-test/src/test/scala/org/bitcoins/core/crypto/AesCryptTest.scala b/core-test/src/test/scala/org/bitcoins/core/crypto/AesCryptTest.scala index 1ea3a78e52..c11440dfa5 100644 --- a/core-test/src/test/scala/org/bitcoins/core/crypto/AesCryptTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/crypto/AesCryptTest.scala @@ -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) => diff --git a/core/src/main/scala/org/bitcoins/core/crypto/AesCrypt.scala b/core/src/main/scala/org/bitcoins/core/crypto/AesCrypt.scala index b79e51cd4d..85798bd093 100644 --- a/core/src/main/scala/org/bitcoins/core/crypto/AesCrypt.scala +++ b/core/src/main/scala/org/bitcoins/core/crypto/AesCrypt.scala @@ -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)) } diff --git a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala index 901974382e..32a5127e8d 100644 --- a/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala +++ b/testkit/src/main/scala/org/bitcoins/testkit/core/gen/CryptoGenerators.scala @@ -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