Skip to content

Commit

Permalink
Add Encrypt module and improve Hasher (#50)
Browse files Browse the repository at this point in the history
* Refactoring Hasher and add Encrypt
  • Loading branch information
geirolz authored Aug 29, 2024
1 parent 853c924 commit 204a2f0
Show file tree
Hide file tree
Showing 27 changed files with 424 additions and 129 deletions.
13 changes: 12 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ lazy val root: Project = project
.settings(
copyReadMe := IO.copyFile(file("docs/compiled/README.md"), file("README.md"))
)
.aggregate(core, effect, docs, pureconfig, typesafeConfig, ciris, circe, `cats-xml`)
.aggregate(core, encrypt, effect, docs, pureconfig, typesafeConfig, ciris, circe, `cats-xml`)

lazy val docs: Project =
project
Expand Down Expand Up @@ -64,6 +64,17 @@ lazy val core: Project =
libraryDependencies ++= ProjectDependencies.Core.dedicated
)

// modules
lazy val modulesFolder: String = "./modules"
lazy val encrypt: Project =
module("encrypt")(
folder = s"$modulesFolder/encrypt",
publishAs = Some(subProjectName("encrypt"))
).dependsOn(core)
.settings(
libraryDependencies ++= ProjectDependencies.Modules.Encrypt.dedicated
)

// integrations
lazy val integrationsFolder: String = "./integrations"
lazy val effect: Project =
Expand Down
2 changes: 1 addition & 1 deletion core/examples/CustomStrategy.sc
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import com.geirolz.secret.Secret
import com.geirolz.secret.strategy.SecretStrategy
import com.geirolz.secret.strategy.SecretStrategy.{DeObfuscator, Obfuscator}
import com.geirolz.secret.Secret
import com.geirolz.secret.util.KeyValueBuffer

