From fba810de6ef6f380492dc6d6f3acc13f0eebbd37 Mon Sep 17 00:00:00 2001 From: David Geirola Date: Fri, 12 Jan 2024 18:55:16 +0100 Subject: [PATCH] Secret new implementation (#90) * Improve Secret memory usage * Refactoring Secret internal * Add isEquals --- .scalafmt.conf | 6 + .../app/toolkit/config/BytesUtils.scala | 20 + .../geirolz/app/toolkit/config/Secret.scala | 411 ++++++++++++------ .../app/toolkit/config/BytesUtilsSuite.scala | 29 ++ .../app/toolkit/config/SecretSuite.scala | 91 +++- examples/src/main/resources/application.conf | 2 + .../com/geirolz/example/app/AppConfig.scala | 7 +- 7 files changed, 419 insertions(+), 147 deletions(-) create mode 100644 config/src/main/scala/com/geirolz/app/toolkit/config/BytesUtils.scala create mode 100644 config/src/test/scala/com/geirolz/app/toolkit/config/BytesUtilsSuite.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index cb05917..9c9b14d 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -25,6 +25,12 @@ newlines.alwaysBeforeElseAfterCurlyIf = false rewrite.rules = [RedundantParens, SortImports] rewrite.redundantBraces.stringInterpolation = true +docstrings.style = SpaceAsterisk +docstrings.oneline = fold +docstrings.removeEmpty = true +docstrings.blankFirstLine = no +docstrings.forceBlankLineBefore = true + spaces.inImportCurlyBraces = false fileOverride { diff --git a/config/src/main/scala/com/geirolz/app/toolkit/config/BytesUtils.scala b/config/src/main/scala/com/geirolz/app/toolkit/config/BytesUtils.scala new file mode 100644 index 0000000..27cb41d --- /dev/null +++ b/config/src/main/scala/com/geirolz/app/toolkit/config/BytesUtils.scala @@ -0,0 +1,20 @@ +package com.geirolz.app.toolkit.config + +import java.nio.ByteBuffer +import java.util + +private[config] object BytesUtils { + + def clearByteArray(bytes: Array[Byte]): Null = { + util.Arrays.fill(bytes, 0.toByte) + null + } + + def clearByteBuffer(buffer: ByteBuffer): Null = { + val zeroBytesArray = new Array[Byte](buffer.capacity()) + util.Arrays.fill(zeroBytesArray, 0.toByte) + buffer.clear() + buffer.put(zeroBytesArray) + null + } +} diff --git a/config/src/main/scala/com/geirolz/app/toolkit/config/Secret.scala b/config/src/main/scala/com/geirolz/app/toolkit/config/Secret.scala index bed3775..bfdf4f0 100644 --- a/config/src/main/scala/com/geirolz/app/toolkit/config/Secret.scala +++ b/config/src/main/scala/com/geirolz/app/toolkit/config/Secret.scala @@ -1,72 +1,97 @@ package com.geirolz.app.toolkit.config import cats.{Eq, MonadError, Show} -import com.geirolz.app.toolkit.config.Secret.{DeObfuser, Obfuser, ObfuserTuple, SecretNoLongerValid, Seed} +import com.geirolz.app.toolkit.config.BytesUtils.{clearByteArray, clearByteBuffer} +import com.geirolz.app.toolkit.config.Secret.{DeObfuser, MonadSecretError, Obfuser, ObfuserTuple, PlainValueBuffer, SecretNoLongerValid} import java.nio.ByteBuffer -import java.nio.charset.StandardCharsets -import java.util -import scala.util.Random +import java.nio.charset.Charset +import java.security.SecureRandom +import scala.util.Try import scala.util.control.NoStackTrace -import scala.util.hashing.Hashing +import scala.util.hashing.{Hashing, MurmurHash3} -/** The `Secret` class represent a secret value of type `T`. +/** Memory-safe and type-safe secret value of type `T`. * - * The value is implicitly obfuscated when creating the `Secret` instance using an implicit `Obfuser` instance which, by default, transform the value - * into a shuffled xor-ed `Array[Byte]`. + * `Secret` does the best to avoid leaking information in memory and in the code BUT an attack is possible and I don't give any certainties or + * guarantees about security using this class, you use it at your own risk. Code is open source, you can check the implementation and take your + * decision consciously. I'll do my best to improve the security and documentation of this class. + * + * Obfuscation + * + * The value is obfuscated when creating the `Secret` instance using an implicit `Obfuser`which, by default, transform the value into a xor-ed + * `ByteBuffer` witch store bytes outside the JVM using direct memory access. * * The obfuscated value is de-obfuscated using an implicit `DeObfuser` instance every time the method `use` is invoked which returns the original - * value un-shuffling the bytes and converting them back to `T` re-apply the xor. + * value converting bytes back to `T` re-apply the xor. + * + * API and Type safety + * + * While obfuscating the value prevents or at least makes it harder to read the value from memory, Secret class API is designed to avoid leaking + * information in other ways. Preventing developers to improperly use the secret value ( logging, etc...). * * Example * {{{ * val secretString: Secret[String] = Secret("my_password") * val database: F[Database] = secretString.use(password => initDb(password)) * }}} + * + * ** Credits ** + * - Personal experience in companies where I worked + * - https://westonal.medium.com/protecting-strings-in-jvm-memory-84c365f8f01c + * - VisualVM + * - ChatGPT */ -final class Secret[T](private var obfuscatedValue: Array[Byte], seed: Seed) { +sealed trait Secret[T] extends AutoCloseable { import cats.syntax.all.* - private var destroyed: Boolean = false - private type MonadSecretError[F[_]] = MonadError[F, ? >: SecretNoLongerValid] - - /** Avoid this method if possible. Unsafely apply `f` with the de-obfuscated value WITHOUT destroying it. + /** Apply `f` with the de-obfuscated value WITHOUT destroying it. * * If the secret is destroyed it will raise a `NoLongerValidSecret` exception. * - * Throws `SecretNoLongerValid` if the secret is destroyed + * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and + * other methods, it will raise a `NoLongerValidSecret` exception. */ - def unsafeUse[U](f: T => U)(implicit deObfuser: DeObfuser[T]): U = - use[Either[SecretNoLongerValid, *], U](f).fold(throw _, identity) + def evalUse[F[_]: MonadSecretError, U](f: T => F[U])(implicit deObfuser: DeObfuser[T]): F[U] - /** Apply `f` with the de-obfuscated value WITHOUT destroying it. + /** Destroy the secret value by filling the obfuscated value with '\0'. * - * If the secret is destroyed it will raise a `NoLongerValidSecret` exception. + * This method is idempotent. * * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and * other methods, it will raise a `NoLongerValidSecret` exception. */ - def use[F[_]: MonadSecretError, U](f: T => U)(implicit deObfuser: DeObfuser[T]): F[U] = - evalUse[F, U](f.andThen(_.pure[F])) + def destroy(): Unit - /** Alias for `use` with `Either[Throwable, *]` + /** Check if the secret is destroyed + * + * @return + * `true` if the secret is destroyed, `false` otherwise */ - def useE[U](f: T => U)(implicit deObfuser: DeObfuser[T]): Either[SecretNoLongerValid, U] = - use[Either[SecretNoLongerValid, *], U](f) + def isDestroyed: Boolean - /** Alias for `useAndDestroy` with `Either[Throwable, *]` + /** Calculate the non-deterministic hash code for this Secret. + * + * This hash code is NOT the hash code of the original value. It is the hash code of the obfuscated value. + * + * Since the obfuscated value based on a random key, the hash code will be different every time. This function is not deterministic. + * + * @return + * the hash code of this secret. If the secret is destroyed it will return `-1`. */ - def useAndDestroyE[U](f: T => U)(implicit deObfuser: DeObfuser[T]): Either[SecretNoLongerValid, U] = - useAndDestroy[Either[SecretNoLongerValid, *], U](f) + def hashCode(): Int - /** Apply `f` with the de-obfuscated value and then destroy the secret value by invoking `destroy` method. + // ------------------------------------------------------------------ + + /** Avoid this method if possible. Unsafely apply `f` with the de-obfuscated value WITHOUT destroying it. * - * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and - * other methods, it will raise a `NoLongerValidSecret` exception. + * If the secret is destroyed it will raise a `NoLongerValidSecret` exception. + * + * Throws `SecretNoLongerValid` if the secret is destroyed */ - def useAndDestroy[F[_]: MonadSecretError, U](f: T => U)(implicit deObfuser: DeObfuser[T]): F[U] = - evalUseAndDestroy[F, U](f.andThen(_.pure[F])) + final def unsafeUse[U](f: T => U)(implicit deObfuser: DeObfuser[T]): U = + use[Either[SecretNoLongerValid, *], U](f).fold(throw _, identity) /** Apply `f` with the de-obfuscated value WITHOUT destroying it. * @@ -75,164 +100,302 @@ final class Secret[T](private var obfuscatedValue: Array[Byte], seed: Seed) { * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and * other methods, it will raise a `NoLongerValidSecret` exception. */ - def evalUse[F[_]: MonadSecretError, U](f: T => F[U])(implicit deObfuser: DeObfuser[T]): F[U] = - if (destroyed) { - implicitly[MonadSecretError[F]].raiseError(SecretNoLongerValid()) - } else - f(deObfuser(obfuscatedValue, seed)) + final def use[F[_]: MonadSecretError, U](f: T => U)(implicit deObfuser: DeObfuser[T]): F[U] = + evalUse[F, U](f.andThen(_.pure[F])) + + /** Alias for `use` with `Either[Throwable, *]` */ + final def useE[U](f: T => U)(implicit deObfuser: DeObfuser[T]): Either[SecretNoLongerValid, U] = + use[Either[SecretNoLongerValid, *], U](f) /** Apply `f` with the de-obfuscated value and then destroy the secret value by invoking `destroy` method. * * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and * other methods, it will raise a `NoLongerValidSecret` exception. */ - def evalUseAndDestroy[F[_]: MonadSecretError, U](f: T => F[U])(implicit deObfuser: DeObfuser[T]): F[U] = - evalUse(f).map { u => destroy(); u } + final def useAndDestroy[F[_]: MonadSecretError, U](f: T => U)(implicit deObfuser: DeObfuser[T]): F[U] = + evalUseAndDestroy[F, U](f.andThen(_.pure[F])) - /** Destroy the secret value by filling the obfuscated value with 0. - * - * This method is idempotent. + /** Alias for `useAndDestroy` with `Either[Throwable, *]` */ + final def useAndDestroyE[U](f: T => U)(implicit deObfuser: DeObfuser[T]): Either[SecretNoLongerValid, U] = + useAndDestroy[Either[SecretNoLongerValid, *], U](f) + + /** Apply `f` with the de-obfuscated value and then destroy the secret value by invoking `destroy` method. * * Once the secret is destroyed it can't be used anymore. If you try to use it using `use`, `useAndDestroy`, `evalUse`, `evalUseAndDestroy` and * other methods, it will raise a `NoLongerValidSecret` exception. */ - def destroy(): Unit = - if (!destroyed) { - util.Arrays.fill(obfuscatedValue, 0.toByte) - obfuscatedValue = null - destroyed = true - } + final def evalUseAndDestroy[F[_]: MonadSecretError, U](f: T => F[U])(implicit deObfuser: DeObfuser[T]): F[U] = + evalUse(f).map { u => destroy(); u } - /** Check if the secret is destroyed + /** Alias for `destroy` */ + final override def close(): Unit = destroy() + + /** Safely compare this secret with the provided `Secret`. + * * @return - * `true` if the secret is destroyed, `false` otherwise + * `true` if the secrets are equal, `false` if they are not equal or if one of the secret is destroyed */ - def isDestroyed: Boolean = destroyed + final def isEquals(that: Secret[T])(implicit deObfuser: DeObfuser[T]): Boolean = + evalUse[Try, Boolean](value => that.use[Try, Boolean](_ == value)).getOrElse(false) - /** @return - * always returns `false` to avoid leaking information - */ - override def equals(obj: Any): Boolean = false + /** Always returns `false`, use `isEqual` instead */ + final override def equals(obj: Any): Boolean = false /** @return * always returns a static place holder string "** SECRET **" to avoid leaking information */ - override def toString: String = Secret.placeHolder - - /** @return - * always returns `-1` to avoid leaking information - */ - override def hashCode(): Int = -1 + final override def toString: String = Secret.placeHolder } object Secret extends Instances { val placeHolder = "** SECRET **" + private[config] type PlainValueBuffer = ByteBuffer + private[config] type ObfuscatedValueBuffer = ByteBuffer + private[config] type KeyBuffer = ByteBuffer + private type MonadSecretError[F[_]] = MonadError[F, ? >: SecretNoLongerValid] case class SecretNoLongerValid() extends RuntimeException("This secret value is no longer valid") with NoStackTrace + private[Secret] class KeyValueTuple( + _keyBuffer: KeyBuffer, + _obfuscatedBuffer: ObfuscatedValueBuffer + ) { + + val roKeyBuffer: KeyBuffer = _keyBuffer.asReadOnlyBuffer() + + val roObfuscatedBuffer: ObfuscatedValueBuffer = _obfuscatedBuffer.asReadOnlyBuffer() + + lazy val obfuscatedHashCode: Int = { + val capacity = roObfuscatedBuffer.capacity() + var bytes: Array[Byte] = new scala.Array[Byte](capacity) + for (i <- 0 until capacity) { + bytes(i) = roObfuscatedBuffer.get(i) + } + val hashCode: Int = MurmurHash3.bytesHash(bytes) + bytes = clearByteArray(bytes) + + hashCode + } + + def destroy(): Unit = { + clearByteBuffer(_keyBuffer) + clearByteBuffer(_obfuscatedBuffer) + () + } + } + + def apply[T: Obfuser](value: T): Secret[T] = { - def apply[T: Obfuser](value: T, seed: Seed = Random.nextLong()): Secret[T] = - new Secret(Obfuser[T].apply(value, seed), seed) + var bufferTuple: KeyValueTuple = Obfuser[T].apply(value) + + new Secret[T] { + + override def evalUse[F[_]: MonadSecretError, U](f: T => F[U])(implicit deObfuser: DeObfuser[T]): F[U] = + if (isDestroyed) + implicitly[MonadSecretError[F]].raiseError(SecretNoLongerValid()) + else + f(deObfuser(bufferTuple)) + + override def destroy(): Unit = { + bufferTuple.destroy() + bufferTuple = null + } + + override def isDestroyed: Boolean = + bufferTuple == null + + override def hashCode(): Int = + if (isDestroyed) -1 else bufferTuple.obfuscatedHashCode + } + } // ---------------- OBFUSER ---------------- - private[Secret] type Seed = Long - trait Obfuser[P] extends ((P, Seed) => Array[Byte]) + trait Obfuser[P] extends (P => KeyValueTuple) object Obfuser { + def apply[P: Obfuser]: Obfuser[P] = implicitly[Obfuser[P]] - def of[P](f: (P, Seed) => Array[Byte]): Obfuser[P] = - (p, s) => f(p, s) - - def default[P](f: P => Array[Byte]): Obfuser[P] = - Obfuser.of((plain, seed) => shuffleAndXorBytes(seed, f(plain))) - - def shuffleAndXorBytes(seed: Seed, bytes: Array[Byte]): Array[Byte] = { - val random: Random = new Random(seed) - val key: Byte = random.nextLong().toByte - random.shuffle(bytes.toSeq).map(b => (b ^ key).toByte).toArray + /** Create a new Obfuser which obfuscate value using a custom formula. + * + * @param f + * the function which obfuscate the value + */ + def of[P](f: P => KeyValueTuple): Obfuser[P] = f(_) + + /** Create a new Obfuser which obfuscate value using a Xor formula. + * + * Formula: `plainValue[i] ^ (key[len - i] ^ (len * i))` + * + * Example: + * {{{ + * //Index = 1 2 3 4 5 + * //Plain = [0x01][0x02][0x03][0x04][0x05] + * //Key = [0x9d][0x10][0xad][0x87][0x2b] + * //Obfuscated = [0x9c][0x12][0xae][0x83][0x2e] + * }}} + */ + def default[P](f: P => PlainValueBuffer): Obfuser[P] = { + + def genKeyBuffer(secureRandom: SecureRandom, size: Int): KeyBuffer = { + val keyBuffer = ByteBuffer.allocateDirect(size) + var keyArray = new Array[Byte](size) + secureRandom.nextBytes(keyArray) + keyBuffer.put(keyArray) + + // clear keyArray + keyArray = clearByteArray(keyArray) + + keyBuffer + } + + of { (plain: P) => + val secureRandom: SecureRandom = new SecureRandom() + var plainBuffer: PlainValueBuffer = f(plain) + val capacity: Int = plainBuffer.capacity() + val keyBuffer: KeyBuffer = genKeyBuffer(secureRandom, capacity) + val valueBuffer: ObfuscatedValueBuffer = ByteBuffer.allocateDirect(capacity) + for (i <- 0 until capacity) { + valueBuffer.put( + ( + plainBuffer.get(i) ^ (keyBuffer.get(capacity - 1 - i) ^ (capacity * i).toByte) + ).toByte + ) + } + + // clear plainBuffer + plainBuffer = clearByteBuffer(plainBuffer) + + new KeyValueTuple(keyBuffer, valueBuffer) + } } } - trait DeObfuser[P] extends ((Array[Byte], Seed) => P) + trait DeObfuser[P] extends (KeyValueTuple => P) object DeObfuser { + def apply[P: DeObfuser]: DeObfuser[P] = implicitly[DeObfuser[P]] - def of[P](f: (Array[Byte], Seed) => P): DeObfuser[P] = - (b, s) => f(b, s) - - def default[P](f: Array[Byte] => P): DeObfuser[P] = - DeObfuser.of((bytes, seed) => f(unshuffleAndXorBytes(seed, bytes))) - - def unshuffleAndXorBytes(seed: Seed, shuffled: Array[Byte]): Array[Byte] = { - val random: Random = new Random(seed) - val key: Byte = random.nextLong().toByte - val shuffled_perm: Seq[Int] = random.shuffle(1 to shuffled.length) - val zipped_ls: Array[(Byte, Int)] = shuffled.zip(shuffled_perm) - - zipped_ls.sortBy(_._2).map(t => (t._1 ^ key).toByte) - } + /** Create a new DeObfuser which de-obfuscate value using a custom formula. + * + * @param f + * the function which de-obfuscate the value + */ + def of[P](f: KeyValueTuple => P): DeObfuser[P] = f(_) + + /** Create a new DeObfuser which de-obfuscate value using a Xor formula. + * + * Formula: `obfuscated[i] ^ (key[len - i] ^ (len * i))` + * + * Example: + * {{{ + * //Index = 1 2 3 4 5 + * //Obfuscated = [0x9c][0x12][0xae][0x83][0x2e] + * //Key = [0x9d][0x10][0xad][0x87][0x2b] + * //Plain = [0x01][0x02][0x03][0x04][0x05] + * }}} + */ + def default[P](f: PlainValueBuffer => P): DeObfuser[P] = + of { bufferTuple => + val capacity: Int = bufferTuple.roKeyBuffer.capacity() + var plainValueBuffer: PlainValueBuffer = ByteBuffer.allocateDirect(capacity) + + for (i <- 0 until capacity) { + plainValueBuffer.put( + ( + bufferTuple.roObfuscatedBuffer.get(i) ^ (bufferTuple.roKeyBuffer.get(capacity - 1 - i) ^ (capacity * i).toByte) + ).toByte + ) + } + + val result = f(plainValueBuffer.asReadOnlyBuffer()) + + // clear plainValueBuffer + plainValueBuffer = clearByteBuffer(plainValueBuffer) + + result + } } case class ObfuserTuple[P](obfuser: Obfuser[P], deObfuser: DeObfuser[P]) { def bimap[U](fO: U => P, fD: P => U): ObfuserTuple[U] = ObfuserTuple[U]( - obfuser = Obfuser.of((plain, seed) => obfuser(fO(plain), seed)), - deObfuser = DeObfuser.of((bytes, seed) => fD(deObfuser(bytes, seed))) + obfuser = Obfuser.of(plain => obfuser(fO(plain))), + deObfuser = DeObfuser.of(bufferTuple => fD(deObfuser(bufferTuple))) ) } object ObfuserTuple { - def allocateByteBuffer[P](capacity: Int)( - bObfuser: ByteBuffer => P => ByteBuffer, - bDeObfuser: ByteBuffer => P + + /** https://westonal.medium.com/protecting-strings-in-jvm-memory-84c365f8f01c + * + * We require a buffer that’s outside of the GCs control. This will ensure that multiple copies cannot be left beyond the time we are done with + * it. + * + * For this we can use ByteBuffer.allocateDirect The documentation for https://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html only + * says a direct buffer may exist outside of the managed heap but it is at least pinned memory, as they are safe for I/O with non JVM code so the + * GC won’t be moving this buffer and making copies. + */ + def withXorDirectByteBuffer[P](capacity: Int)( + fillBuffer: ByteBuffer => P => ByteBuffer, + readBuffer: ByteBuffer => P ): ObfuserTuple[P] = ObfuserTuple( - obfuser = Obfuser.default(p => bObfuser(ByteBuffer.allocate(capacity)).apply(p).array()), - deObfuser = DeObfuser.default(b => bDeObfuser(ByteBuffer.wrap(b))) + obfuser = Obfuser.default((plainValue: P) => fillBuffer(ByteBuffer.allocateDirect(capacity)).apply(plainValue)), + deObfuser = DeObfuser.default((buffer: PlainValueBuffer) => readBuffer(buffer.rewind().asReadOnlyBuffer())) ) + + def xorStringObfuserTuple(charset: Charset): ObfuserTuple[String] = + xorBytesArrayObfuserTuple.bimap(_.getBytes(charset), new String(_, charset)) } } sealed trait Instances { - implicit val stringObfuserTuple: ObfuserTuple[String] = - ObfuserTuple( - Obfuser.default(_.getBytes(StandardCharsets.UTF_8)), - DeObfuser.default(off => new String(off, StandardCharsets.UTF_8)) + implicit val xorBytesArrayObfuserTuple: ObfuserTuple[Array[Byte]] = + ObfuserTuple[Array[Byte]]( + obfuser = Obfuser.default((plainBytes: Array[Byte]) => ByteBuffer.allocateDirect(plainBytes.length).put(plainBytes)), + deObfuser = DeObfuser.default((plainBuffer: PlainValueBuffer) => { + val result = new Array[Byte](plainBuffer.capacity()) + plainBuffer.rewind().get(result) + result + }) ) - implicit val byteObfuserTuple: ObfuserTuple[Byte] = - ObfuserTuple(Obfuser.default(i => Array(i)), DeObfuser.default(_.head)) + implicit val xorStdCharsetStringObfuserTuple: ObfuserTuple[String] = + ObfuserTuple.xorStringObfuserTuple(Charset.defaultCharset()) - implicit val charObfuserTuple: ObfuserTuple[Char] = - ObfuserTuple.allocateByteBuffer(2)(_.putChar, _.getChar) + implicit val xorByteObfuserTuple: ObfuserTuple[Byte] = + ObfuserTuple.withXorDirectByteBuffer(1)(_.put, _.get) - implicit val intObfuserTuple: ObfuserTuple[Int] = - ObfuserTuple.allocateByteBuffer(4)(_.putInt, _.getInt) + implicit val xorCharObfuserTuple: ObfuserTuple[Char] = + ObfuserTuple.withXorDirectByteBuffer(2)(_.putChar, _.getChar) - implicit val shortObfuserTuple: ObfuserTuple[Short] = - ObfuserTuple.allocateByteBuffer(2)(_.putShort, _.getShort) + implicit val xorShortObfuserTuple: ObfuserTuple[Short] = + ObfuserTuple.withXorDirectByteBuffer(2)(_.putShort, _.getShort) - implicit val floatObfuserTuple: ObfuserTuple[Float] = - ObfuserTuple.allocateByteBuffer(4)(_.putFloat, _.getFloat) + implicit val xorIntObfuserTuple: ObfuserTuple[Int] = + ObfuserTuple.withXorDirectByteBuffer(4)(_.putInt, _.getInt) - implicit val doubleObfuserTuple: ObfuserTuple[Double] = - ObfuserTuple.allocateByteBuffer(8)(_.putDouble, _.getDouble) + implicit val xorLongObfuserTuple: ObfuserTuple[Long] = + ObfuserTuple.withXorDirectByteBuffer(8)(_.putLong, _.getLong) - implicit val boolObfuserTuple: ObfuserTuple[Boolean] = - ObfuserTuple( - Obfuser.default(i => if (i) Array(1) else Array(0)), - DeObfuser.default(_.head match { - case 1 => true - case 0 => false - }) + implicit val xorFloatObfuserTuple: ObfuserTuple[Float] = + ObfuserTuple.withXorDirectByteBuffer(4)(_.putFloat, _.getFloat) + + implicit val xorDoubleObfuserTuple: ObfuserTuple[Double] = + ObfuserTuple.withXorDirectByteBuffer(8)(_.putDouble, _.getDouble) + + implicit val xorBoolObfuserTuple: ObfuserTuple[Boolean] = + ObfuserTuple.withXorDirectByteBuffer(1)( + (b: PlainValueBuffer) => (v: Boolean) => b.put(if (v) 1.toByte else 0.toByte), + _.get == 1.toByte ) implicit val bigIntObfuserTuple: ObfuserTuple[BigInt] = - ObfuserTuple(Obfuser.default(_.toByteArray), DeObfuser.default(BigInt(_))) + xorBytesArrayObfuserTuple.bimap(_.toByteArray, BigInt(_)) implicit val bigDecimalObfuserTuple: ObfuserTuple[BigDecimal] = - stringObfuserTuple.bimap(_.toString, str => BigDecimal(str)) + xorStdCharsetStringObfuserTuple.bimap(_.toString, str => BigDecimal(str)) implicit def unzipObfuserTupleToObfuser[P: ObfuserTuple]: Obfuser[P] = implicitly[ObfuserTuple[P]].obfuser @@ -244,8 +407,8 @@ sealed trait Instances { Hashing.fromFunction(_.hashCode()) implicit def eq[T]: Eq[Secret[T]] = - (_, _) => false + Eq.fromUniversalEquals implicit def show[T]: Show[Secret[T]] = - _ => Secret.placeHolder + Show.fromToString } diff --git a/config/src/test/scala/com/geirolz/app/toolkit/config/BytesUtilsSuite.scala b/config/src/test/scala/com/geirolz/app/toolkit/config/BytesUtilsSuite.scala new file mode 100644 index 0000000..3c20c82 --- /dev/null +++ b/config/src/test/scala/com/geirolz/app/toolkit/config/BytesUtilsSuite.scala @@ -0,0 +1,29 @@ +package com.geirolz.app.toolkit.config + +import java.nio.ByteBuffer + +class BytesUtilsSuite extends munit.FunSuite { + + test("clearByteArray") { + val bytes: Array[Byte] = Array[Byte](1, 2, 3, 4, 5) + BytesUtils.clearByteArray(bytes) + assertEquals(bytes.toList, List[Byte](0, 0, 0, 0, 0)) + } + + test("clearByteBuffer - HeapByteBuffer") { + val buffer = ByteBuffer.wrap(Array[Byte](1, 2, 3, 4, 5)) + BytesUtils.clearByteBuffer(buffer) + assertEquals(buffer.array().toList, List[Byte](0, 0, 0, 0, 0)) + } + + test("clearByteBuffer - DirectByteBuffer") { + val buffer = ByteBuffer.allocateDirect(5) + buffer.put(Array[Byte](1, 2, 3, 4, 5)) + BytesUtils.clearByteBuffer(buffer) + + val array = new Array[Byte](buffer.capacity()) + buffer.rewind().get(array) + + assertEquals(array.toList, List[Byte](0, 0, 0, 0, 0)) + } +} diff --git a/config/src/test/scala/com/geirolz/app/toolkit/config/SecretSuite.scala b/config/src/test/scala/com/geirolz/app/toolkit/config/SecretSuite.scala index 8f884f7..de6cb20 100644 --- a/config/src/test/scala/com/geirolz/app/toolkit/config/SecretSuite.scala +++ b/config/src/test/scala/com/geirolz/app/toolkit/config/SecretSuite.scala @@ -1,6 +1,6 @@ package com.geirolz.app.toolkit.config -import com.geirolz.app.toolkit.config.Secret.{DeObfuser, Obfuser, ObfuserTuple, SecretNoLongerValid} +import com.geirolz.app.toolkit.config.Secret.{ObfuserTuple, SecretNoLongerValid} import org.scalacheck.Arbitrary import org.scalacheck.Prop.forAll @@ -11,6 +11,7 @@ class SecretSuite extends munit.ScalaCheckSuite { testObfuserTupleFor[String] testObfuserTupleFor[Int] + testObfuserTupleFor[Long] testObfuserTupleFor[Short] testObfuserTupleFor[Char] testObfuserTupleFor[Byte] @@ -20,37 +21,83 @@ class SecretSuite extends munit.ScalaCheckSuite { testObfuserTupleFor[BigInt] testObfuserTupleFor[BigDecimal] - test("shuffleAndXorBytes works properly returning the same value for the same seed and value") { - val seed: Long = 1111 - val value: String = "12345678" - assertEquals( - obtained = new String(Obfuser.shuffleAndXorBytes(seed, value.getBytes)), - expected = new String(Obfuser.shuffleAndXorBytes(seed, value.getBytes)) - ) + test("Simple Secret String") { + Secret("TEST").useAndDestroyE(_ => ()) } - test("shuffleAndXorBytes works properly obfuscating and de-obfuscating a String value") { - val seed: Long = 1111 - val value: String = "12345678" - val obfuscated: Array[Byte] = Obfuser.shuffleAndXorBytes(seed, value.getBytes) - val deObfuscated: Array[Byte] = DeObfuser.unshuffleAndXorBytes(seed, obfuscated) - - assertEquals( - obtained = new String(deObfuscated), - expected = value - ) + test("Simple Secret with long String") { + Secret( + """|C#iur0#UsxTWzUZ5QPn%KGo$922SMvc5zYLqrcdE6SU6ZpFQrk3&W + |1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!uPoG%dxTab0QtTab0Qta + |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL + |KGo$922SMvc5zYLqrcdEKGo$922SMvc5zYLqrcdE6SU6ZpFQrk36S + |U6ZpFQrk31hRbc48obb1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!u + |PoG%dxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMvc5zY + |LqrcdE6SdxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMv + |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL + |1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!uPoG%dxTab0QtTab0Qta + |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL + |KGo$922SMvc5zYLqrcdEKGo$922SMvc5zYLqrcdE6SU6ZpFQrk36S + |U6ZpFQrk31hRbc48obb1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!u + |PoG%dxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMvc5zY + |LqrcdE6SdxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMv + |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL + |qrcdEKGo$922SMvc5zYU6ZpFQrk31hRbc48obb1c48obbQrqgk36S + |PoG%dxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMvc5zY + |LqrcdE6SdxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMv + |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL + |1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!uPoG%dxTab0QtTab0Qta + |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL + |KGo$922SMvc5zYLqrcdEKGo$922SMvc5zYLqrcdE6SU6ZpFQrk36S + |U6ZpFQrk31hRbc48obb1c48obb&Rngv9twgMHTuXG@hRb@FZg@u!u + |PoG%dxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMvc5zY + |LqrcdE6SdxTab0QtTab0QtaKGo$922SMvc5zYLqrcdEKGo$922SMv + |c5zYU6ZpRngv9twgMHTuXGFdxTab0QtTab0QtaKGo$922SMvc5zYL + |qrcdEKGo$922SMvc5zYU6ZpFQrk31hRbc48obb1c48obbQrqgk36S + |qrcdEKGo$922SMvc5zYU6ZpFQrk31hRbc48obb1c48obbQrqgk36S + |""".stripMargin + ).useAndDestroyE(_ => ()) } private def testObfuserTupleFor[T: Arbitrary: ObfuserTuple](implicit c: ClassTag[T]): Unit = { - property(s"Secret equals for type ${c.runtimeClass.getSimpleName} always return false") { + val typeName = c.runtimeClass.getSimpleName.capitalize + + property(s"Secret[$typeName] succesfully obfuscate") { + forAll { (value: T) => + Secret(value) + assert(cond = true) + } + } + + property(s"Secret[$typeName] equals always return false") { + forAll { (value: T) => + assertNotEquals(Secret(value), Secret(value)) + } + } + + property(s"Secret[$typeName] isEquals works properly") { + forAll { (value: T) => + val s1 = Secret(value) + val s2 = Secret(value) + + assert(s1.isEquals(s2)) + s1.destroy() + assert(!s1.isEquals(s2)) + assert(!s2.isEquals(s1)) + s2.destroy() + assert(!s1.isEquals(s2)) + } + } + + property(s"Secret[$typeName] hashCode is different from the value one") { forAll { (value: T) => - assert(Secret(value) != Secret(value)) + assert(Secret(value).hashCode() != value.hashCode()) } } // use - property(s"Secret obfuscate and de-obfuscate type ${c.runtimeClass.getSimpleName} properly - use") { + property(s"Secret[$typeName] obfuscate and de-obfuscate properly - use") { forAll { (value: T) => assert( Secret(value) @@ -66,7 +113,7 @@ class SecretSuite extends munit.ScalaCheckSuite { } // useAndDestroy - property(s"Secret obfuscate and de-obfuscate type ${c.runtimeClass.getSimpleName} properly - useAndDestroy") { + property(s"Secret[$typeName] obfuscate and de-obfuscate properly - useAndDestroy") { forAll { (value: T) => val secret: Secret[T] = Secret(value) diff --git a/examples/src/main/resources/application.conf b/examples/src/main/resources/application.conf index 16f0141..24afec5 100644 --- a/examples/src/main/resources/application.conf +++ b/examples/src/main/resources/application.conf @@ -6,3 +6,5 @@ http-server { kafka-broker { host: "0.0.0.0" } + +database-password: "password" \ No newline at end of file diff --git a/examples/src/main/scala-2/com/geirolz/example/app/AppConfig.scala b/examples/src/main/scala-2/com/geirolz/example/app/AppConfig.scala index 042b1a7..a220a8e 100644 --- a/examples/src/main/scala-2/com/geirolz/example/app/AppConfig.scala +++ b/examples/src/main/scala-2/com/geirolz/example/app/AppConfig.scala @@ -2,12 +2,14 @@ package com.geirolz.example.app import cats.Show import com.comcast.ip4s.{Hostname, Port} +import com.geirolz.app.toolkit.config.Secret import io.circe.Encoder import pureconfig.ConfigReader case class AppConfig( httpServer: HttpServerConfig, - kafkaBroker: KafkaBrokerSetting + kafkaBroker: KafkaBrokerSetting, + databasePassword: Secret[String] ) object AppConfig { @@ -26,6 +28,9 @@ object AppConfig { implicit val portCirceEncoder: Encoder[Port] = Encoder.encodeInt.contramap(_.value) + implicit def secretEncoder[T]: Encoder[Secret[T]] = + Encoder.encodeString.contramap(_.toString) + implicit val showInstanceForConfig: Show[AppConfig] = _.asJson.toString() }