Skip to content

Commit

Permalink
Tighten up types for encryption metadata.
Browse files Browse the repository at this point in the history
  • Loading branch information
mcdan committed Jan 31, 2020
1 parent a68f11e commit 10355da
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 81 deletions.
10 changes: 7 additions & 3 deletions common/scala/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -481,13 +481,17 @@ whisk {
# In general this setting should be left to its default disabled state
retry-no-http-response-exception = false
}
# Support key sizes of: 0, 16, 32. Which correspond to, no encryption, AES128 and AES256. For AES256 your JVM must be
# capable of that size key.
# Enabling this will start to encrypt all default parameters for actions and packages. Be careful using this as
# it will slowly migrate all the actions that have been 'updated' to use encrypted parameters but going back would
# require a currently non-existing migration step.
parameter-storage {
key = ""
# Base64 encoded 256 bit key
#aes256 = ""
# Base64 encoded 128 bit key
#aes128 = ""
# The current algorithm to use for parameter encryption, this can be changed but you have to leave all the keys
# configured for any algorithm you used previously.
#current = "aes128|aes256"
}
}
#placeholder for test overrides so that tests can override defaults in application.conf (todo: move all defaults to reference.conf)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
}
val encrypt = p._2.encryption match {
case (JsNull) => None
case _ => Some("encryption" -> p._2.encryption.toJson)
case _ => Some("encryption" -> p._2.encryption)
}
// Have do use this slightly strange construction to get the json object order identical.
JsObject(ListMap() ++ encrypt ++ init ++ Map("key" -> p._1.name.toJson, "value" -> p._2.value.toJson))
Expand All @@ -105,7 +105,7 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
if (p._2.encryption == JsNull)
p._2.value.toJson
else
JsObject("value" -> p._2.value.toJson, "encryption" -> p._2.encryption.toJson, "init" -> p._2.init.toJson)
JsObject("value" -> p._2.value.toJson, "encryption" -> p._2.encryption, "init" -> p._2.init.toJson)
(p._1.name, newValue)
}))

Expand Down Expand Up @@ -148,7 +148,7 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para

