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.
diff --git a/ansible/group_vars/all b/ansible/group_vars/all
index 0c54c92..342ad1d 100644
--- a/ansible/group_vars/all
+++ b/ansible/group_vars/all
@@ -49,8 +49,8 @@
   version:
     date: "{{ansible_date_time.iso8601}}"
   feature_flags:
-    require_api_key_annotation: "{{ require_api_key_annotation | default(true) | lower  }}"
-    require_response_payload: "{{ require_response_payload | default(true) | lower  }}"
+    require_api_key_annotation: "{{ require_api_key_annotation | default(true) | lower }}"
+    require_response_payload: "{{ require_response_payload | default(true) | lower }}"
 
 ##
 # configuration parameters related to support runtimes (see org.apache.openwhisk.core.entity.ExecManifest for schema of the manifest).
diff --git a/common/scala/src/main/resources/application.conf b/common/scala/src/main/resources/application.conf
index 50a748a..ea96e92 100644
--- a/common/scala/src/main/resources/application.conf
+++ b/common/scala/src/main/resources/application.conf
@@ -567,13 +567,17 @@
     # 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 {
-        # Base64 encoded 256 bit key
-        #aes-256 = ""
-        # Base64 encoded 128 bit key
-        #aes-128 = ""
         # 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 = "aes-128|aes-256"
+        # Allowed values:
+        #   "off|noop" -> no op/no encryption
+        #   "aes-128"  -> AES with 128 bit key (given as base64 encoded string)
+        #   "aes-256"  -> AES with 256 bit key (given as base64 encoded string)
+        current = "off"
+        # Base64 encoded 128 bit key
+        #aes-128 = ""
+        # Base64 encoded 256 bit key
+        #aes-256 = ""
     }
 }
 #placeholder for test overrides so that tests can override defaults in application.conf (todo: move all defaults to reference.conf)
diff --git a/common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala b/common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala
index 5ca2a71..c86426d 100644
--- a/common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala
+++ b/common/scala/src/main/scala/org/apache/openwhisk/core/connector/Message.scala
@@ -57,6 +57,7 @@
                              blocking: Boolean,
                              content: Option[JsObject],
                              initArgs: Set[String] = Set.empty,
+                             lockedArgs: Map[String, String] = Map.empty,
                              cause: Option[ActivationId] = None,
                              traceContext: Option[Map[String, String]] = None)
     extends Message {
@@ -171,7 +172,7 @@
   def parse(msg: String) = Try(serdes.read(msg.parseJson))
 
   private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
-  implicit val serdes = jsonFormat11(ActivationMessage.apply)
+  implicit val serdes = jsonFormat12(ActivationMessage.apply)
 }
 
 object CombinedCompletionAndResultMessage extends DefaultJsonProtocol {
diff --git a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/Parameter.scala b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/Parameter.scala
index 89494fd..4a66867 100644
--- a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/Parameter.scala
+++ b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/Parameter.scala
@@ -17,13 +17,13 @@
 
 package org.apache.openwhisk.core.entity
 
-import org.apache.openwhisk.core.entity.size.{SizeInt, SizeString}
+import scala.util.{Failure, Success, Try}
 import spray.json.DefaultJsonProtocol._
 import spray.json._
 
-import scala.collection.immutable.ListMap
 import scala.language.postfixOps
-import scala.util.{Failure, Success, Try}
+import org.apache.openwhisk.core.entity.size.SizeInt
+import org.apache.openwhisk.core.entity.size.SizeString
 
 /**
  * Parameters is a key-value map from parameter names to parameter values. The value of a
@@ -32,7 +32,7 @@
  * @param key the parameter name, assured to be non-null because it is a value
  * @param value the parameter value, assured to be non-null because it is a value
  */
-protected[core] class Parameters protected[entity] (private val params: Map[ParameterName, ParameterValue])
+protected[core] class Parameters protected[entity] (protected[entity] val params: Map[ParameterName, ParameterValue])
     extends AnyVal {
 
   /**
@@ -46,13 +46,6 @@
       .foldLeft(0 B)(_ + _)
   }
 
-  protected[entity] def +(p: (ParameterName, ParameterValue)) = {
-
-    Option(p) map { p =>
-      new Parameters(params + (p._1 -> p._2))
-    } getOrElse this
-  }
-
   protected[entity] def +(p: ParameterName, v: ParameterValue) = {
     new Parameters(params + (p -> v))
   }
@@ -71,43 +64,35 @@
     Try(new Parameters(params - new ParameterName(p))) getOrElse this
   }
 
-  /** Gets list all defined parameters. */
+  /** Gets set of all defined parameters. */
   protected[core] def definedParameters: Set[String] = {
     params.keySet filter (params(_).isDefined) map (_.name)
   }
 
-  /** Gets list all defined parameters. */
+  /** Gets set of all defined parameters. */
   protected[core] def initParameters: Set[String] = {
     params.keySet filter (params(_).init) map (_.name)
   }
 
-  protected[core] def getMap = {
-    params
+  /**
+   * Gets map of all locked (encrypted) parameters, excluding parameters from given set.
+   */
+  protected[core] def lockedParameters(exclude: Set[String] = Set.empty): Map[String, String] = {
+    params.collect {
+      case p if p._2.encryption.isDefined && !exclude.contains(p._1.name) => (p._1.name -> p._2.encryption.get)
+    }
   }
+
   protected[core] def toJsArray = {
     JsArray(params map { p =>
-      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)
-      }
-      // 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))
+      val init = if (p._2.init) Some("init" -> JsTrue) else None
+      val encrypt = p._2.encryption.map(e => ("encryption" -> JsString(e)))
+
+      JsObject(Map("key" -> p._1.name.toJson, "value" -> p._2.value) ++ init ++ encrypt)
     } toSeq: _*)
   }
 
-  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, "init" -> p._2.init.toJson)
-      (p._1.name, newValue)
-    }))
+  protected[core] def toJsObject = JsObject(params.map(p => (p._1.name -> p._2.value.toJson)))
 
   override def toString = toJsArray.compactPrint
 
@@ -144,6 +129,40 @@
       case _            => true
     } getOrElse valueForNonExistent
   }