given SecretStrategy[String] = SecretStrategy[String](
Expand Down
2 changes: 1 addition & 1 deletion core/examples/Simple.sc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import com.geirolz.secret.Secret

val s1 = Secret("my_password")
s1.hashed
s1.hash

s1.euse(secret => print(secret))
s1.euseAndDestroy(secret => print(secret))
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/scala/com/geirolz/secret/DeferredSecret.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.geirolz.secret

import cats.{Eval, Functor, MonadThrow}
import cats.MonadThrow
import cats.syntax.all.*
import com.geirolz.secret.util.{Hasher, Location}
import com.geirolz.secret.strategy.SecretStrategy
import com.geirolz.secret.transform.Hasher
import com.geirolz.secret.util.Location

/** Specialized version of `Secret` that allows to defer the acquisition of the secret value. This is useful when you
* want to acquire the secret value only when it's needed and not before ( for instance, an HTTP call to a secret
Expand Down
16 changes: 11 additions & 5 deletions core/src/main/scala/com/geirolz/secret/OneShotSecret.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package com.geirolz.secret

import com.geirolz.secret.internal.{SecretApi, SecretCompanionApi, Vault}
import com.geirolz.secret.strategy.SecretStrategy
import com.geirolz.secret.util.{Hasher, Location}
import com.geirolz.secret.transform.Hasher
import com.geirolz.secret.util.Location

/** A `OneShotSecret` is a secret that can be used only once.
*
Expand All @@ -11,9 +12,14 @@ import com.geirolz.secret.util.{Hasher, Location}
* @tparam T
* type of the secret
*/
final class OneShotSecret[T] private[secret] (vault: Vault[T]) extends SecretApi[T](vault)
final class OneShotSecret[T] private[secret] (vault: Vault[T]) extends SecretApi[T](vault):
override type Self[X] = OneShotSecret[X]
private[secret] override val companion: SecretCompanionApi[Self] = OneShotSecret

object OneShotSecret extends SecretCompanionApi[Secret.OneShot]:

private[secret] given SecretCompanionApi[Secret.OneShot] = this

private[secret] def fromVault[T](vault: Vault[T]): Secret.OneShot[T] =
new OneShotSecret(vault)

Expand All @@ -25,12 +31,12 @@ object OneShotSecret extends SecretCompanionApi[Secret.OneShot]:
*
* @param value
* the value to obfuscate
* @param collectDestructionLocation
* @param recDestructionLocation
* if `true` the location where the secret was destroyed will be collected
* @return
* a new `OneShotSecret`
*/
override def apply[T](value: => T, collectDestructionLocation: Boolean = true)(using
override def apply[T](value: => T, recDestructionLocation: Boolean = true)(using
strategy: SecretStrategy[T],
hasher: Hasher
): Secret.OneShot[T] = fromVault(Vault[T](value, collectDestructionLocation))
): Secret.OneShot[T] = fromVault(Vault[T](value, recDestructionLocation))
18 changes: 12 additions & 6 deletions core/src/main/scala/com/geirolz/secret/Secret.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package com.geirolz.secret

import cats.{Eq, MonadThrow}
import cats.Eq
import com.geirolz.secret.Secret.*
import com.geirolz.secret.internal.{SecretApi, SecretCompanionApi, Vault}
import com.geirolz.secret.strategy.SecretStrategy
import com.geirolz.secret.transform.Hasher
import com.geirolz.secret.util.*

import scala.util.Try
Expand Down Expand Up @@ -41,6 +42,9 @@ abstract sealed class Secret[T] private (vault: Vault[T]) extends SecretApi[T](v

import cats.syntax.all.*

type Self[X] = Secret[X]
private[secret] val companion: SecretCompanionApi[Self] = Secret

/** Duplicate the secret without destroying it.
*
* If this was destroyed the duplicated will also be destroyed.
Expand Down Expand Up @@ -71,8 +75,8 @@ abstract sealed class Secret[T] private (vault: Vault[T]) extends SecretApi[T](v
* secretString.isDestroyed // false
* }}}
*/
final def map[U: SecretStrategy](f: T => U)(using Hasher): Secret[U] =
transform(_.euse(f.andThen(Secret[U](_))))
final def map[U: SecretStrategy](f: T => U): Secret[U] =
transform(_.euse(f.andThen(newFromThis(_))))

/** flat map the value using the specified function.
*
Expand All @@ -88,7 +92,7 @@ abstract sealed class Secret[T] private (vault: Vault[T]) extends SecretApi[T](v
* secretString.isDestroyed // false
* }}}
*/
final def flatMap[U: SecretStrategy](f: T => Secret[U])(using Hasher): Secret[U] =
final def flatMap[U: SecretStrategy](f: T => Secret[U]): Secret[U] =
transform(_.euse(f))

/** [[Secret.OneShot]] version of this secret */
Expand Down Expand Up @@ -119,6 +123,8 @@ abstract sealed class Secret[T] private (vault: Vault[T]) extends SecretApi[T](v

object Secret extends SecretCompanionApi[Secret]:

private[secret] given SecretCompanionApi[Secret] = this

type OneShot[T] = OneShotSecret[T]
final val oneShot = OneShotSecret

Expand All @@ -131,9 +137,9 @@ object Secret extends SecretCompanionApi[Secret]:
override def duplicate: Secret[T] = this

/** Create a secret from the given value */
override def apply[T](value: => T, collectDestructionLocation: Boolean = true)(using
override def apply[T](value: => T, recDestructionLocation: Boolean = true)(using
strategy: SecretStrategy[T],
hasher: Hasher
): Secret[T] =
new Secret[T](Vault[T](value, collectDestructionLocation)):
new Secret[T](Vault[T](value, recDestructionLocation)):
override def duplicate: Secret[T] = map(identity)
2 changes: 0 additions & 2 deletions core/src/main/scala/com/geirolz/secret/SecretDestroyed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package com.geirolz.secret
import cats.Show
import com.geirolz.secret.util.Location

import scala.util.control.NoStackTrace

case class SecretDestroyed(destructionLocation: Location)
extends RuntimeException(s"This secret has been already destroyed here: $destructionLocation")

Expand Down
86 changes: 62 additions & 24 deletions core/src/main/scala/com/geirolz/secret/internal/SecretApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@ package com.geirolz.secret.internal

import cats.syntax.all.*
import cats.{Eq, MonadThrow, Monoid, Show}
import com.geirolz.secret.strategy.SecretStrategy
import com.geirolz.secret.util.{Hasher, Location, SysEnv}
import com.geirolz.secret.*
import com.geirolz.secret.strategy.SecretStrategy
import com.geirolz.secret.transform.Hasher
import com.geirolz.secret.util.{Location, SysEnv}

import java.nio.charset.{Charset, StandardCharsets}
import scala.util.hashing.Hashing

private[secret] transparent trait SecretApi[T](protected val vault: Vault[T]) extends AutoCloseable:
private[secret] transparent trait SecretApi[T](
protected val vault: Vault[T]
) extends AutoCloseable:

import cats.syntax.all.*

type Self[X] <: SecretApi[X]

private[secret] val companion: SecretCompanionApi[[X] =>> Self[X]]

/** Destroy the secret value by filling the obfuscated value with '\0'.
*
* This method is idempotent.
Expand Down Expand Up @@ -61,35 +69,61 @@ private[secret] transparent trait SecretApi[T](protected val vault: Vault[T]) ex
}

// ---------------- TRANSFORMATIONS METHODS ----------------

/** map the value using the specified function and then destroy this.
/** Creates a new secret mapping the value using the specified function and then destroy this.
*
* If the secret were destroyed it will raise a `SecretDestroyed` when you try to use the new secret.
*/
final def mapAndDestroy[U: SecretStrategy](f: T => U)(using Location, Hasher): Secret[U] =
transform(_.useAndDestroy(f.andThen(Secret[U](_))))

/** flat map the value using the specified function and then destroy this.
def mapAndDestroy[U: SecretStrategy](f: T => U)(using Location): Self[U] =
transform(
_.euseAndDestroy(
f.andThen(newFromThis(_))
)
)

/** Flat map the value using the specified function and then destroy this and returns the new Secret returned by the
* specified function.
*
* If the secret were destroyed it will raise a `SecretDestroyed` when you try to use the new secret.
*/
final def flatMapAndDestroy[U: SecretStrategy](f: T => Secret[U])(using Location, Hasher): Secret[U] =
final def flatMapAndDestroy[U: SecretStrategy](f: T => Self[U])(using Location): Self[U] =
transform(_.useAndDestroy(f))

// ---------------- DATA ----------------
/** Creates a new secret that contains the hashed value of the secret and destroy the current one.
*
* The secret value is hashed using the specified `hasher` and `charset`.
*
* By default the charset is `StandardCharsets.UTF_8` and the hasher is the one used by the secret.
*
* If the secret were destroyed it will raise a `SecretDestroyed` when you try to use the new secret.
*
* @param hasher
* the hasher to use
* @param charset
* the charset to use
*/
final def asHashedAndDestroy(
hasher: Hasher = vault._hasher,
charset: Charset = StandardCharsets.UTF_8
): Self[String] =
flatMapAndDestroy[String] { v =>
newFromThis(
hasher.hashAsString(v.toString.getBytes(charset))
)
}

// ---------------- DATA ----------------
/** @return
* a hashed representation of the secret value
*/
final def hashed: String = vault.hashed
final def hash: String = vault.hash

/** Safely compare the hashed value of this secret with the provided `Secret`. */
inline def isHashedEquals(that: Secret[T]): Boolean =
hashed == that.hashed
/** Safely compare the hash value of this secret with the provided `Secret`. */
inline def isHashEquals(that: Secret[T]): Boolean =
hash == that.hash

/** Always returns `false` to prevents leaking information.
*
* Use [[isHashedEquals]] or [[isValueEquals]]
* Use [[isHashEquals]] or [[isValueEquals]]
*/
inline final override def equals(obj: Any): Boolean = false

Expand All @@ -111,10 +145,18 @@ private[secret] transparent trait SecretApi[T](protected val vault: Vault[T]) ex
vault.evalUse[F, T](_.pure[F])

/** Transform this secret */
private[secret] def transform[U](t: this.type => Either[SecretDestroyed, Secret[U]]): Secret[U] =
t(this) match
private[secret] def transform[U](t: Self[T] => Either[SecretDestroyed, Self[U]]): Self[U] =
t(this.asInstanceOf[Self[T]]) match
case Right(u) => u
case Left(e) => Secret.destroyed[U](e.destructionLocation)
case Left(e) => companion.destroyed[U](e.destructionLocation)

/** Create a new secret from this Secret settings */
private[secret] def newFromThis[U: SecretStrategy](value: U): Self[U] =
given Hasher = vault._hasher
companion[U](
value = value,
recDestructionLocation = vault._recDestructionLocation
)

private[secret] transparent trait SecretCompanionApi[SecretTpe[X] <: SecretApi[X]]
extends SecretApiSyntax[SecretTpe],
Expand All @@ -124,7 +166,7 @@ private[secret] transparent trait SecretCompanionApi[SecretTpe[X] <: SecretApi[X
def destroyed[T](location: Location = Location.unknown): SecretTpe[T]

/** Create a secret from the given value. */
def apply[T](value: => T, collectDestructionLocation: Boolean = true)(using
def apply[T](value: => T, recDestructionLocation: Boolean = true)(using
strategy: SecretStrategy[T],
hasher: Hasher
): SecretTpe[T]
Expand All @@ -143,10 +185,6 @@ private[secret] transparent trait SecretCompanionApi[SecretTpe[X] <: SecretApi[X
.flatMap(_.liftTo[F](new NoSuchElementException(s"Missing environment variable [$name]")))
.map(apply(_))

/** Create a new secret with the given value without collecting the destruction location */
def noLocation[T: SecretStrategy](value: => T)(using Hasher): SecretTpe[T] =
apply(value, collectDestructionLocation = false)

/** Syntax for the SecretPlatform */
private[secret] transparent sealed trait SecretApiSyntax[SecretTpe[X] <: SecretApi[X]]:
this: SecretCompanionApi[SecretTpe] =>
Expand Down
Loading

0 comments on commit 204a2f0

Please sign in to comment.