/**
* A ParameterName is a parameter name for an action or trigger to bind to its environment.
* It wraps a normalized string as a valueread type.
* It wraps a normalized string as a value type.
*
* It is a value type (hence == is .equals, immutable and cannot be assigned null).
* The constructor is private so that argument requirements are checked and normalized
Expand All @@ -175,10 +175,11 @@ protected[entity] class ParameterName protected[entity] (val name: String) exten
*
* @param v the value of the parameter, may be null
* @param init if true, this parameter value is only offered to the action during initialization
* @param encryptionDetails the name of the encrypter used to store the parameter.
*/
protected[entity] case class ParameterValue protected[entity] (private val v: JsValue,
val init: Boolean,
val e: JsValue = JsNull) {
val encryptionDetails: Option[JsString] = None) {

/** @return JsValue if defined else JsNull. */
protected[entity] def value = Option(v) getOrElse JsNull
Expand All @@ -187,7 +188,7 @@ protected[entity] case class ParameterValue protected[entity] (private val v: Js
protected[entity] def isDefined = value != JsNull

/** @return JsValue if defined else JsNull. */
protected[entity] def encryption = Option(e).getOrElse(JsNull)
protected[entity] def encryption = encryptionDetails getOrElse JsNull

/**
* The size of the ParameterValue entity as ByteSize.
Expand Down Expand Up @@ -217,7 +218,7 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
protected[core] def apply(p: String, v: String, init: Boolean = false): Parameters = {
require(p != null && p.trim.nonEmpty, "key undefined")
Parameters() + (new ParameterName(ArgNormalizer.trim(p)),
ParameterValue(Option(v).map(_.trim.toJson).getOrElse(JsNull), init, JsNull))
ParameterValue(Option(v).map(_.trim.toJson).getOrElse(JsNull), init, None))
}

/**
Expand All @@ -233,7 +234,7 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
protected[core] def apply(p: String, v: JsValue, init: Boolean): Parameters = {
require(p != null && p.trim.nonEmpty, "key undefined")
Parameters() + (new ParameterName(ArgNormalizer.trim(p)),
ParameterValue(Option(v).getOrElse(JsNull), init, JsNull))
ParameterValue(Option(v).getOrElse(JsNull), init, None))
}

/**
Expand All @@ -248,7 +249,7 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
protected[core] def apply(p: String, v: JsValue): Parameters = {
require(p != null && p.trim.nonEmpty, "key undefined")
Parameters() + (new ParameterName(ArgNormalizer.trim(p)),
ParameterValue(Option(v).getOrElse(JsNull), false, JsNull))
ParameterValue(Option(v).getOrElse(JsNull), false, None))
}

def readMergedList(value: JsValue): Parameters =
Expand All @@ -261,11 +262,11 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
val paramVal: ParameterValue = tuple._2 match {
case o: JsObject =>
o.getFields("value", "init", "encryption") match {
case Seq(v: JsValue, JsBoolean(i), e: JsValue) =>
ParameterValue(v, i, e)
case _ => ParameterValue(o, false, JsNull)
case Seq(v: JsValue, JsBoolean(i), e: JsString) =>
ParameterValue(v, i, Some(e))
case _ => ParameterValue(o, false, None)
}
case v: JsValue => ParameterValue(v, false, JsNull)
case v: JsValue => ParameterValue(v, false, None)
}
(key, paramVal)
})
Expand Down Expand Up @@ -295,9 +296,13 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
val JsObject(o) = value
o.foreach(i =>
i._2.asJsObject.getFields("value", "init", "encryption") match {
case Seq(v: JsValue, JsBoolean(init), e: JsValue) if e != JsNull =>
val key = new ParameterName(i._1)
val value = ParameterValue(v, init, Some(JsString(e.toString())))
converted = converted + (key -> value)
case Seq(v: JsValue, JsBoolean(init), e: JsValue) =>
val key = new ParameterName(i._1)
val value = ParameterValue(v, init, e)
val value = ParameterValue(v, init, None)
converted = converted + (key -> value)
})
if (converted.size == 0) {
Expand Down Expand Up @@ -325,17 +330,17 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
val key = new ParameterName(k)
val value = ParameterValue(v, false)
(key, value)
case Seq(JsString(k), v: JsValue, JsBoolean(i), e: JsValue) =>
case Seq(JsString(k), v: JsValue, JsBoolean(i), e: JsString) =>
val key = new ParameterName(k)
val value = ParameterValue(v, i, e)
val value = ParameterValue(v, i, Some(e))
(key, value)
case Seq(JsString(k), v: JsValue, JsBoolean(i)) =>
val key = new ParameterName(k)
val value = ParameterValue(v, i)
(key, value)
case Seq(JsString(k), v: JsValue, e: JsValue) if (i.asJsObject.fields.contains("encryption")) =>
case Seq(JsString(k), v: JsValue, e: JsString) if (i.asJsObject.fields.contains("encryption")) =>
val key = new ParameterName(k)
val value = ParameterValue(v, false, e)
val value = ParameterValue(v, false, None)
(key, value)
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,55 +29,66 @@ import spray.json.DefaultJsonProtocol._
import spray.json.{JsNull, JsString}
import pureconfig.generic.auto._

private trait encrypter {
def encrypt(p: ParameterValue): ParameterValue
def decrypt(p: ParameterValue): ParameterValue
val name: String
}

case class ParameterStorageConfig(key: String = "") {
def getKeyBytes(): Array[Byte] = {
if (key.length == 0) {
Array[Byte]()
} else {
Base64.getDecoder().decode(key)
}
}

}
case class ParameterStorageConfig(current: String = "", aes128: String = "", aes256: String = "")

object ParameterEncryption {
private val storageConfigLoader = loadConfig[ParameterStorageConfig](ConfigKeys.parameterStorage)
var storageConfig = storageConfigLoader.getOrElse(ParameterStorageConfig.apply())
private def enc = storageConfig.getKeyBytes.length match {
case 16 => new Aes128(storageConfig.getKeyBytes)
case 32 => new Aes256(storageConfig.getKeyBytes)
case 0 => new NoopCrypt
case _ =>
throw new IllegalArgumentException(
s"Only 0, 16 and 32 characters support for key size but instead got ${storageConfig.getKeyBytes.length}")
}
def lock(params: Parameters): Parameters = {
val configuredEncryptors = new encrypters(storageConfig)
new Parameters(
params.getMap
.map(({
case (paramName, paramValue) if paramValue.encryption == JsNull =>
paramName -> enc.encrypt(paramValue)
paramName -> configuredEncryptors.getCurrentEncrypter().encrypt(paramValue)
case (paramName, paramValue) => paramName -> paramValue
})))
}
def unlock(params: Parameters): Parameters = {
val configuredEncryptors = new encrypters(storageConfig)
new Parameters(
params.getMap
.map(({
case (paramName, paramValue)
if paramValue.encryption != JsNull && paramValue.encryption.convertTo[String] == enc.name =>
paramName -> enc.decrypt(paramValue)
if paramValue.encryption != JsNull && !configuredEncryptors
.getEncrypter(paramValue.encryption.convertTo[String])
.isEmpty =>
paramName -> configuredEncryptors
.getEncrypter(paramValue.encryption.convertTo[String])
.get
.decrypt(paramValue)
case (paramName, paramValue) => paramName -> paramValue
})))
}
}

private trait encrypter {
def encrypt(p: ParameterValue): ParameterValue
def decrypt(p: ParameterValue): ParameterValue
val name: String
}

private class encrypters(val storageConfig: ParameterStorageConfig) {
private val availableEncrypters = Map("" -> new NoopCrypt()) ++
(if (!storageConfig.aes256.isEmpty) Some(Aes256.name -> new Aes256(getKeyBytes(storageConfig.aes256))) else None) ++
(if (!storageConfig.aes128.isEmpty) Some(Aes128.name -> new Aes128(getKeyBytes(storageConfig.aes128))) else None)

protected[entity] def getCurrentEncrypter(): encrypter = {
availableEncrypters.get(ParameterEncryption.storageConfig.current).get
}
protected[entity] def getEncrypter(name: String) = {
availableEncrypters.get(name)
}

def getKeyBytes(key: String): Array[Byte] = {
if (key.length == 0) {
Array[Byte]()
} else {
Base64.getDecoder().decode(key)
}
}
}

private trait AesEncryption extends encrypter {
val key: Array[Byte]
val ivLen: Int
Expand All @@ -88,7 +99,6 @@ private trait AesEncryption extends encrypter {
private val secureRandom = new SecureRandom()

def encrypt(value: ParameterValue): ParameterValue = {

val iv = new Array[Byte](ivLen)
secureRandom.nextBytes(iv)
val gcmSpec = new GCMParameterSpec(tLen, iv)
Expand All @@ -102,7 +112,7 @@ private trait AesEncryption extends encrypter {
byteBuffer.put(iv)
byteBuffer.put(cipherText)
val cipherMessage = byteBuffer.array
ParameterValue(JsString(Base64.getEncoder.encodeToString(cipherMessage)), value.init, JsString(name))
ParameterValue(JsString(Base64.getEncoder.encodeToString(cipherMessage)), value.init, Some(JsString(name)))
}

def decrypt(value: ParameterValue): ParameterValue = {
Expand All @@ -127,11 +137,17 @@ private trait AesEncryption extends encrypter {

}

private case class Aes128(val key: Array[Byte], val ivLen: Int = 12, val name: String = "aes128")
private object Aes128 {
val name: String = "aes128"
}
private case class Aes128(val key: Array[Byte], val ivLen: Int = 12, val name: String = Aes128.name)
extends AesEncryption
with encrypter

private case class Aes256(val key: Array[Byte], val ivLen: Int = 128, val name: String = "aes256")
private object Aes256 {
val name: String = "aes256"
}
private case class Aes256(val key: Array[Byte], val ivLen: Int = 128, val name: String = Aes256.name)
extends AesEncryption
with encrypter

Expand Down
Loading

0 comments on commit 10355da

Please sign in to comment.