+
+  /**
+   * Encrypts any parameters that are not yet encoded.
+   *
+   * @param encoder the encoder to transform parameter values with
+   * @return parameters with all values encrypted
+   */
+  def lock(encoder: Option[Encrypter] = None): Parameters = {
+    encoder
+      .map { coder =>
+        new Parameters(params.map {
+          case (paramName, paramValue) if paramValue.encryption.isEmpty =>
+            paramName -> coder.encrypt(paramValue)
+          case p => p
+        })
+      }
+      .getOrElse(this)
+  }
+
+  /**
+   * Decodes parameters. If the encryption scheme for a parameter is not recognized, it is not modified.
+   *
+   * @param decoder the decoder to use to transform locked values
+   * @return parameters will all values decoded (where scheme is known)
+   */
+  def unlock(decoder: ParameterEncryption): Parameters = {
+    new Parameters(params.map {
+      case p @ (paramName, paramValue) =>
+        paramValue.encryption
+          .map(paramName -> decoder.encryptor(_).decrypt(paramValue))
+          .getOrElse(p)
+    })
+  }
+
 }
 
 /**
@@ -175,11 +194,11 @@
  *
  * @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.
+ * @param encryption the name of the encryption algorithm used to store the parameter or none (plain text)
  */
 protected[entity] case class ParameterValue protected[entity] (private val v: JsValue,
                                                                val init: Boolean,
-                                                               val encryptionDetails: Option[JsString] = None) {
+                                                               val encryption: Option[String] = None) {
 
   /** @return JsValue if defined else JsNull. */
   protected[entity] def value = Option(v) getOrElse JsNull
@@ -187,9 +206,6 @@
   /** @return true iff value is not JsNull. */
   protected[entity] def isDefined = value != JsNull
 
-  /** @return JsValue if defined else JsNull. */
-  protected[entity] def encryption = encryptionDetails getOrElse JsNull
-
   /**
    * The size of the ParameterValue entity as ByteSize.
    */
@@ -208,8 +224,8 @@
    * Creates a parameter tuple from a pair of strings.
    * A convenience method for tests.
    *
-   * @param p the parameter name
-   * @param v the parameter value
+   * @param p    the parameter name
+   * @param v    the parameter value
    * @param init the parameter is for initialization
    * @return (ParameterName, ParameterValue)
    * @throws IllegalArgumentException if key is not defined
@@ -224,8 +240,8 @@
   /**
    * Creates a parameter tuple from a parameter name and JsValue.
    *
-   * @param p the parameter name
-   * @param v the parameter value
+   * @param p    the parameter name
+   * @param v    the parameter value
    * @param init the parameter is for initialization
    * @return (ParameterName, ParameterValue)
    * @throws IllegalArgumentException if key is not defined
@@ -252,29 +268,6 @@
     ParameterValue(Option(v).getOrElse(JsNull), false, None))
   }
 
-  def readMergedList(value: JsValue): Parameters =
-    Try {
-
-      val JsObject(obj) = value
-      new Parameters(
-        obj
-          .map((tuple: (String, JsValue)) => {
-            val key = new ParameterName(tuple._1)
-            val paramVal: ParameterValue = tuple._2 match {
-              case o: JsObject =>
-                o.getFields("value", "init", "encryption") match {
-                  case Seq(v: JsValue, JsBoolean(i), e: JsString) =>
-                    ParameterValue(v, i, Some(e))
-                  case _ => ParameterValue(o, false, None)
-                }
-              case v: JsValue => ParameterValue(v, false, None)
-            }
-            (key, paramVal)
-          })
-          .toMap)
-    } getOrElse deserializationError(
-      "parameters malformed, could not get a JsObject from: " + (if (value != null) value.toString() else ""))
-
   override protected[core] implicit val serdes = new RootJsonFormat[Parameters] {
     def write(p: Parameters) = p.toJsArray
 
@@ -285,35 +278,12 @@
      * @param parameters the JSON representation of an parameter array
      * @return Parameters instance if parameters conforms to schema
      */
-    def read(value: JsValue) =
-      Try {
-        val JsArray(params) = value
-        params
-      } flatMap {
-        read(_)
-      } getOrElse {
-        Try {
-          var converted = new ListMap[ParameterName, ParameterValue]()
-          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.convertTo[String])))
-                converted = converted + (key -> value)
-              case Seq(v: JsValue, JsBoolean(init), e: JsValue) =>
-                val key = new ParameterName(i._1)
-                val value = ParameterValue(v, init, None)
-                converted = converted + (key -> value)
-          })
-          if (converted.size == 0) {
-            deserializationError("parameters malformed no parameters available: " + value.toString())
-          } else {
-            new Parameters(converted)
-          }
-        } getOrElse deserializationError(
-          "parameters malformed could not read directly: " + (if (value != null) value.toString() else ""))
+    def read(value: JsValue): Parameters = {
+      value match {
+        case JsArray(params) => read(params).getOrElse(deserializationError("parameters malformed!"))
+        case _               => deserializationError("parameters malformed!")
       }
+    }
 
     /**
      * Gets parameters as a Parameters instances.
@@ -323,29 +293,33 @@
      * @return Parameters instance if parameters conforms to schema
      */
     def read(params: Vector[JsValue]) = Try {
-      new Parameters(
-        params
-          .map(i => {
-            i.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: JsString) =>
-                val key = new ParameterName(k)
-                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: JsString) if (i.asJsObject.fields.contains("encryption")) =>
-                val key = new ParameterName(k)
-                val value = ParameterValue(v, false, Some(e))
-                (key, value)
-            }
-          })
-          .toMap)
+      new Parameters(params.map {
+        case o @ JsObject(fields) =>
+          o.getFields("key", "value", "init", "encryption") match {
+            case Seq(JsString(k), v: JsValue) if fields.contains("value") =>
+              val key = new ParameterName(k)
+              val value = ParameterValue(v, false)
+              (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, JsBoolean(i), JsString(e)) =>
+              val key = new ParameterName(k)
+              val value = ParameterValue(v, i, Some(e))
+              (key, value)
+            case Seq(JsString(k), v: JsValue, JsBoolean(i), JsNull) =>
+              val key = new ParameterName(k)
+              val value = ParameterValue(v, i, None)
+              (key, value)
+            case Seq(JsString(k), v: JsValue, JsString(e))
+                if fields.contains("value") && fields.contains("encryption") =>
+              val key = new ParameterName(k)
+              val value = ParameterValue(v, false, Some(e))
+              (key, value)
+          }
+        case _ => deserializationError("invalid parameter")
+      }.toMap)
     }
   }
 }
