A Scala 3, functional, type-safe and memory-safe library to handle secret values
Secret library does its best to avoid leaking information in memory and in the code, BUT an attack is always possible,
and I don't give any certainties or guarantees about security by using this library, you use it at your own risk. The code is open-sourced; you can check the implementation and take your
decision consciously. I'll do my best to improve the security and documentation of this project.
Please, drop a ⭐️ if you are interested in this project and you want to support it.
Note
Scala 3 only, Scala 2 is not supported.
libraryDependencies += "com.github.geirolz" %% "secret" % "0.0.15"import com.geirolz.secret.*
import scala.util.Try
val reusableSecret: Secret[String] = Secret("password") // reusable secret
// reusableSecret: Secret[String] = ** SECRET **
val oneShotSecret: Secret.OneShot[String] = Secret.oneShot("password") // one shot secret
// oneShotSecret: OneShotSecret[String] = ** SECRET **
val deferredSecret: Secret.Deferred[Try, String] = Secret.deferred(Try("password")) // deferred secret
// deferredSecret: DeferredSecret[[T >: Nothing <: Any] => Try[T], String] = com.geirolz.secret.DeferredSecret$$anon$1@10d4c821By default the value is obfuscated when creating the Secret instance using the implicit SecretStrategy which, by default, transform the value into a xor-ed
ByteBuffer which store bytes outside the JVM using direct memory access.
The obfuscated value is de-obfuscated using the implicit SecretStrategy instance every time use, and derived method, are invoked which returns the original
value converting the bytes back to T re-applying the xor formula.
While obfuscating the value prevents or at least makes it harder to read the value from the memory, Secret class API are designed to avoid leaking
information in other ways. Preventing developers to improperly use the secret value ( logging, etc...).
Example
import com.geirolz.secret.*
import scala.util.Try
case class Database(password: String)
val secretString: Secret[String] = Secret("password")
// secretString: Secret[String] = ** SECRET **
val database: Either[SecretDestroyed, Database] = secretString.euseAndDestroy(password => Database(password))
// database: Either[SecretDestroyed, Database] = Right(Database("password"))
// if you try to access the secret value once used, you'll get an error
secretString.euse(println(_))
// res2: Either[SecretDestroyed, Unit] = Left(
// SecretDestroyed(README.md:50:109)
// )These integrations aim to enhance the functionality and capabilities of Secret type making it easier to use in different contexts.
libraryDependencies += "com.github.geirolz" %% "secret-effect" % "0.0.15"import com.geirolz.secret.*
import cats.effect.{IO, Resource}
val s: Secret[String] = Secret("password")
// !!!! this will not destroy the secret because it uses a duplicated one !!!
val res: Resource[IO, String] = s.resource[IO]
// this will destroy the secret because it uses the original one
val res2: Resource[IO, String] = s.resourceDestroy[IO]
// this will destroy the secret because it uses the original one
val res3: Resource[IO, String] = Secret.resource[IO, String]("password")libraryDependencies += "com.github.geirolz" %% "secret-zio" % "0.0.15"Provides ZManaged integration for Secret, OneShotSecret, and DeferredSecret types.
import com.geirolz.secret.*
import com.geirolz.secret.zio.*
import _root_.zio.{Task, ZIO}
import _root_.zio.managed.ZManaged
val s: Secret[String] = Secret("password")
// this will not destroy the secret because it uses a duplicated one
val managed: ZManaged[Any, Throwable, String] = s.managed
// this will destroy the secret because it uses the original one
val managedDestroy: ZManaged[Any, Throwable, String] = s.managedDestroy
// this will destroy the secret because it uses the original one
val managedFromValue: ZManaged[Any, Throwable, String] = Secret.managed("password")libraryDependencies += "com.github.geirolz" %% "secret-pureconfig" % "0.0.15"Just provides the ConfigReader instance for Secret[T] type.
There must be an ConfigReader[T] and a SecretStrategy[T] instances implicitly in the scope.
import com.geirolz.secret.pureconfig.givenlibraryDependencies += "com.github.geirolz" %% "secret-typesafe-config" % "0.0.15"import com.geirolz.secret.typesafe.config.givenlibraryDependencies += "com.github.geirolz" %% "secret-ciris" % "0.0.15"import com.geirolz.secret.ciris.givenProvides the json Decoder instance for Secret[T] and OneShotSecret[T] type.
libraryDependencies += "com.github.geirolz" %% "secret-circe" % "0.0.15"import com.geirolz.secret.circe.givenProvides the json JsonDecoder, JsonEncoder, and JsonCodec instances for Secret[T] and OneShotSecret[T] types.
libraryDependencies += "com.github.geirolz" %% "secret-zio-json" % "0.0.15"import com.geirolz.secret.ziojson.givenProvides the xml Decoder instance for Secret[T] and OneShotSecret[T] type.
libraryDependencies += "com.github.geirolz" %% "secret-cats-xml" % "0.0.15"import com.geirolz.secret.catsxml.givenThe encrypt module allows you to transform a Secret[String] (or one-shot secret) into an encrypted secret of another type, for example wrapping your secrets in strongly encrypted values. This is achieved through the Encryptor[O] typeclass, which lets you define how to encrypt a String into your protected output type O. With the provided extension methods, you can easily encrypt secrets while keeping them safe from accidental leaks—optionally destroying the original, unencrypted secret in the process.
libraryDependencies += "com.github.geirolz" %% "secret-encrypt" % "0.0.15"Common use case:
Suppose you want to ensure that the actual plain secret value never leaves the memory in unencrypted form. You define an Encryptor for your ciphertext type (e.g., EncryptedValue or just an Array[Byte]). Then, you call encrypt, encryptDeferred, encryptAndDestroy, or encryptAndDestroyDeferred on your secret.
Example:
import com.geirolz.secret.*
import com.geirolz.secret.encrypt.*
import scala.util.Try
// Conceptual encrypted value type
case class MyEncrypted(bytes: Array[Byte])
// Your custom Encryptor implementation
val myEncryptor: Encryptor[MyEncrypted] = new Encryptor[MyEncrypted] {
def encrypt(t: String): Try[MyEncrypted] =
Try(MyEncrypted(t.getBytes.reverse))
}
val original: Secret[String] = Secret("my_password")
// Encrypt, but keep the original
val encrypted: Try[Secret[MyEncrypted]] = original.encrypt(myEncryptor)
// Encrypt and destroy the original secret immediately
val encryptedAndDestroyed: Try[Secret[MyEncrypted]] = original.encryptAndDestroy(myEncryptor)If you are using Secret in your company, please let me know and I'll add it to the list! It means a lot to me.
If you want to use a custom obfuscation strategy for a specific type you can implement a custom SecretStrategy and provide an implicit instance of it during the secret creation.
If you think that your strategy can be useful for other people, please consider to contribute to the project and add it to the library.
import com.geirolz.secret.strategy.SecretStrategy
import com.geirolz.secret.strategy.SecretStrategy.{DeObfuscator, Obfuscator}
import com.geirolz.secret.util.KeyValueBuffer
import com.geirolz.secret.Secret
given SecretStrategy[String] = SecretStrategy[String](
Obfuscator.of[String](_ => KeyValueBuffer.directEmpty(0)),
DeObfuscator.of[String](_ => "CUSTOM"),
)
Secret("my_password").euse(secret => secret)
// res12: Either[SecretDestroyed, String] = Right("CUSTOM")If you want to use a custom obfuscation strategy algebra you can implement a custom SecretStrategyAlgebra and provide an implicit SecretStrategyFactory instance built on it during the secret creation.
If you think that your strategy can be useful for other people, please consider to contribute to the project and add it to the library.
import com.geirolz.secret.strategy.SecretStrategy.{DeObfuscator, Obfuscator}
import com.geirolz.secret.strategy.{SecretStrategy, SecretStrategyAlgebra}
import com.geirolz.secret.util.KeyValueBuffer
import com.geirolz.secret.{PlainValueBuffer, Secret}
import java.nio.ByteBuffer
// build the custom algebra
val myCustomAlgebra = new SecretStrategyAlgebra:
final def obfuscator[P](f: P => PlainValueBuffer): Obfuscator[P] =
Obfuscator.of { (plain: P) => KeyValueBuffer(ByteBuffer.allocateDirect(0), f(plain)) }
final def deObfuscator[P](f: PlainValueBuffer => P): DeObfuscator[P] =
DeObfuscator.of { bufferTuple => f(bufferTuple.roObfuscatedBuffer) }
// myCustomAlgebra: SecretStrategyAlgebra = repl.MdocSession$MdocApp13$$anon$16@1344c12c
// build factory based on the algebra
val myCustomStrategyFactory = myCustomAlgebra.newFactory
// myCustomStrategyFactory: SecretStrategyFactory = com.geirolz.secret.strategy.SecretStrategyFactory@41aa6ef6
// ----------------------------- USAGE -----------------------------
// implicitly in the scope
import myCustomStrategyFactory.given
Secret("my_password").euse(secret => secret)
// res14: Either[SecretDestroyed, String] = Right("my_password")
// or restricted to a specific scope
myCustomStrategyFactory {
Secret("my_password").euse(secret => secret)
}
// res15: Either[SecretDestroyed, String] = Right("my_password")We welcome contributions from the open-source community to make Secret even better. If you have any bug reports, feature requests, or suggestions, please submit them via GitHub issues. Pull requests are also welcome.
Before contributing, please read our Contribution Guidelines to understand the development process and coding conventions.
Please remember te following:
- Run
sbt scalafmtAllbefore submitting a PR. - Run
sbt gen-docto update the documentation.
Secret is released under the Apache License 2.0. Feel free to use it in your open-source or commercial projects.