geirolz / secret   0.0.15

Apache License 2.0 GitHub

A functional, type-safe and memory-safe class to handle secret values

Scala versions: 3.x

Secret

Build Status Codacy Badge Codacy Badge Sonatype Nexus (Releases) Scala Steward badge Mergify Status GitHub license

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.14"

Usage

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@2ed6f149

Obfuscation

By 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.

API and Type safety

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(
//   value = Database(password = "password")
// )

// if you try to access the secret value once used, you'll get an error
secretString.euse(println(_))
// res2: Either[SecretDestroyed, Unit] = Left(
//   value = SecretDestroyed(destructionLocation = README.md:50:113)
// )

Integrations

These integrations aim to enhance the functionality and capabilities of Secret type making it easier to use in different contexts.

Cats Effect

libraryDependencies += "com.github.geirolz" %% "secret-effect" % "0.0.11"
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")

Pureconfig

libraryDependencies += "com.github.geirolz" %% "secret-pureconfig" % "0.0.11"

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.given

Typesafe Config

libraryDependencies += "com.github.geirolz" %% "secret-typesafe-config" % "0.0.11"
import com.geirolz.secret.typesafe.config.given

Ciris

libraryDependencies += "com.github.geirolz" %% "secret-ciris" % "0.0.11"
import com.geirolz.secret.ciris.given

Circe

Provides the json Decoder instance for Secret[T] and OneShotSecret[T] type.

libraryDependencies += "com.github.geirolz" %% "secret-circe" % "0.0.11"
import com.geirolz.secret.circe.given

Cats-xml

Provides the xml Decoder instance for Secret[T] and OneShotSecret[T] type.

libraryDependencies += "com.github.geirolz" %% "secret-cats-xml" % "0.0.11"
import com.geirolz.secret.catsxml.given

Adopters

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.

Custom Obfuscation Strategy for a specific type

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)
// res10: Either[SecretDestroyed, String] = Right(value = "CUSTOM")

Custom Obfuscation Strategy algebra

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$MdocApp11$$anon$12@6512f34b

// build factory based on the algebra
val myCustomStrategyFactory = myCustomAlgebra.newFactory
// myCustomStrategyFactory: SecretStrategyFactory = com.geirolz.secret.strategy.SecretStrategyFactory@5be15840

// ----------------------------- USAGE -----------------------------
// implicitly in the scope

import myCustomStrategyFactory.given

Secret("my_password").euse(secret => secret)
// res12: Either[SecretDestroyed, String] = Right(value = "my_password")

// or restricted to a specific scope
myCustomStrategyFactory {
  Secret("my_password").euse(secret => secret)
}
// res13: Either[SecretDestroyed, String] = Right(value = "my_password")

Contributing

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 scalafmtAll before submitting a PR.
  • Run sbt gen-doc to update the documentation.

License

Secret is released under the Apache License 2.0. Feel free to use it in your open-source or commercial projects.

Acknowledgements