diff --git a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/ParameterEncryption.scala b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/ParameterEncryption.scala
index 482579c..2169eaf 100644
--- a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/ParameterEncryption.scala
+++ b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/ParameterEncryption.scala
@@ -26,79 +26,85 @@
 import org.apache.openwhisk.core.ConfigKeys
 import pureconfig.loadConfig
 import spray.json.DefaultJsonProtocol._
-import spray.json.{JsNull, JsString}
+import spray.json._
 import pureconfig.generic.auto._
 import spray.json._
-case class ParameterStorageConfig(current: String = "", aes128: String = "", aes256: String = "")
 
-object ParameterEncryption {
-  private val storageConfigLoader = loadConfig[ParameterStorageConfig](ConfigKeys.parameterStorage)
-  var storageConfig = storageConfigLoader.getOrElse(ParameterStorageConfig.apply())
-  def lock(params: Parameters): Parameters = {
-    val configuredEncryptors = new encrypters(storageConfig)
-    new Parameters(
-      params.getMap
-        .map(({
-          case (paramName, paramValue) if paramValue.encryption == JsNull =>
-            paramName -> configuredEncryptors.getCurrentEncrypter().encrypt(paramValue)
-          case (paramName, paramValue) => paramName -> paramValue
-        })))
+protected[core] case class ParameterStorageConfig(current: String = ParameterEncryption.NO_ENCRYPTION,
+                                                  aes128: Option[String] = None,
+                                                  aes256: Option[String] = None)
+
+protected[core] class ParameterEncryption(val default: Option[Encrypter], encryptors: Map[String, Encrypter]) {
+
+  /**
+   * Gets the coder for the given scheme name.
+   *
+   * @param name the name of the encryption algorithm (defaults to current from last configuration)
+   * @return the coder if there is one else no-op encryptor
+   */
+  def encryptor(name: String): Encrypter = {
+    encryptors.get(name).getOrElse(ParameterEncryption.noop)
   }
-  def unlock(params: Parameters): Parameters = {
-    val configuredEncryptors = new encrypters(storageConfig)
-    new Parameters(
-      params.getMap
-        .map(({
-          case (paramName, 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
-        })))
+
+}
+
+protected[core] object ParameterEncryption {
+
+  val NO_ENCRYPTION = "noop"
+  val AES128_ENCRYPTION = "aes-128"
+  val AES256_ENCRYPTION = "aes-256"
+
+  val noop = new Encrypter {
+    override val name = NO_ENCRYPTION
+  }
+
+  val singleton: ParameterEncryption = {
+    val configLoader = loadConfig[ParameterStorageConfig](ConfigKeys.parameterStorage)
+    val config = configLoader.getOrElse(ParameterStorageConfig(noop.name))
+    ParameterEncryption(config)
+  }
+
+  def apply(config: ParameterStorageConfig): ParameterEncryption = {
+    val availableEncoders = Map(noop.name -> noop) ++
+      config.aes128.map(k => AES128_ENCRYPTION -> new Aes128(k)) ++
+      config.aes256.map(k => AES256_ENCRYPTION -> new Aes256(k))
+
+    val current = config.current.toLowerCase match {
+      case "" | "off" | NO_ENCRYPTION => NO_ENCRYPTION
+      case s                          => s
+    }
+
+    val defaultEncoder: Encrypter = availableEncoders.get(current).getOrElse(noop)
+    new ParameterEncryption(Option(defaultEncoder).filter(_ != noop), availableEncoders)
   }
 }
 
-private trait encrypter {
-  def encrypt(p: ParameterValue): ParameterValue
-  def decrypt(p: ParameterValue): ParameterValue
+protected[core] trait Encrypter {
   val name: String
+  def encrypt(p: ParameterValue): ParameterValue = p
+  def decrypt(p: ParameterValue): ParameterValue = p
+  def decrypt(v: JsString): JsValue = v
 }
 
-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] = {
+protected[core] object Encrypter {
+  protected[entity] def getKeyBytes(key: String): Array[Byte] = {
     if (key.length == 0) {
-      Array[Byte]()
+      Array.empty
     } else {
-      Base64.getDecoder().decode(key)
+      Base64.getDecoder.decode(key)
     }
   }
 }
 
-private trait AesEncryption extends encrypter {
+protected[core] trait AesEncryption extends Encrypter {
   val key: Array[Byte]
   val ivLen: Int
   val name: String
   private val tLen = 128
-  private val secretKey = new SecretKeySpec(key, "AES")
-
   private val secureRandom = new SecureRandom()
+  private lazy val secretKey = new SecretKeySpec(key, "AES")
 
-  def encrypt(value: ParameterValue): ParameterValue = {
+  override def encrypt(value: ParameterValue): ParameterValue = {
     val iv = new Array[Byte](ivLen)
     secureRandom.nextBytes(iv)
     val gcmSpec = new GCMParameterSpec(tLen, iv)
@@ -112,11 +118,18 @@
     byteBuffer.put(iv)
     byteBuffer.put(cipherText)
     val cipherMessage = byteBuffer.array
-    ParameterValue(JsString(Base64.getEncoder.encodeToString(cipherMessage)), value.init, Some(JsString(name)))
+    ParameterValue(JsString(Base64.getEncoder.encodeToString(cipherMessage)), value.init, Some(name))
   }
 
-  def decrypt(value: ParameterValue): ParameterValue = {
-    val cipherMessage = value.value.convertTo[String].getBytes(StandardCharsets.UTF_8)
+  override def decrypt(p: ParameterValue): ParameterValue = {
+    p.value match {
+      case s: JsString => p.copy(v = decrypt(s), encryption = None)
+      case _           => p
+    }
+  }
+
+  override def decrypt(value: JsString): JsValue = {
+    val cipherMessage = value.convertTo[String].getBytes(StandardCharsets.UTF_8)
     val byteBuffer = ByteBuffer.wrap(Base64.getDecoder.decode(cipherMessage))
     val ivLength = byteBuffer.getInt
     if (ivLength != ivLen) {
@@ -132,32 +145,19 @@
     cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)
     val plainTextBytes = cipher.doFinal(cipherText)
     val plainText = new String(plainTextBytes, StandardCharsets.UTF_8)
-    ParameterValue(plainText.parseJson, value.init)
+    plainText.parseJson
   }
 
 }
 
-private object Aes128 {
-  val name: String = "aes-128"
+protected[core] class Aes128(val k: String) extends AesEncryption with Encrypter {
+  override val key = Encrypter.getKeyBytes(k)
+  override val name = ParameterEncryption.AES128_ENCRYPTION
+  override val ivLen = 12
 }
-private case class Aes128(val key: Array[Byte], val ivLen: Int = 12, val name: String = Aes128.name)
-    extends AesEncryption
-    with encrypter
 
-private object Aes256 {
-  val name: String = "aes-256"
-}
-private case class Aes256(val key: Array[Byte], val ivLen: Int = 128, val name: String = Aes256.name)
-    extends AesEncryption
-    with encrypter
-
-private class NoopCrypt extends encrypter {
-  val name = ""
-  def encrypt(p: ParameterValue): ParameterValue = {
-    p
-  }
-
-  def decrypt(p: ParameterValue): ParameterValue = {
-    p
-  }
+protected[core] class Aes256(val k: String) extends AesEncryption with Encrypter {
+  override val key = Encrypter.getKeyBytes(k)
+  override val name = ParameterEncryption.AES256_ENCRYPTION
+  override val ivLen = 128
 }
diff --git a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskAction.scala b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskAction.scala
index 6dd60a1..4370a17 100644
--- a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskAction.scala
+++ b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskAction.scala
@@ -348,6 +348,7 @@
 
 object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[WhiskAction] with DefaultJsonProtocol {
   import WhiskActivation.instantSerdes
+
   val execFieldName = "exec"
   val requireWhiskAuthHeader = "x-require-whisk-auth"
 
@@ -384,7 +385,9 @@
       val stream = new ByteArrayInputStream(bytes)
       super.putAndAttach(
         db,
-        doc.copy(parameters = ParameterEncryption.lock(doc.parameters)).revision[WhiskAction](doc.rev),
+        doc
+          .copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default))
+          .revision[WhiskAction](doc.rev),
         attachmentUpdater,
         attachmentType,
         stream,
@@ -406,7 +409,9 @@
         case _ =>
           super.put(
             db,
-            doc.copy(parameters = ParameterEncryption.lock(doc.parameters)).revision[WhiskAction](doc.rev),
+            doc
+              .copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default))
+              .revision[WhiskAction](doc.rev),
             old)
       }
     } match {
diff --git a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskPackage.scala b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskPackage.scala
index ec9e0ec..3acac54 100644
--- a/common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskPackage.scala
+++ b/common/scala/src/main/scala/org/apache/openwhisk/core/entity/WhiskPackage.scala
@@ -198,14 +198,19 @@
     }
     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)
+    super.put(
+      db,
+      doc.copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default)).revision[WhiskPackage](doc.rev),
+      old)
   }
 }
 
diff --git a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala
index 1ac1151..621a10e 100644
--- a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala
+++ b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/actions/PrimitiveActions.scala
@@ -179,6 +179,7 @@
       waitForResponse.isDefined,
       args,
       action.parameters.initParameters,
+      action.parameters.lockedParameters(payload.map(_.fields.keySet).getOrElse(Set.empty)),
       cause = cause,
       WhiskTracerProvider.tracer.getTraceContext(transid))
 
