Skip to content

Commit

Permalink
Create AES128 and AES256 encryption for parameters
Browse files Browse the repository at this point in the history
Encrypt just before putting into the db
Decrypt only right before invoking the action
  • Loading branch information
mcdan committed Jan 9, 2020
1 parent d9e4c10 commit da7bd79
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 18 deletions.
8 changes: 8 additions & 0 deletions common/scala/src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,14 @@ 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 = ""
}
}
#placeholder for test overrides so that tests can override defaults in application.conf (todo: move all defaults to reference.conf)
test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,4 +265,6 @@ object ConfigKeys {
val swaggerUi = "whisk.swagger-ui"

val apacheClientConfig = "whisk.apache-client"

val parameterStorage = "whisk.parameter-storage"
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@

package org.apache.openwhisk.core.entity

import scala.util.{Failure, Success, Try}
import org.apache.openwhisk.core.entity.size.{SizeInt, SizeString}
import spray.json.DefaultJsonProtocol._
import spray.json._

import scala.language.postfixOps
import org.apache.openwhisk.core.entity.size.SizeInt
import org.apache.openwhisk.core.entity.size.SizeString
import scala.util.{Failure, Success, Try}

/**
* Parameters is a key-value map from parameter names to parameter values. The value of a
Expand All @@ -47,6 +46,7 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
}

protected[entity] def +(p: (ParameterName, ParameterValue)) = {

Option(p) map { p =>
new Parameters(params + (p._1 -> p._2))
} getOrElse this
Expand Down Expand Up @@ -80,15 +80,32 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
params.keySet filter (params(_).init) map (_.name)
}

protected[core] def getMap = {
params
}
protected[core] def toJsArray = {
JsArray(params map { p =>
if (p._2.init) {
JsObject("key" -> p._1.name.toJson, "value" -> p._2.value.toJson, "init" -> JsTrue)
} else JsObject("key" -> p._1.name.toJson, "value" -> p._2.value.toJson)
val init = p._2.init match {
case true => Some("init" -> p._2.init.toJson)
case _ => None
}
val encrypt = p._2.encryption match {
case (JsNull) => None
case _ => Some("encryption" -> p._2.encryption.toJson)
}
JsObject(Map("key" -> p._1.name.toJson, "value" -> p._2.value.toJson) ++ init ++ encrypt)
} toSeq: _*)
}

protected[core] def toJsObject = JsObject(params.map(p => (p._1.name -> p._2.value.toJson)))
protected[core] def toJsObject =
JsObject(params.map(p => {
val newValue =
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)
(p._1.name, newValue)
}))

override def toString = toJsArray.compactPrint

Expand Down Expand Up @@ -157,14 +174,19 @@ 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
*/
protected[entity] case class ParameterValue protected[entity] (private val v: JsValue, val init: Boolean) {
protected[entity] case class ParameterValue protected[entity] (private val v: JsValue,
val init: Boolean,
val e: JsValue = JsNull) {

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

/** @return true iff value is not JsNull. */
protected[entity] def isDefined = value != JsNull

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

/**
* The size of the ParameterValue entity as ByteSize.
*/
Expand Down Expand Up @@ -243,7 +265,27 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
params
} flatMap {
read(_)
} getOrElse deserializationError("parameters malformed!")
} getOrElse {
// Used when the container proxy is reading back a merged version of the params.
Try {
var converted = Map[ParameterName, ParameterValue]()
new Parameters(value.asJsObject.fields.map { item: (String, JsValue) =>
{
item._2 match {
case JsString(s) => (new ParameterName(item._1), new ParameterValue(JsString(s), false))
case _ => {
item._2.asJsObject.getFields("value", "init", "encryption") match {
case Seq(v: JsValue, JsBoolean(i), e: JsValue) =>
(new ParameterName(item._1), new ParameterValue(v, i, e))
}
}
}
}
})
} getOrElse {
deserializationError("parameters malformed!")
}
}

/**
* Gets parameters as a Parameters instances.
Expand All @@ -254,11 +296,19 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
*/
def read(params: Vector[JsValue]) = Try {
new Parameters(params map {
_.asJsObject.getFields("key", "value", "init") match {
_.asJsObject.getFields("key", "value", "init", "encryption") match {
case Seq(JsString(k), v: JsValue) =>
val key = new ParameterName(k)
val value = ParameterValue(v, false)
(key, value)
case Seq(JsString(k), v: JsValue, JsBoolean(i), e: JsValue) =>
val key = new ParameterName(k)
val value = ParameterValue(v, i, e)
(key, value)
case Seq(JsString(k), v: JsValue, e: JsValue) =>
val key = new ParameterName(k)
val value = ParameterValue(v, false, e)
(key, value)
case Seq(JsString(k), v: JsValue, JsBoolean(i)) =>
val key = new ParameterName(k)
val value = ParameterValue(v, i)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.openwhisk.core.entity

import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.util.Base64

import javax.crypto.Cipher
import javax.crypto.spec.{GCMParameterSpec, SecretKeySpec}
import org.apache.openwhisk.core.ConfigKeys
import pureconfig.loadConfig
import spray.json.DefaultJsonProtocol._
import spray.json.{JsNull, JsString}

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

case class ParameterStorageConfig(key: String = "")

object ParameterEncryption {

private val storageConfigLoader = loadConfig[ParameterStorageConfig](ConfigKeys.parameterStorage)
var storageConfig = storageConfigLoader.getOrElse(ParameterStorageConfig.apply())
private val enc: encrypter = storageConfig.key.length match {
case 16 => new Aes128(storageConfig.key)
case 32 => new Aes256(storageConfig.key)
case 0 => new NoopCrypt
case _ => throw new IllegalArgumentException("Only 0, 16 and 32 characters support for key size.")
}

def lock(params: Parameters): Parameters = {
new Parameters(
params.getMap
.map(({
case (paramName, paramValue) if paramValue.encryption == JsNull =>
paramName -> enc.encrypt(paramValue)
case (paramName, paramValue) => paramName -> paramValue
})))
}
def unlock(params: Parameters): Parameters = {
new Parameters(
params.getMap
.map(({
case (paramName, paramValue)
if paramValue.encryption != JsNull && paramValue.encryption.convertTo[String] == enc.name =>
paramName -> enc.decrypt(paramValue)
case (paramName, paramValue) => paramName -> paramValue
})))
}
}

private trait AesEncryption extends encrypter {
val key: String
val ivLen: Int
val name: String
private val tLen = key.length * 8
private val secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES")

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)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)
val clearText = value.value.convertTo[String].getBytes(StandardCharsets.UTF_8)
val cipherText = cipher.doFinal(clearText)

val byteBuffer = ByteBuffer.allocate(4 + iv.length + cipherText.length)
byteBuffer.putInt(iv.length)
byteBuffer.put(iv)
byteBuffer.put(cipherText)
val cipherMessage = byteBuffer.array
ParameterValue(JsString(Base64.getEncoder.encodeToString(cipherMessage)), value.init, JsString(name))
}

def decrypt(value: ParameterValue): ParameterValue = {
val cipherMessage = value.value.convertTo[String].getBytes(StandardCharsets.UTF_8)
val byteBuffer = ByteBuffer.wrap(Base64.getDecoder.decode(cipherMessage))
val ivLength = byteBuffer.getInt
if (ivLength != ivLen) {
throw new IllegalArgumentException("invalid iv length")
}
val iv = new Array[Byte](ivLength)
byteBuffer.get(iv)
val cipherText = new Array[Byte](byteBuffer.remaining)
byteBuffer.get(cipherText)

val gcmSpec = new GCMParameterSpec(tLen, iv)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)
val plainTextBytes = cipher.doFinal(cipherText)
val plainText = new String(plainTextBytes, StandardCharsets.UTF_8)
ParameterValue(JsString(plainText), value.init)
}

}

private case class Aes128(val key: String, val ivLen: Int = 12, val name: String = "aes128")
extends AesEncryption
with encrypter

private case class Aes256(val key: String, val ivLen: Int = 128, val name: String = "aes256")
extends AesEncryption
with encrypter

private class NoopCrypt extends encrypter {
val name = "noop"
def encrypt(p: ParameterValue): ParameterValue = {
p
}

def decrypt(p: ParameterValue): ParameterValue = {
p
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,6 @@ case class ExecutableWhiskActionMetaData(namespace: EntityPath,

object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[WhiskAction] with DefaultJsonProtocol {
import WhiskActivation.instantSerdes

val execFieldName = "exec"
val requireWhiskAuthHeader = "x-require-whisk-auth"

Expand Down Expand Up @@ -382,9 +381,16 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[
(code.getBytes(UTF_8), ContentTypes.`text/plain(UTF-8)`)
}
val stream = new ByteArrayInputStream(bytes)
super.putAndAttach(db, doc, attachmentUpdater, attachmentType, stream, oldAttachment, Some { a: WhiskAction =>
a.copy(exec = exec.inline(code.getBytes(UTF_8)))
})
super.putAndAttach(
db,
doc.copy(parameters = ParameterEncryption.lock(doc.parameters)).revision[WhiskAction](doc.rev),
attachmentUpdater,
attachmentType,
stream,
oldAttachment,
Some { a: WhiskAction =>
a.copy(exec = exec.inline(code.getBytes(UTF_8)))
})
}

Try {
Expand All @@ -397,7 +403,10 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[
case exec @ BlackBoxExec(_, Some(Inline(code)), _, _, binary) =>
putWithAttachment(code, binary, exec)
case _ =>
super.put(db, doc, old)
super.put(
db,
doc.copy(parameters = ParameterEncryption.lock(doc.parameters)).revision[WhiskAction](doc.rev),
old)
}
} match {
case Success(f) => f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import spray.json.DefaultJsonProtocol
import spray.json.DefaultJsonProtocol._
import spray.json._
import org.apache.openwhisk.common.TransactionId
import org.apache.openwhisk.core.database.DocumentFactory
import org.apache.openwhisk.core.database.{ArtifactStore, CacheChangeNotification, DocumentFactory}
import org.apache.openwhisk.core.entity.types.EntityStore

/**
Expand Down Expand Up @@ -204,10 +204,15 @@ object WhiskPackage
}
jsonFormat8(WhiskPackage.apply)
}

override val cacheEnabled = true

lazy val publicPackagesView: View = WhiskQueries.entitiesView(collection = s"$collectionName-public")
// overriden to store encrypted parameters.
override def put[A >: WhiskPackage](db: ArtifactStore[A], doc: WhiskPackage, old: Option[WhiskPackage])(
implicit transid: TransactionId,
notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
super.put(db, doc.copy(parameters = ParameterEncryption.lock(doc.parameters)).revision[WhiskPackage](doc.rev), old)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,14 @@ class ContainerProxy(factory: (TransactionId,
*/
def initializeAndRun(container: Container, job: Run)(implicit tid: TransactionId): Future[WhiskActivation] = {
val actionTimeout = job.action.limits.timeout.duration
val (env, parameters) = ContainerProxy.partitionArguments(job.msg.content, job.msg.initArgs)
val unlockedContent = job.msg.content match {
case Some(js) => {
Some(ParameterEncryption.unlock(Parameters.serdes.read(js)).toJsObject)
}
case _ => job.msg.content
}

val (env, parameters) = ContainerProxy.partitionArguments(unlockedContent, job.msg.initArgs)

val environment = Map(
"namespace" -> job.msg.user.namespace.name.toJson,
Expand Down
Loading

0 comments on commit da7bd79

Please sign in to comment.