secure-string-context |
---|
|
A Scala library for pre-processing certain types of values during string interpolation to ensure all values can be shown securely, and checked at compile-time.
It's as simple as:
import safe.strings._
val name = "Jeff"
println(ss"Hello my name is ${safe(name)}!")
println(safe"Hello my name is ${safe(name)}!")
This will print Hello my name is Jeff!
twice
Note that ss
(aka safe
) is identical to the s
string interpolation, however, it will not print
values that are not wrapped in the safe
method by default.
Some types can be made to be implicitly Safe, so that you don't need to wrap them in the safe
method.
And all types can be forcibly stringified with the raw
method.
Compiles | value: Any | value: [T: CanBeSafe] | value: [T: Safe] |
---|---|---|---|
ss"$value" | X | X | √ |
ss"${safe(value)}" | X | √ | √ |
ss"${raw(value)}" | √ | √ | √ |
To declare whole types to be safe to print without using the safe
keyword, just create an implicit
instance of [[Safe]] for that type.
For example:
val user = User(name = "Jeff", password = "12345") // Hey, it's easy to remember
println(ss"User created successfully: $user")
^ compile error
In order to use this type of value, you must define an implicit instance of Safe[User]
. Typically,
it is best to put this on the companion object so that you never have to import it.
case class User(name: String, password: String)
object User {
implicit val safe: Safe[User] = new Safe[User] {
override def write(value: User): String = {
value.map(password = encrypt(value.password)).toString
}
}
}
And voila! Now when printing an instance of User
in a secure string, the Safe[User]
will be used to
ensure that the password is encrypted before being printed to a log or something.
And now this will print the user with an encrypted password field:
println(ss"User created successfully: $user")
You can chain Safe types together so that you can reuse the serialization logic and be checked at compile-time.
For example:
trait Encrypted[T] {
def bytes: Array[Byte]
def decrypt(implicit service: DecryptionService): T
}
object Encrypted {
implicit def SafeEncrypted[T](value: Encrypted[T]): Safe[Encrypted[T]] = {
new Safe[T] {
override def write(value: Encrypted[T]): String = new String(value.bytes, "UTF-8")
}
}
}
And now any value wrapped with Encrypted
will be stringifyed as UTF-8 encoded / encrypted bytes.
Alternatively, you could stringify it with a common log encryption key or whatever suits your fancy.
The main point is that you can reuse the Safe typeclass instances to print these values.
case class User(name: String, password: Encrypted[String])