diff --git a/core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/InvokerSupervision.scala b/core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/InvokerSupervision.scala
index e1b7c64..5a9d367 100644
--- a/core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/InvokerSupervision.scala
+++ b/core/controller/src/main/scala/org/apache/openwhisk/core/loadBalancer/InvokerSupervision.scala
@@ -416,7 +416,8 @@
         rootControllerIndex = controllerInstance,
         blocking = false,
         content = None,
-        initArgs = Set.empty)
+        initArgs = Set.empty,
+        lockedArgs = Map.empty)
 
       context.parent ! ActivationRequest(activationMessage, invokerInstance)
     }
diff --git a/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala b/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala
index eebcc7c..d84947b 100644
--- a/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala
+++ b/core/invoker/src/main/scala/org/apache/openwhisk/core/containerpool/ContainerProxy.scala
@@ -754,6 +754,7 @@
     }
     hpa ! HealthPingEnabled(true)
   }
+
   private def disableHealthPing() = {
     healthPingActor.foreach(_ ! HealthPingEnabled(false))
   }
@@ -774,14 +775,10 @@
   def initializeAndRun(container: Container, job: Run, reschedule: Boolean = false)(
     implicit tid: TransactionId): Future[WhiskActivation] = {
     val actionTimeout = job.action.limits.timeout.duration
-    val unlockedContent = job.msg.content match {
-      case Some(js) => {
-        Some(ParameterEncryption.unlock(Parameters.readMergedList(js)).toJsObject)
-      }
-      case _ => job.msg.content
-    }
+    val unlockedArgs =
+      ContainerProxy.unlockArguments(job.msg.content, job.msg.lockedArgs, ParameterEncryption.singleton)
 
-    val (env, parameters) = ContainerProxy.partitionArguments(unlockedContent, job.msg.initArgs)
+    val (env, parameters) = ContainerProxy.partitionArguments(unlockedArgs, job.msg.initArgs)
 
     val environment = Map(
       "namespace" -> job.msg.user.namespace.name.toJson,
@@ -1094,6 +1091,18 @@
         (env, JsObject(args))
     }
   }
