Skip to content

Commit a1ba987

Browse files
authored
Improvements to parameter encryption to support per-namespace keys (#4855)
* Review notes and refactoring. No intended semantic change. * Remove 'strange construction' of param as json. Simplify expression. * Remove unnecessary cons of Encrypter class. * Refactoring of encryptor names. * Move lock/unlock to Parameters. Refactor tests. * Partition params into locked and unlocked sets. * Remove getter, make field protected/accessible for test. * Comments. * Revert changes to test suite. * Exclude overriden parameters from decryption. * Tighten tests. Add test for unlocking args in container proxy. * Fix test.
1 parent db59f6f commit a1ba987

File tree

18 files changed

+408
-379
lines changed

18 files changed

+408
-379
lines changed

ansible/group_vars/all

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ whisk:
4949
version:
5050
date: "{{ansible_date_time.iso8601}}"
5151
feature_flags:
52-
require_api_key_annotation: "{{ require_api_key_annotation | default(true) | lower }}"
53-
require_response_payload: "{{ require_response_payload | default(true) | lower }}"
52+
require_api_key_annotation: "{{ require_api_key_annotation | default(true) | lower }}"
53+
require_response_payload: "{{ require_response_payload | default(true) | lower }}"
5454

5555
##
5656
# configuration parameters related to support runtimes (see org.apache.openwhisk.core.entity.ExecManifest for schema of the manifest).

common/scala/src/main/resources/application.conf

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -567,13 +567,17 @@ whisk {
567567
# it will slowly migrate all the actions that have been 'updated' to use encrypted parameters but going back would
568568
# require a currently non-existing migration step.
569569
parameter-storage {
570-
# Base64 encoded 256 bit key
571-
#aes-256 = ""
572-
# Base64 encoded 128 bit key
573-
#aes-128 = ""
574570
# The current algorithm to use for parameter encryption, this can be changed but you have to leave all the keys
575571
# configured for any algorithm you used previously.
576-
#current = "aes-128|aes-256"
572+
# Allowed values:
573+
# "off|noop" -> no op/no encryption
574+
# "aes-128" -> AES with 128 bit key (given as base64 encoded string)
575+
# "aes-256" -> AES with 256 bit key (given as base64 encoded string)
576+
current = "off"
577+
# Base64 encoded 128 bit key
578+
#aes-128 = ""
579+
# Base64 encoded 256 bit key
580+
#aes-256 = ""
577581
}
578582
}
579583
#placeholder for test overrides so that tests can override defaults in application.conf (todo: move all defaults to reference.conf)

common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ case class ActivationMessage(override val transid: TransactionId,
5757
blocking: Boolean,
5858
content: Option[JsObject],
5959
initArgs: Set[String] = Set.empty,
60+
lockedArgs: Map[String, String] = Map.empty,
6061
cause: Option[ActivationId] = None,
6162
traceContext: Option[Map[String, String]] = None)
6263
extends Message {
@@ -171,7 +172,7 @@ object ActivationMessage extends DefaultJsonProtocol {
171172
def parse(msg: String) = Try(serdes.read(msg.parseJson))
172173

173174
private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
174-
implicit val serdes = jsonFormat11(ActivationMessage.apply)
175+
implicit val serdes = jsonFormat12(ActivationMessage.apply)
175176
}
176177

177178
object CombinedCompletionAndResultMessage extends DefaultJsonProtocol {

common/scala/src/main/scala/org/apache/openwhisk/core/entity/Parameter.scala

Lines changed: 91 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717

1818
package org.apache.openwhisk.core.entity
1919

20-
import org.apache.openwhisk.core.entity.size.{SizeInt, SizeString}
20+
import scala.util.{Failure, Success, Try}
2121
import spray.json.DefaultJsonProtocol._
2222
import spray.json._
2323

24-
import scala.collection.immutable.ListMap
2524
import scala.language.postfixOps
26-
import scala.util.{Failure, Success, Try}
25+
import org.apache.openwhisk.core.entity.size.SizeInt
26+
import org.apache.openwhisk.core.entity.size.SizeString
2727

2828
/**
2929
* Parameters is a key-value map from parameter names to parameter values. The value of a
@@ -32,7 +32,7 @@ import scala.util.{Failure, Success, Try}
3232
* @param key the parameter name, assured to be non-null because it is a value
3333
* @param value the parameter value, assured to be non-null because it is a value
3434
*/
35-
protected[core] class Parameters protected[entity] (private val params: Map[ParameterName, ParameterValue])
35+
protected[core] class Parameters protected[entity] (protected[entity] val params: Map[ParameterName, ParameterValue])
3636
extends AnyVal {
3737

3838
/**
@@ -46,13 +46,6 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
4646
.foldLeft(0 B)(_ + _)
4747
}
4848

49-
protected[entity] def +(p: (ParameterName, ParameterValue)) = {
50-
51-
Option(p) map { p =>
52-
new Parameters(params + (p._1 -> p._2))
53-
} getOrElse this
54-
}
55-
5649
protected[entity] def +(p: ParameterName, v: ParameterValue) = {
5750
new Parameters(params + (p -> v))
5851
}
@@ -71,43 +64,35 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
7164
Try(new Parameters(params - new ParameterName(p))) getOrElse this
7265
}
7366

74-
/** Gets list all defined parameters. */
67+
/** Gets set of all defined parameters. */
7568
protected[core] def definedParameters: Set[String] = {
7669
params.keySet filter (params(_).isDefined) map (_.name)
7770
}
7871

79-
/** Gets list all defined parameters. */
72+
/** Gets set of all defined parameters. */
8073
protected[core] def initParameters: Set[String] = {
8174
params.keySet filter (params(_).init) map (_.name)
8275
}
8376

84-
protected[core] def getMap = {
85-
params
77+
/**
78+
* Gets map of all locked (encrypted) parameters, excluding parameters from given set.
79+
*/
80+
protected[core] def lockedParameters(exclude: Set[String] = Set.empty): Map[String, String] = {
81+
params.collect {
82+
case p if p._2.encryption.isDefined && !exclude.contains(p._1.name) => (p._1.name -> p._2.encryption.get)
83+
}
8684
}
85+
8786
protected[core] def toJsArray = {
8887
JsArray(params map { p =>
89-
val init = p._2.init match {
90-
case true => Some("init" -> p._2.init.toJson)
91-
case _ => None
92-
}
93-
val encrypt = p._2.encryption match {
94-
case (JsNull) => None
95-
case _ => Some("encryption" -> p._2.encryption)
96-
}
97-
// Have do use this slightly strange construction to get the json object order identical.
98-
JsObject(ListMap() ++ encrypt ++ init ++ Map("key" -> p._1.name.toJson, "value" -> p._2.value.toJson))
88+
val init = if (p._2.init) Some("init" -> JsTrue) else None
89+
val encrypt = p._2.encryption.map(e => ("encryption" -> JsString(e)))
90+
91+
JsObject(Map("key" -> p._1.name.toJson, "value" -> p._2.value) ++ init ++ encrypt)
9992
} toSeq: _*)
10093
}
10194

102-
protected[core] def toJsObject =
103-
JsObject(params.map(p => {
104-
val newValue =
105-
if (p._2.encryption == JsNull)
106-
p._2.value.toJson
107-
else
108-
JsObject("value" -> p._2.value.toJson, "encryption" -> p._2.encryption, "init" -> p._2.init.toJson)
109-
(p._1.name, newValue)
110-
}))
95+
protected[core] def toJsObject = JsObject(params.map(p => (p._1.name -> p._2.value.toJson)))
11196

11297
override def toString = toJsArray.compactPrint
11398

@@ -144,6 +129,40 @@ protected[core] class Parameters protected[entity] (private val params: Map[Para
144129
case _ => true
145130
} getOrElse valueForNonExistent
146131
}
132+
133+
/**
134+
* Encrypts any parameters that are not yet encoded.
135+
*
136+
* @param encoder the encoder to transform parameter values with
137+
* @return parameters with all values encrypted
138+
*/
139+
def lock(encoder: Option[Encrypter] = None): Parameters = {
140+
encoder
141+
.map { coder =>
142+
new Parameters(params.map {
143+
case (paramName, paramValue) if paramValue.encryption.isEmpty =>
144+
paramName -> coder.encrypt(paramValue)
145+
case p => p
146+
})
147+
}
148+
.getOrElse(this)
149+
}
150+
151+
/**
152+
* Decodes parameters. If the encryption scheme for a parameter is not recognized, it is not modified.
153+
*
154+
* @param decoder the decoder to use to transform locked values
155+
* @return parameters will all values decoded (where scheme is known)
156+
*/
157+
def unlock(decoder: ParameterEncryption): Parameters = {
158+
new Parameters(params.map {
159+
case p @ (paramName, paramValue) =>
160+
paramValue.encryption
161+
.map(paramName -> decoder.encryptor(_).decrypt(paramValue))
162+
.getOrElse(p)
163+
})
164+
}
165+
147166
}
148167

149168
/**
@@ -175,21 +194,18 @@ protected[entity] class ParameterName protected[entity] (val name: String) exten
175194
*
176195
* @param v the value of the parameter, may be null
177196
* @param init if true, this parameter value is only offered to the action during initialization
178-
* @param encryptionDetails the name of the encrypter used to store the parameter.
197+
* @param encryption the name of the encryption algorithm used to store the parameter or none (plain text)
179198
*/
180199
protected[entity] case class ParameterValue protected[entity] (private val v: JsValue,
181200
val init: Boolean,
182-
val encryptionDetails: Option[JsString] = None) {
201+
val encryption: Option[String] = None) {
183202

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

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

190-
/** @return JsValue if defined else JsNull. */
191-
protected[entity] def encryption = encryptionDetails getOrElse JsNull
192-
193209
/**
194210
* The size of the ParameterValue entity as ByteSize.
195211
*/
@@ -208,8 +224,8 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
208224
* Creates a parameter tuple from a pair of strings.
209225
* A convenience method for tests.
210226
*
211-
* @param p the parameter name
212-
* @param v the parameter value
227+
* @param p the parameter name
228+
* @param v the parameter value
213229
* @param init the parameter is for initialization
214230
* @return (ParameterName, ParameterValue)
215231
* @throws IllegalArgumentException if key is not defined
@@ -224,8 +240,8 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
224240
/**
225241
* Creates a parameter tuple from a parameter name and JsValue.
226242
*
227-
* @param p the parameter name
228-
* @param v the parameter value
243+
* @param p the parameter name
244+
* @param v the parameter value
229245
* @param init the parameter is for initialization
230246
* @return (ParameterName, ParameterValue)
231247
* @throws IllegalArgumentException if key is not defined
@@ -252,29 +268,6 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
252268
ParameterValue(Option(v).getOrElse(JsNull), false, None))
253269
}
254270

255-
def readMergedList(value: JsValue): Parameters =
256-
Try {
257-
258-
val JsObject(obj) = value
259-
new Parameters(
260-
obj
261-
.map((tuple: (String, JsValue)) => {
262-
val key = new ParameterName(tuple._1)
263-
val paramVal: ParameterValue = tuple._2 match {
264-
case o: JsObject =>
265-
o.getFields("value", "init", "encryption") match {
266-
case Seq(v: JsValue, JsBoolean(i), e: JsString) =>
267-
ParameterValue(v, i, Some(e))
268-
case _ => ParameterValue(o, false, None)
269-
}
270-
case v: JsValue => ParameterValue(v, false, None)
271-
}
272-
(key, paramVal)
273-
})
274-
.toMap)
275-
} getOrElse deserializationError(
276-
"parameters malformed, could not get a JsObject from: " + (if (value != null) value.toString() else ""))
277-
278271
override protected[core] implicit val serdes = new RootJsonFormat[Parameters] {
279272
def write(p: Parameters) = p.toJsArray
280273

@@ -285,35 +278,12 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
285278
* @param parameters the JSON representation of an parameter array
286279
* @return Parameters instance if parameters conforms to schema
287280
*/
288-
def read(value: JsValue) =
289-
Try {
290-
val JsArray(params) = value
291-
params
292-
} flatMap {
293-
read(_)
294-
} getOrElse {
295-
Try {
296-
var converted = new ListMap[ParameterName, ParameterValue]()
297-
val JsObject(o) = value
298-
o.foreach(i =>
299-
i._2.asJsObject.getFields("value", "init", "encryption") match {
300-
case Seq(v: JsValue, JsBoolean(init), e: JsValue) if e != JsNull =>
301-
val key = new ParameterName(i._1)
302-
val value = ParameterValue(v, init, Some(JsString(e.convertTo[String])))
303-
converted = converted + (key -> value)
304-
case Seq(v: JsValue, JsBoolean(init), e: JsValue) =>
305-
val key = new ParameterName(i._1)
306-
val value = ParameterValue(v, init, None)
307-
converted = converted + (key -> value)
308-
})
309-
if (converted.size == 0) {
310-
deserializationError("parameters malformed no parameters available: " + value.toString())
311-
} else {
312-
new Parameters(converted)
313-
}
314-
} getOrElse deserializationError(
315-
"parameters malformed could not read directly: " + (if (value != null) value.toString() else ""))
281+
def read(value: JsValue): Parameters = {
282+
value match {
283+
case JsArray(params) => read(params).getOrElse(deserializationError("parameters malformed!"))
284+
case _ => deserializationError("parameters malformed!")
316285
}
286+
}
317287

318288
/**
319289
* Gets parameters as a Parameters instances.
@@ -323,29 +293,33 @@ protected[core] object Parameters extends ArgNormalizer[Parameters] {
323293
* @return Parameters instance if parameters conforms to schema
324294
*/
325295
def read(params: Vector[JsValue]) = Try {
326-
new Parameters(
327-
params
328-
.map(i => {
329-
i.asJsObject.getFields("key", "value", "init", "encryption") match {
330-
case Seq(JsString(k), v: JsValue) =>
331-
val key = new ParameterName(k)
332-
val value = ParameterValue(v, false)
333-
(key, value)
334-
case Seq(JsString(k), v: JsValue, JsBoolean(i), e: JsString) =>
335-
val key = new ParameterName(k)
336-
val value = ParameterValue(v, i, Some(e))
337-
(key, value)
338-
case Seq(JsString(k), v: JsValue, JsBoolean(i)) =>
339-
val key = new ParameterName(k)
340-
val value = ParameterValue(v, i)
341-
(key, value)
342-
case Seq(JsString(k), v: JsValue, e: JsString) if (i.asJsObject.fields.contains("encryption")) =>
343-
val key = new ParameterName(k)
344-
val value = ParameterValue(v, false, Some(e))
345-
(key, value)
346-
}
347-
})
348-
.toMap)
296+
new Parameters(params.map {
297+
case o @ JsObject(fields) =>
298+
o.getFields("key", "value", "init", "encryption") match {
299+
case Seq(JsString(k), v: JsValue) if fields.contains("value") =>
300+
val key = new ParameterName(k)
301+
val value = ParameterValue(v, false)
302+
(key, value)
303+
case Seq(JsString(k), v: JsValue, JsBoolean(i)) =>
304+
val key = new ParameterName(k)
305+
val value = ParameterValue(v, i)
306+
(key, value)
307+
case Seq(JsString(k), v: JsValue, JsBoolean(i), JsString(e)) =>
308+
val key = new ParameterName(k)
309+
val value = ParameterValue(v, i, Some(e))
310+
(key, value)
311+
case Seq(JsString(k), v: JsValue, JsBoolean(i), JsNull) =>
312+
val key = new ParameterName(k)
313+
val value = ParameterValue(v, i, None)
314+
(key, value)
315+
case Seq(JsString(k), v: JsValue, JsString(e))
316+
if fields.contains("value") && fields.contains("encryption") =>
317+
val key = new ParameterName(k)
318+
val value = ParameterValue(v, false, Some(e))
319+
(key, value)
320+
}
321+
case _ => deserializationError("invalid parameter")
322+
}.toMap)
349323
}
350324
}
351325
}

0 commit comments

Comments
 (0)