mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-01-19 05:43:51 +01:00
Add toBase64/fromBase64 to AesEncryptedData
This commit is contained in:
parent
77d056e5fa
commit
161db9ff92
@ -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) =>
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user