+
+  def unlockArguments(content: Option[JsObject],
+                      lockedArgs: Map[String, String],
+                      decoder: ParameterEncryption): Option[JsObject] = {
+    content.map {
+      case JsObject(fields) =>
+        JsObject(fields.map {
+          case (k, v: JsString) if lockedArgs.contains(k) => (k -> decoder.encryptor(lockedArgs(k)).decrypt(v))
+          case p                                          => p
+        })
+    }
+  }
 }
 
 object TCPPingClient {
diff --git a/tests/src/test/resources/application.conf.j2 b/tests/src/test/resources/application.conf.j2
index 5f61695..e2eaa0f 100644
--- a/tests/src/test/resources/application.conf.j2
+++ b/tests/src/test/resources/application.conf.j2
@@ -95,7 +95,7 @@
     }
 
     parameter-storage {
-        key = ""
+        current = "off"
     }
     
     elasticsearch {
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerPoolTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerPoolTests.scala
index 8c2fea5..63930fe 100644
--- a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerPoolTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerPoolTests.scala
@@ -87,7 +87,8 @@
       ControllerInstanceId("0"),
       blocking = false,
       content = None,
-      initArgs = Set.empty)
+      initArgs = Set.empty,
+      lockedArgs = Map.empty)
     Run(action, message)
   }
 
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala
index 968a5ea..dc8a33f 100644
--- a/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/core/containerpool/test/ContainerProxyTests.scala
@@ -125,7 +125,8 @@
     ControllerInstanceId("0"),
     blocking = false,
     content = Some(activationArguments),
-    initArgs = Set("ENV_VAR"))
+    initArgs = Set("ENV_VAR"),
+    lockedArgs = Map.empty)
 
   /*
    * Helpers for assertions and actor lifecycles
@@ -296,6 +297,18 @@
     }
   }
 
+  it should "unlock arguments" in {
+    val k128 = "ra1V6AfOYAv0jCzEdufIFA=="
+    val coder = ParameterEncryption(ParameterStorageConfig("aes-128", aes128 = Some(k128)))
+    val locker = Some(coder.encryptor("aes-128"))
+
+    val param = Parameters("a", "abc").lock(locker).merge(Some(JsObject("b" -> JsString("xyz"))))
+    param.get.compactPrint should not include "abc"
+    ContainerProxy.unlockArguments(param, Map("a" -> "aes-128"), coder) shouldBe Some {
+      JsObject("a" -> JsString("abc"), "b" -> JsString("xyz"))
+    }
+  }
+
   /*
    * SUCCESSFUL CASES
    */
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/ActionsApiTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/ActionsApiTests.scala
index 3917122..9a1a96e 100644
--- a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/ActionsApiTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/ActionsApiTests.scala
@@ -19,28 +19,29 @@
 
 import java.time.Instant
 
-import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.{sprayJsonMarshaller, sprayJsonUnmarshaller}
-import akka.http.scaladsl.model.StatusCodes._
-import akka.http.scaladsl.model.headers.RawHeader
-import akka.http.scaladsl.server.Route
-import org.apache.commons.lang3.StringUtils
-import org.apache.openwhisk.core.connector.ActivationMessage
-import org.apache.openwhisk.core.controller.WhiskActionsApi
-import org.apache.openwhisk.core.database.UserContext
-import org.apache.openwhisk.core.entitlement.Collection
-import org.apache.openwhisk.core.entity.Attachments.Inline
-import org.apache.openwhisk.core.entity._
-import org.apache.openwhisk.core.entity.size._
-import org.apache.openwhisk.core.entity.test.ExecHelpers
-import org.apache.openwhisk.http.{ErrorResponse, Messages}
-import org.junit.runner.RunWith
-import org.scalatest.junit.JUnitRunner
-import org.scalatest.{FlatSpec, Matchers}
-import spray.json.DefaultJsonProtocol._
-import spray.json._
-
 import scala.concurrent.duration.DurationInt
 import scala.language.postfixOps
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import akka.http.scaladsl.model.StatusCodes._
+import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller
+import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller
+import akka.http.scaladsl.server.Route
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import org.apache.openwhisk.core.controller.WhiskActionsApi
+import org.apache.openwhisk.core.entity._
+import org.apache.openwhisk.core.entity.size._
+import org.apache.openwhisk.core.entitlement.Collection
+import org.apache.openwhisk.http.ErrorResponse
+import org.apache.openwhisk.http.Messages
+import org.apache.openwhisk.core.database.UserContext
+import akka.http.scaladsl.model.headers.RawHeader
+import org.apache.commons.lang3.StringUtils
+import org.apache.openwhisk.core.connector.ActivationMessage
+import org.apache.openwhisk.core.entity.Attachments.Inline
+import org.apache.openwhisk.core.entity.test.ExecHelpers
+import org.scalatest.{FlatSpec, Matchers}
 
 /**
  * Tests Actions API.
@@ -223,22 +224,22 @@
     }
   }
 
-//  it should "ignore updated field when updating action" in {
-//    implicit val tid = transid()
-//
-//    val action = WhiskAction(namespace, aname(), jsDefault(""))
-//    val dummyUpdated = WhiskEntity.currentMillis().toEpochMilli
-//
-//    val content = JsObject(
-//      "exec" -> JsObject("code" -> "".toJson, "kind" -> action.exec.kind.toJson),
-//      "updated" -> dummyUpdated.toJson)
-//
-//    Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
-//      status should be(OK)
-//      val response = responseAs[WhiskAction]
-//      response.updated.toEpochMilli should be > dummyUpdated
-//    }
-//  }
+  it should "ignore updated field when updating action" in {
+    implicit val tid = transid()
+
+    val action = WhiskAction(namespace, aname(), jsDefault(""))
+    val dummyUpdated = WhiskEntity.currentMillis().toEpochMilli
+
+    val content = JsObject(
+      "exec" -> JsObject("code" -> "".toJson, "kind" -> action.exec.kind.toJson),
+      "updated" -> dummyUpdated.toJson)
+
+    Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
+      status should be(OK)
+      val response = responseAs[WhiskAction]
+      response.updated.toEpochMilli should be > dummyUpdated
+    }
+  }
 
   def getExecPermutations() = {
     implicit val tid = transid()
@@ -1702,9 +1703,9 @@
 
 @RunWith(classOf[JUnitRunner])
 class WhiskActionsApiTests extends FlatSpec with Matchers with ExecHelpers {
+  import WhiskActionsApi.amendAnnotations
   import Annotations.ProvideApiKeyAnnotationName
   import WhiskAction.execFieldName
-  import WhiskActionsApi.amendAnnotations
 
   val baseParams = Parameters("a", JsString("A")) ++ Parameters("b", JsString("B"))
   val keyTruthyAnnotation = Parameters(ProvideApiKeyAnnotationName, JsTrue)
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/entity/test/ParameterEncryptionTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/entity/test/ParameterEncryptionTests.scala
index c6aec18..8fe2a5d 100644
--- a/tests/src/test/scala/org/apache/openwhisk/core/entity/test/ParameterEncryptionTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/core/entity/test/ParameterEncryptionTests.scala
@@ -28,223 +28,235 @@
 @RunWith(classOf[JUnitRunner])
 class ParameterEncryptionTests extends FlatSpec with Matchers with BeforeAndAfter {
 
-  after {
-    ParameterEncryption.storageConfig = new ParameterStorageConfig("")
-  }
+  val k128 = "ra1V6AfOYAv0jCzEdufIFA=="
+  val k256 = "j5rLzhtxwzPyUVUy8/p8XJmBoKeDoSzNJP1SITJEY9E="
+
+  // default is no-op but keys are available to decode encoded params
+  val noop = ParameterEncryption(ParameterStorageConfig(aes128 = Some(k128), aes256 = Some(k256)))
+
+  val aes128decoder = ParameterEncryption(ParameterStorageConfig("aes-128", aes128 = Some(k128)))
+  val aes128encoder = aes128decoder.default
+
+  val aes256decoder = ParameterEncryption(ParameterStorageConfig("aes-256", aes256 = Some(k256)))
+  val aes256encoder = aes256decoder.default
 
   val parameters = new Parameters(
     Map(
       new ParameterName("one") -> new ParameterValue("secret".toJson, false),
       new ParameterName("two") -> new ParameterValue("secret".toJson, true)))
 
+  behavior of "ParameterEncryption"
+
+  it should "not have a default coder when turned off" in {
+    ParameterEncryption(ParameterStorageConfig("")).default shouldBe empty
+    ParameterEncryption(ParameterStorageConfig("off")).default shouldBe empty
+    ParameterEncryption(ParameterStorageConfig("noop")).default shouldBe empty
+    ParameterEncryption(ParameterStorageConfig("OFF")).default shouldBe empty
+    ParameterEncryption(ParameterStorageConfig("NOOP")).default shouldBe empty
+  }
+
   behavior of "Parameters"
-  it should "handle complex objects in param body" in {
-    val input =
-      """
-        |{
-        |    "__ow_headers": {
-        |        "accept": "*/*",
-        |        "accept-encoding": "gzip, deflate",
-        |        "host": "controllers",
-        |        "user-agent": "Apache-HttpClient/4.5.5 (Java/1.8.0_212)",
-        |        "x-request-id": "fd2263668266da5a5433109076191d95"
-        |    },
-        |    "__ow_method": "get",
-        |    "__ow_path": "/a",
-        |    "a": "A"
-        |}
-        |""".stripMargin
-    val ps = Parameters.readMergedList(input.parseJson)
-    ps.get("a").get.convertTo[String] shouldBe "A"
-  }
 
-  it should "handle decryption json objects" in {
+  it should "handle decryption of json objects" in {
     val originalValue =
       """
-        |{
-        |"paramName1":{"encryption":null,"init":false,"value":"from-action"},
-        |"paramName2":{"encryption":null,"init":false,"value":"from-pack"}
-        |}
+        |[
+        |  { "key": "paramName1", "init": false, "value": "from-action" },
+        |  { "key": "paramName2", "init": false, "value": "from-pack" }
+        |]
         |""".stripMargin
-    val ps = Parameters.serdes.read(originalValue.parseJson)
-    ps.get("paramName1").get.convertTo[String] shouldBe "from-action"
-    ps.get("paramName2").get.convertTo[String] shouldBe "from-pack"
+
+    val p = Parameters.serdes.read(originalValue.parseJson)
+    p.get("paramName1").get.convertTo[String] shouldBe "from-action"
+    p.get("paramName2").get.convertTo[String] shouldBe "from-pack"
+    p.params.foreach {
+      case (_, paramValue) =>
+        paramValue.encryption shouldBe empty
+    }
   }
 
-  it should "drop encryption payload when no longer encrypted" in {
+  it should "handle decryption of json objects with null field" in {
     val originalValue =
       """
-        |{
-        |"paramName1":{"encryption":null,"init":false,"value":"from-action"},
-        |"paramName2":{"encryption":null,"init":false,"value":"from-action"}
-        |}
+        |[
+        |  { "key": "paramName1", "encryption":null, "init": false, "value": "from-action" },
+        |  { "key": "paramName2", "encryption":null, "init": false, "value": "from-pack" }
+        |]
         |""".stripMargin
-    val ps = Parameters.serdes.read(originalValue.parseJson)
-    val o = ps.toJsObject
-    o.fields.map((tuple: (String, JsValue)) => {
-      tuple._2.convertTo[String] shouldBe "from-action"
-    })
+
+    val p = Parameters.serdes.read(originalValue.parseJson)
+    p.get("paramName1").get.convertTo[String] shouldBe "from-action"
+    p.get("paramName2").get.convertTo[String] shouldBe "from-pack"
+    p.params.foreach {
+      case (_, paramValue) =>
+        paramValue.encryption shouldBe empty
+    }
   }
 
-  it should "read the merged unencrypted parameters during mixed storage" in {
+  it should "drop encryption propery when no longer encrypted" in {
     val originalValue =
       """
-        |{"name":"from-action","other":"from-action"}
+        |[
+        |  { "key": "paramName1", "encryption":null, "init": false, "value": "from-action" },
+        |  { "key": "paramName2", "encryption":null, "init": false, "value": "from-pack" }
+        |]
         |""".stripMargin
-    val ps = Parameters.readMergedList(originalValue.parseJson)
-    val o = ps.toJsObject
-    o.fields.map((tuple: (String, JsValue)) => {
-      tuple._2.convertTo[String] shouldBe "from-action"
-    })
+
+    val p = Parameters.serdes.read(originalValue.parseJson)
+    Parameters.serdes.write(p).compactPrint should not include "encryption"
+    p.params.foreach {
+      case (_, paramValue) =>
+        paramValue.encryption shouldBe empty
+    }
   }
 
   it should "read the merged message payload from kafka into parameters" in {
-    ParameterEncryption.storageConfig = new ParameterStorageConfig("aes-128", "ra1V6AfOYAv0jCzEdufIFA==")
-    val locked = ParameterEncryption.lock(parameters)
-
-    val unlockedParam = new ParameterValue(JsString("test-plain"), false)
-    val mixedParams =
-      locked.merge(Some((new Parameters(Map.empty) + (new ParameterName("plain") -> unlockedParam)).toJsObject))
-    val params = Parameters.readMergedList(mixedParams.get)
-    params.get("one").get shouldBe locked.get("one").get
-    params.get("two").get shouldBe locked.get("two").get
-    params.get("two").get should not be locked.get("one").get
-    params.get("plain").get shouldBe JsString("test-plain")
+    val locked = parameters.lock(aes128encoder)
+    val mixedParams = locked.merge(Some(Parameters("plain", "test-plain").toJsObject))
+    mixedParams shouldBe defined
+    mixedParams.get.fields("one") shouldBe locked.get("one").get
+    mixedParams.get.fields("two") shouldBe locked.get("two").get
+    mixedParams.get.fields("two") should not be locked.get("one").get
+    mixedParams.get.fields("plain") shouldBe JsString("test-plain")
   }
 
   behavior of "AesParameterEncryption"
+
   it should "correctly mark the encrypted parameters after lock" in {
-    ParameterEncryption.storageConfig = new ParameterStorageConfig("aes-128", "ra1V6AfOYAv0jCzEdufIFA==")
-    val locked = ParameterEncryption.lock(parameters)
-    locked.getMap.map(({
+    val locked = parameters.lock(aes128encoder)
+
+    locked.params.foreach {
       case (_, paramValue) =>
-        paramValue.encryption.convertTo[String] shouldBe "aes-128"
+        paramValue.encryption shouldBe Some("aes-128")
         paramValue.value.convertTo[String] should not be "secret"
-    }))
+    }
   }
 
   it should "serialize to json correctly" in {
-    val output =
-      """\Q{"one":{"encryption":"aes-128","init":false,"value":"\E.*\Q"},"two":{"encryption":"aes-128","init":true,"value":"\E.*\Q"}}""".stripMargin.r
-    ParameterEncryption.storageConfig = new ParameterStorageConfig("aes-128", "ra1V6AfOYAv0jCzEdufIFA==")
-    val locked = ParameterEncryption.lock(parameters)
-    val dbString = locked.toJsObject.toString
-    dbString should fullyMatch regex output
+    val locked = parameters.lock(aes128encoder)
+    locked.toJsObject.toString should fullyMatch regex """\Q{"one":"\E.*\Q","two":"\E.*\Q"}""".stripMargin.r
+    locked.lockedParameters() shouldBe Map("one" -> "aes-128", "two" -> "aes-128")
   }
 
-  it should "correctly decrypted encrypted values" in {
-    ParameterEncryption.storageConfig = new ParameterStorageConfig("aes-128", "ra1V6AfOYAv0jCzEdufIFA==")
-    val locked = ParameterEncryption.lock(parameters)
-    locked.getMap.map(({
+  it should "serialize to json correctly when a locked parameter is overriden" in {
+    val locked = parameters.lock(aes128encoder)
+    locked
+      .merge(Some(JsObject("one" -> JsString("override"))))
+      .get
+      .compactPrint should fullyMatch regex """\Q{"one":"override","two":"\E.*\Q"}""".stripMargin.r
+    locked.lockedParameters(Set("one")) shouldBe Map("two" -> "aes-128")
+  }
+
+  it should "correctly decrypt encrypted values" in {
+    val locked = parameters.lock(aes128encoder)
+
+    locked.params.foreach {
       case (_, paramValue) =>
-        paramValue.encryption.convertTo[String] shouldBe "aes-128"
+        paramValue.encryption shouldBe Some("aes-128")
         paramValue.value.convertTo[String] should not be "secret"
-    }))
+    }
 
-    val unlocked = ParameterEncryption.unlock(locked)
-    unlocked.getMap.map(({
+    val unlocked = locked.unlock(aes128decoder)
+    unlocked.params.foreach {
       case (_, paramValue) =>
-        paramValue.encryption shouldBe JsNull
+        paramValue.encryption shouldBe empty
         paramValue.value.convertTo[String] shouldBe "secret"
-    }))
+    }
   }
-  it should "correctly decrypted encrypted JsObject values" in {
-    ParameterEncryption.storageConfig = new ParameterStorageConfig("aes-128", "ra1V6AfOYAv0jCzEdufIFA==")
-    val obj = Map("key" -> "xyz".toJson, "value" -> "v1".toJson).toJson
 
+  it should "correctly decrypt encrypted JsObject values" in {
+    val obj = Map("key" -> "xyz".toJson, "value" -> "v1".toJson).toJson
     val complexParam = new Parameters(Map(new ParameterName("one") -> new ParameterValue(obj, false)))
 
-    val locked = ParameterEncryption.lock(complexParam)
-    locked.getMap.map(({
+    val locked = complexParam.lock(aes128encoder)
+    locked.params.foreach {
       case (_, paramValue) =>
-        paramValue.encryption.convertTo[String] shouldBe "aes-128"
+        paramValue.encryption shouldBe Some("aes-128")
         paramValue.value.convertTo[String] should not be "secret"
-    }))
+    }
 
-    val unlocked = ParameterEncryption.unlock(locked)
-    unlocked.getMap.map(({
+    val unlocked = locked.unlock(aes128decoder)
+    unlocked.params.foreach {
       case (_, paramValue) =>
-        paramValue.encryption shouldBe JsNull
+        paramValue.encryption shouldBe empty
         paramValue.value shouldBe obj
-    }))
+    }
   }
-  it should "correctly decrypted encrypted multiline values" in {
-    ParameterEncryption.storageConfig = new ParameterStorageConfig("aes-128", "ra1V6AfOYAv0jCzEdufIFA==")
+  it should "correctly decrypt encrypted multiline values" in {
     val lines = "line1\nline2\nline3\nline4"
     val multiline = new Parameters(Map(new ParameterName("one") -> new ParameterValue(JsString(lines), false)))
 
-    val locked = ParameterEncryption.lock(multiline)
-    locked.getMap.map(({
+    val locked = multiline.lock(aes128encoder)
+    locked.params.foreach {
       case (_, paramValue) =>
-        paramValue.encryption.convertTo[String] shouldBe "aes-128"
+        paramValue.encryption shouldBe Some("aes-128")
         paramValue.value.convertTo[String] should not be "secret"
-    }))
+    }
 
-    val unlocked = ParameterEncryption.unlock(locked)
-    unlocked.getMap.map(({
+    val unlocked = locked.unlock(aes128decoder)
+    unlocked.params.foreach {
       case (_, paramValue) =>
-        paramValue.encryption shouldBe JsNull
+        paramValue.encryption shouldBe empty
         paramValue.value.convertTo[String] shouldBe lines
-    }))
+    }
   }
+
   // Not sure having cancelled tests is a good idea either, need to work on aes256 packaging.
   it should "work if with aes256 if policy allows it" in {
-    ParameterEncryption.storageConfig =
-      new ParameterStorageConfig("aes-256", "", "j5rLzhtxwzPyUVUy8/p8XJmBoKeDoSzNJP1SITJEY9E=")
     try {
-      val locked = ParameterEncryption.lock(parameters)
-      locked.getMap.map(({
+      val locked = parameters.lock(aes256encoder)
+      locked.params.foreach {
         case (_, paramValue) =>
-          paramValue.encryption.convertTo[String] shouldBe "aes-256"
+          paramValue.encryption shouldBe Some("aes-256")
           paramValue.value.convertTo[String] should not be "secret"
-      }))
+      }
 
-      val unlocked = ParameterEncryption.unlock(locked)
-      unlocked.getMap.map(({
+      val unlocked = locked.unlock(noop)
+      unlocked.params.foreach {
         case (_, paramValue) =>
-          paramValue.encryption shouldBe JsNull
+          paramValue.encryption shouldBe empty
           paramValue.value.convertTo[String] shouldBe "secret"
-      }))
+      }
     } catch {
       case e: InvalidAlgorithmParameterException =>
         cancel(e.toString)
     }
   }
+
   it should "support reverting back to Noop encryption" in {
-    ParameterEncryption.storageConfig = new ParameterStorageConfig("aes-128", "ra1V6AfOYAv0jCzEdufIFA==", "")
     try {
-      val locked = ParameterEncryption.lock(parameters)
-      locked.getMap.map(({
+      val locked = parameters.lock(aes128encoder)
+      locked.params.foreach {
         case (_, paramValue) =>
-          paramValue.encryption.convertTo[String] shouldBe "aes-128"
+          paramValue.encryption shouldBe Some("aes-128")
           paramValue.value.convertTo[String] should not be "secret"
-      }))
+      }
 
-      val lockedJson = locked.toJsObject
+      val lockedJson = Parameters.serdes.write(locked).compactPrint
+      val toDecrypt = Parameters.serdes.read(lockedJson.parseJson)
 
-      ParameterEncryption.storageConfig = new ParameterStorageConfig("", "ra1V6AfOYAv0jCzEdufIFA==", "")
-
-      val toDecrypt = Parameters.serdes.read(lockedJson)
-
-      val unlocked = ParameterEncryption.unlock(toDecrypt)
-      unlocked.getMap.map(({
+      // defaults to no-op
+      val unlocked = toDecrypt.unlock(noop)
+      unlocked.params.foreach {
         case (_, paramValue) =>
-          paramValue.encryption shouldBe JsNull
+          paramValue.encryption shouldBe empty
           paramValue.value.convertTo[String] shouldBe "secret"
-      }))
-      unlocked.toJsObject should not be JsNull
+      }
+
+      unlocked.toJsObject shouldBe JsObject("one" -> "secret".toJson, "two" -> "secret".toJson)
     } catch {
       case e: InvalidAlgorithmParameterException =>
         cancel(e.toString)
     }
   }
 
-  behavior of "NoopEncryption"
+  behavior of "No-op Encryption"
+
   it should "not mark parameters as encrypted" in {
-    val locked = ParameterEncryption.lock(parameters)
-    locked.getMap.map(({
+    val locked = parameters.lock()
+    locked.params.foreach {
       case (_, paramValue) =>
         paramValue.value.convertTo[String] shouldBe "secret"
-    }))
+    }
   }
 }
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/entity/test/SchemaTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/entity/test/SchemaTests.scala
index 7b6cad4..ccab9df 100644
--- a/tests/src/test/scala/org/apache/openwhisk/core/entity/test/SchemaTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/core/entity/test/SchemaTests.scala
@@ -695,7 +695,7 @@
     val json = Seq[JsValue](
       JsArray(JsObject("key" -> "k".toJson, "value" -> "v".toJson)),
       JsArray(JsObject("key" -> "k".toJson, "value" -> "v".toJson, "init" -> JsFalse)),
-      JsArray(JsObject("key" -> "k".toJson, "value" -> "v".toJson, "init" -> JsTrue)))
+      JsArray(JsObject(Map("key" -> "k".toJson, "value" -> "v".toJson, "init" -> JsTrue))))
 
     val params = json.map { p =>
       Parameters.serdes.read(p)
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/InvokerSupervisionTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/InvokerSupervisionTests.scala
index aa07914..1cf7383 100644
--- a/tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/InvokerSupervisionTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/InvokerSupervisionTests.scala
@@ -195,7 +195,8 @@
       rootControllerIndex = ControllerInstanceId("0"),
       blocking = false,
       content = None,
-      initArgs = Set.empty)
+      initArgs = Set.empty,
+      lockedArgs = Map.empty)
     val msg = ActivationRequest(activationMessage, invokerInstance)
 
     supervisor ! msg
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/ShardingContainerPoolBalancerTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/ShardingContainerPoolBalancerTests.scala
index dab21aa..11fee6d 100644
--- a/tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/ShardingContainerPoolBalancerTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/core/loadBalancer/test/ShardingContainerPoolBalancerTests.scala
@@ -508,7 +508,8 @@
         ControllerInstanceId("0"),
         blocking = false,
         content = None,
-        initArgs = Set.empty)
+        initArgs = Set.empty,
+        lockedArgs = Map.empty)
 
       //send activation to loadbalancer
       aid -> balancer.publish(actionMetaData.toExecutableWhiskAction.get, msg)