[privacy] Add generated privacy keys to GroupDataProvider. (#21555)

* [privacy] Revise PrivacyEncrypt/Decrypt APIs.

* [privacy] Implement PrivacyEncrypt/Decrypt APIs.

* [privacy] Add privacy keys to KeyContext and OperationalKey store.

* [privacy] Add tests for SetKeySet properly adding privacy keys.

* [privacy] Consolidate credential derivation.

* Restyled by clang-format

Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/credentials/GroupDataProviderImpl.cpp b/src/credentials/GroupDataProviderImpl.cpp
index 48acaac..0626a8e 100644
--- a/src/credentials/GroupDataProviderImpl.cpp
+++ b/src/credentials/GroupDataProviderImpl.cpp
@@ -697,21 +697,11 @@
     }
 };
 
-struct OperationalKey
-{
-    // Validity start time in microseconds since 2000-01-01T00:00:00 UTC ("the Epoch")
-    uint64_t start_time;
-    // Session Id
-    uint16_t hash;
-    // Operational group key
-    uint8_t value[Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES];
-};
-
 struct KeySetData : PersistentData<kPersistentBufferMax>
 {
     static constexpr TLV::Tag TagPolicy() { return TLV::ContextTag(1); }
     static constexpr TLV::Tag TagNumKeys() { return TLV::ContextTag(2); }
-    static constexpr TLV::Tag TagOperationalKeys() { return TLV::ContextTag(3); }
+    static constexpr TLV::Tag TagGroupCredentials() { return TLV::ContextTag(3); }
     static constexpr TLV::Tag TagStartTime() { return TLV::ContextTag(4); }
     static constexpr TLV::Tag TagKeyHash() { return TLV::ContextTag(5); }
     static constexpr TLV::Tag TagKeyValue() { return TLV::ContextTag(6); }
@@ -725,7 +715,7 @@
     uint16_t keyset_id                       = 0;
     GroupDataProvider::SecurityPolicy policy = GroupDataProvider::SecurityPolicy::kCacheAndSync;
     uint8_t keys_count                       = 0;
-    OperationalKey operational_keys[KeySet::kEpochKeysMax];
+    Crypto::GroupOperationalCredentials operational_keys[KeySet::kEpochKeysMax];
 
     KeySetData() = default;
     KeySetData(chip::FabricIndex fabric, chip::KeysetId id) : fabric_index(fabric) { keyset_id = id; }
@@ -749,7 +739,7 @@
         next = kInvalidKeysetId;
     }
 
-    OperationalKey * GetCurrentKey()
+    Crypto::GroupOperationalCredentials * GetCurrentGroupCredentials()
     {
         // An epoch key update SHALL order the keys from oldest to newest,
         // the current epoch key having the second newest time if time
@@ -778,14 +768,14 @@
         // operational_keys
         {
             TLV::TLVType array, item;
-            ReturnErrorOnFailure(writer.StartContainer(TagOperationalKeys(), TLV::kTLVType_Array, array));
+            ReturnErrorOnFailure(writer.StartContainer(TagGroupCredentials(), TLV::kTLVType_Array, array));
             for (auto & key : operational_keys)
             {
                 ReturnErrorOnFailure(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, item));
                 ReturnErrorOnFailure(writer.Put(TagStartTime(), static_cast<uint64_t>(key.start_time)));
                 ReturnErrorOnFailure(writer.Put(TagKeyHash(), key.hash));
                 ReturnErrorOnFailure(
-                    writer.Put(TagKeyValue(), ByteSpan(key.value, Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES)));
+                    writer.Put(TagKeyValue(), ByteSpan(key.encryption_key, Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES)));
                 ReturnErrorOnFailure(writer.EndContainer(item));
             }
             ReturnErrorOnFailure(writer.EndContainer(array));
@@ -810,9 +800,10 @@
         // keys_count
         ReturnErrorOnFailure(reader.Next(TagNumKeys()));
         ReturnErrorOnFailure(reader.Get(keys_count));
+        // TODO(#21614): Enforce maximum number of 3 keys in a keyset
         {
             // operational_keys
-            ReturnErrorOnFailure(reader.Next(TagOperationalKeys()));
+            ReturnErrorOnFailure(reader.Next(TagGroupCredentials()));
             VerifyOrReturnError(TLV::kTLVType_Array == reader.GetType(), CHIP_ERROR_INTERNAL);
 
             TLV::TLVType array, item;
@@ -830,11 +821,14 @@
                 ReturnErrorOnFailure(reader.Next(TagKeyHash()));
                 ReturnErrorOnFailure(reader.Get(key.hash));
                 // key value
-                ByteSpan value;
+                ByteSpan encryption_key;
                 ReturnErrorOnFailure(reader.Next(TagKeyValue()));
-                ReturnErrorOnFailure(reader.Get(value));
-                VerifyOrReturnError(Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES == value.size(), CHIP_ERROR_INTERNAL);
-                memcpy(key.value, value.data(), value.size());
+                ReturnErrorOnFailure(reader.Get(encryption_key));
+                VerifyOrReturnError(Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES == encryption_key.size(), CHIP_ERROR_INTERNAL);
+                memcpy(key.encryption_key, encryption_key.data(), encryption_key.size());
+                // Re-derive privacy key from encryption key when loading from storage to save on storage size.
+                MutableByteSpan privacy_key(key.privacy_key);
+                ReturnErrorOnFailure(Crypto::DeriveGroupPrivacyKey(encryption_key, privacy_key));
                 ReturnErrorOnFailure(reader.ExitContainer(item));
             }
             ReturnErrorOnFailure(reader.ExitContainer(array));
@@ -899,7 +893,7 @@
     mEndpointIterators.ReleaseAll();
     mKeySetIterators.ReleaseAll();
     mGroupSessionsIterator.ReleaseAll();
-    mKeyContexPool.ReleaseAll();
+    mGroupKeyContexPool.ReleaseAll();
 }
 
 void GroupDataProviderImpl::SetStorageDelegate(PersistentStorageDelegate * storage)
@@ -1599,9 +1593,8 @@
     for (size_t i = 0; i < in_keyset.num_keys_used; ++i)
     {
         ByteSpan epoch_key(in_keyset.epoch_keys[i].key, Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES);
-        MutableByteSpan key_span(keyset.operational_keys[i].value);
-        ReturnErrorOnFailure(Crypto::DeriveGroupOperationalKey(epoch_key, compressed_fabric_id, key_span));
-        ReturnErrorOnFailure(Crypto::DeriveGroupSessionId(key_span, keyset.operational_keys[i].hash));
+        ReturnErrorOnFailure(
+            Crypto::DeriveGroupOperationalCredentials(epoch_key, compressed_fabric_id, keyset.operational_keys[i]));
     }
 
     if (found)
@@ -1638,6 +1631,7 @@
     out_keyset.epoch_keys[0].start_time = keyset.operational_keys[0].start_time;
     out_keyset.epoch_keys[1].start_time = keyset.operational_keys[1].start_time;
     out_keyset.epoch_keys[2].start_time = keyset.operational_keys[2].start_time;
+
     return CHIP_NO_ERROR;
 }
 
@@ -1791,11 +1785,12 @@
             // Group found, get the keyset
             KeySetData keyset;
             VerifyOrReturnError(keyset.Find(mStorage, fabric, mapping.keyset_id), nullptr);
-            OperationalKey * key = keyset.GetCurrentKey();
-            if (nullptr != key)
+            Crypto::GroupOperationalCredentials * creds = keyset.GetCurrentGroupCredentials();
+            if (nullptr != creds)
             {
-                return mKeyContexPool.CreateObject(*this, ByteSpan(key->value, Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES),
-                                                   key->hash);
+                return mGroupKeyContexPool.CreateObject(
+                    *this, ByteSpan(creds->encryption_key, Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES), creds->hash,
+                    ByteSpan(creds->privacy_key, Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES));
             }
         }
     }
@@ -1826,7 +1821,7 @@
         if (key_idx < keyset.keys_count)
         {
             out_keyset.epoch_keys[key_idx].start_time = keyset.operational_keys[key_idx].start_time;
-            memcpy(&out_keyset.epoch_keys[key_idx].key[0], keyset.operational_keys[key_idx].value, EpochKey::kLengthBytes);
+            memcpy(&out_keyset.epoch_keys[key_idx].key[0], keyset.operational_keys[key_idx].encryption_key, EpochKey::kLengthBytes);
         }
     }
 
@@ -1835,36 +1830,37 @@
 
 void GroupDataProviderImpl::GroupKeyContext::Release()
 {
-    memset(mKeyValue, 0, sizeof(mKeyValue));
-    mProvider.mKeyContexPool.ReleaseObject(this);
+    memset(mEncryptionKey, 0, sizeof(mEncryptionKey));
+    memset(mPrivacyKey, 0, sizeof(mPrivacyKey));
+    mProvider.mGroupKeyContexPool.ReleaseObject(this);
 }
 
-CHIP_ERROR GroupDataProviderImpl::GroupKeyContext::EncryptMessage(const ByteSpan & plaintext, const ByteSpan & aad,
+CHIP_ERROR GroupDataProviderImpl::GroupKeyContext::MessageEncrypt(const ByteSpan & plaintext, const ByteSpan & aad,
                                                                   const ByteSpan & nonce, MutableByteSpan & mic,
                                                                   MutableByteSpan & ciphertext) const
 {
     uint8_t * output = ciphertext.data();
-    return Crypto::AES_CCM_encrypt(plaintext.data(), plaintext.size(), aad.data(), aad.size(), mKeyValue,
+    return Crypto::AES_CCM_encrypt(plaintext.data(), plaintext.size(), aad.data(), aad.size(), mEncryptionKey,
                                    Crypto::kAES_CCM128_Key_Length, nonce.data(), nonce.size(), output, mic.data(), mic.size());
 }
 
-CHIP_ERROR GroupDataProviderImpl::GroupKeyContext::DecryptMessage(const ByteSpan & ciphertext, const ByteSpan & aad,
+CHIP_ERROR GroupDataProviderImpl::GroupKeyContext::MessageDecrypt(const ByteSpan & ciphertext, const ByteSpan & aad,
                                                                   const ByteSpan & nonce, const ByteSpan & mic,
                                                                   MutableByteSpan & plaintext) const
 {
     uint8_t * output = plaintext.data();
-    return Crypto::AES_CCM_decrypt(ciphertext.data(), ciphertext.size(), aad.data(), aad.size(), mic.data(), mic.size(), mKeyValue,
-                                   Crypto::kAES_CCM128_Key_Length, nonce.data(), nonce.size(), output);
+    return Crypto::AES_CCM_decrypt(ciphertext.data(), ciphertext.size(), aad.data(), aad.size(), mic.data(), mic.size(),
+                                   mEncryptionKey, Crypto::kAES_CCM128_Key_Length, nonce.data(), nonce.size(), output);
 }
 
-CHIP_ERROR GroupDataProviderImpl::GroupKeyContext::EncryptPrivacy(MutableByteSpan & header, uint16_t session_id,
-                                                                  const ByteSpan & payload, const ByteSpan & mic) const
+CHIP_ERROR GroupDataProviderImpl::GroupKeyContext::PrivacyEncrypt(const ByteSpan & input, const ByteSpan & nonce,
+                                                                  MutableByteSpan & output) const
 {
     return CHIP_ERROR_NOT_IMPLEMENTED;
 }
 
-CHIP_ERROR GroupDataProviderImpl::GroupKeyContext::DecryptPrivacy(MutableByteSpan & header, uint16_t session_id,
-                                                                  const ByteSpan & payload, const ByteSpan & mic) const
+CHIP_ERROR GroupDataProviderImpl::GroupKeyContext::PrivacyDecrypt(const ByteSpan & input, const ByteSpan & nonce,
+                                                                  MutableByteSpan & output) const
 {
     return CHIP_ERROR_NOT_IMPLEMENTED;
 }
@@ -1876,7 +1872,7 @@
 }
 
 GroupDataProviderImpl::GroupSessionIteratorImpl::GroupSessionIteratorImpl(GroupDataProviderImpl & provider, uint16_t session_id) :
-    mProvider(provider), mSessionId(session_id), mKeyContext(provider)
+    mProvider(provider), mSessionId(session_id), mGroupKeyContext(provider)
 {
     FabricList fabric_list;
     ReturnOnFailure(fabric_list.Load(provider.mStorage));
@@ -1968,14 +1964,15 @@
             continue;
         }
 
-        OperationalKey & key = keyset.operational_keys[mKeyIndex++];
-        if (key.hash == mSessionId)
+        Crypto::GroupOperationalCredentials & creds = keyset.operational_keys[mKeyIndex++];
+        if (creds.hash == mSessionId)
         {
-            mKeyContext.SetKey(ByteSpan(key.value, sizeof(key.value)), mSessionId);
+            mGroupKeyContext.SetKey(ByteSpan(creds.encryption_key, sizeof(creds.encryption_key)), mSessionId);
+            mGroupKeyContext.SetPrivacyKey(ByteSpan(creds.privacy_key, sizeof(creds.privacy_key)));
             output.fabric_index    = fabric.fabric_index;
             output.group_id        = mapping.group_id;
             output.security_policy = keyset.policy;
-            output.key             = &mKeyContext;
+            output.key             = &mGroupKeyContext;
             return true;
         }
     }
diff --git a/src/credentials/GroupDataProviderImpl.h b/src/credentials/GroupDataProviderImpl.h
index a67ce73..73c8bc2 100644
--- a/src/credentials/GroupDataProviderImpl.h
+++ b/src/credentials/GroupDataProviderImpl.h
@@ -152,34 +152,41 @@
     public:
         GroupKeyContext(GroupDataProviderImpl & provider) : mProvider(provider) {}
 
-        GroupKeyContext(GroupDataProviderImpl & provider, const ByteSpan & key, uint16_t hash) : mProvider(provider)
+        GroupKeyContext(GroupDataProviderImpl & provider, const ByteSpan & encryptionKey, uint16_t hash,
+                        const ByteSpan & privacyKey) :
+            mProvider(provider)
         {
-            SetKey(key, hash);
+            SetKey(encryptionKey, hash);
+            SetPrivacyKey(privacyKey);
         }
 
-        void SetKey(const ByteSpan & key, uint16_t hash)
+        void SetKey(const ByteSpan & encryptionKey, uint16_t hash)
         {
             mKeyHash = hash;
-            memcpy(mKeyValue, key.data(), std::min(key.size(), sizeof(mKeyValue)));
+            memcpy(mEncryptionKey, encryptionKey.data(), std::min(encryptionKey.size(), sizeof(mEncryptionKey)));
+        }
+
+        void SetPrivacyKey(const ByteSpan & privacyKey)
+        {
+            memcpy(mPrivacyKey, privacyKey.data(), std::min(privacyKey.size(), sizeof(mPrivacyKey)));
         }
 
         uint16_t GetKeyHash() override { return mKeyHash; }
 
-        CHIP_ERROR EncryptMessage(const ByteSpan & plaintext, const ByteSpan & aad, const ByteSpan & nonce, MutableByteSpan & mic,
+        CHIP_ERROR MessageEncrypt(const ByteSpan & plaintext, const ByteSpan & aad, const ByteSpan & nonce, MutableByteSpan & mic,
                                   MutableByteSpan & ciphertext) const override;
-        CHIP_ERROR DecryptMessage(const ByteSpan & ciphertext, const ByteSpan & aad, const ByteSpan & nonce, const ByteSpan & mic,
+        CHIP_ERROR MessageDecrypt(const ByteSpan & ciphertext, const ByteSpan & aad, const ByteSpan & nonce, const ByteSpan & mic,
                                   MutableByteSpan & plaintext) const override;
-        CHIP_ERROR EncryptPrivacy(MutableByteSpan & header, uint16_t session_id, const ByteSpan & payload,
-                                  const ByteSpan & mic) const override;
-        CHIP_ERROR DecryptPrivacy(MutableByteSpan & header, uint16_t session_id, const ByteSpan & payload,
-                                  const ByteSpan & mic) const override;
+        CHIP_ERROR PrivacyEncrypt(const ByteSpan & input, const ByteSpan & nonce, MutableByteSpan & output) const override;
+        CHIP_ERROR PrivacyDecrypt(const ByteSpan & input, const ByteSpan & nonce, MutableByteSpan & output) const override;
 
         void Release() override;
 
     protected:
         GroupDataProviderImpl & mProvider;
-        uint16_t mKeyHash                                                 = 0;
-        uint8_t mKeyValue[Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES] = { 0 };
+        uint16_t mKeyHash                                                      = 0;
+        uint8_t mEncryptionKey[Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES] = { 0 };
+        uint8_t mPrivacyKey[Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES]    = { 0 };
     };
 
     class KeySetIteratorImpl : public KeySetIterator
@@ -218,7 +225,7 @@
         uint16_t mKeyIndex       = 0;
         uint16_t mKeyCount       = 0;
         bool mFirstMap           = true;
-        GroupKeyContext mKeyContext;
+        GroupKeyContext mGroupKeyContext;
     };
     bool IsInitialized() { return (mStorage != nullptr); }
     CHIP_ERROR RemoveEndpoints(FabricIndex fabric_index, GroupId group_id);
@@ -229,7 +236,7 @@
     ObjectPool<EndpointIteratorImpl, kIteratorsMax> mEndpointIterators;
     ObjectPool<KeySetIteratorImpl, kIteratorsMax> mKeySetIterators;
     ObjectPool<GroupSessionIteratorImpl, kIteratorsMax> mGroupSessionsIterator;
-    ObjectPool<GroupKeyContext, kIteratorsMax> mKeyContexPool;
+    ObjectPool<GroupKeyContext, kIteratorsMax> mGroupKeyContexPool;
 };
 
 } // namespace Credentials
diff --git a/src/credentials/tests/TestGroupDataProvider.cpp b/src/credentials/tests/TestGroupDataProvider.cpp
index d2b59ee..c2aad08 100644
--- a/src/credentials/tests/TestGroupDataProvider.cpp
+++ b/src/credentials/tests/TestGroupDataProvider.cpp
@@ -1138,7 +1138,7 @@
     NL_TEST_ASSERT(
         apSuite,
         CHIP_NO_ERROR ==
-            key_context->EncryptMessage(plaintext, ByteSpan(aad, sizeof(aad)), ByteSpan(nonce, sizeof(nonce)), tag, ciphertext));
+            key_context->MessageEncrypt(plaintext, ByteSpan(aad, sizeof(aad)), ByteSpan(nonce, sizeof(nonce)), tag, ciphertext));
 
     // The ciphertext must be different to the original message
     NL_TEST_ASSERT(apSuite, memcmp(ciphertext.data(), kMessage, sizeof(kMessage)));
@@ -1169,7 +1169,7 @@
             // Decrypt the ciphertext
             NL_TEST_ASSERT(apSuite,
                            CHIP_NO_ERROR ==
-                               session.key->DecryptMessage(ciphertext, ByteSpan(aad, sizeof(aad)), ByteSpan(nonce, sizeof(nonce)),
+                               session.key->MessageDecrypt(ciphertext, ByteSpan(aad, sizeof(aad)), ByteSpan(nonce, sizeof(nonce)),
                                                            tag, plaintext));
 
             // The new plaintext must match the original message
diff --git a/src/crypto/CHIPCryptoPAL.cpp b/src/crypto/CHIPCryptoPAL.cpp
index eb0bb48..ec2e415 100644
--- a/src/crypto/CHIPCryptoPAL.cpp
+++ b/src/crypto/CHIPCryptoPAL.cpp
@@ -830,6 +830,19 @@
                               sizeof(kGroupPrivacyInfo), out_key.data(), Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES);
 }
 
+CHIP_ERROR DeriveGroupOperationalCredentials(const ByteSpan & epoch_key, const ByteSpan & compressed_fabric_id,
+                                             GroupOperationalCredentials & operational_credentials)
+{
+    MutableByteSpan encryption_key(operational_credentials.encryption_key);
+    MutableByteSpan privacy_key(operational_credentials.privacy_key);
+
+    ReturnErrorOnFailure(Crypto::DeriveGroupOperationalKey(epoch_key, compressed_fabric_id, encryption_key));
+    ReturnErrorOnFailure(Crypto::DeriveGroupSessionId(encryption_key, operational_credentials.hash));
+    ReturnErrorOnFailure(Crypto::DeriveGroupPrivacyKey(encryption_key, privacy_key));
+
+    return CHIP_NO_ERROR;
+}
+
 CHIP_ERROR ExtractVIDPIDFromAttributeString(DNAttrType attrType, const ByteSpan & attr,
                                             AttestationCertVidPid & vidpidFromMatterAttr, AttestationCertVidPid & vidpidFromCNAttr)
 {
diff --git a/src/crypto/CHIPCryptoPAL.h b/src/crypto/CHIPCryptoPAL.h
index 58d4845..cb18202 100644
--- a/src/crypto/CHIPCryptoPAL.h
+++ b/src/crypto/CHIPCryptoPAL.h
@@ -1485,6 +1485,21 @@
 CHIP_ERROR ExtractVIDPIDFromX509Cert(const ByteSpan & x509Cert, AttestationCertVidPid & vidpid);
 
 /**
+ * @brief The set of credentials needed to operate group message security with symmetric keys.
+ */
+typedef struct GroupOperationalCredentials
+{
+    /// Validity start time in microseconds since 2000-01-01T00:00:00 UTC ("the Epoch")
+    uint64_t start_time;
+    /// Session Id
+    uint16_t hash;
+    /// Operational group key
+    uint8_t encryption_key[Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES];
+    /// Privacy key
+    uint8_t privacy_key[Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES];
+} GroupOperationalCredentials;
+
+/**
  * @brief Opaque context used to protect a symmetric key. The key operations must
  *        be performed without exposing the protected key value.
  */
@@ -1511,7 +1526,7 @@
      * for ciphertext, and plaintext.
      * @return CHIP_ERROR
      */
-    virtual CHIP_ERROR EncryptMessage(const ByteSpan & plaintext, const ByteSpan & aad, const ByteSpan & nonce,
+    virtual CHIP_ERROR MessageEncrypt(const ByteSpan & plaintext, const ByteSpan & aad, const ByteSpan & nonce,
                                       MutableByteSpan & mic, MutableByteSpan & ciphertext) const = 0;
     /**
      * @brief Perform the message decryption as described in 4.7.3.(Security Processing of Incoming Messages)
@@ -1523,30 +1538,26 @@
      * for plaintext, and ciphertext.
      * @return CHIP_ERROR
      */
-    virtual CHIP_ERROR DecryptMessage(const ByteSpan & ciphertext, const ByteSpan & aad, const ByteSpan & nonce,
+    virtual CHIP_ERROR MessageDecrypt(const ByteSpan & ciphertext, const ByteSpan & aad, const ByteSpan & nonce,
                                       const ByteSpan & mic, MutableByteSpan & plaintext) const = 0;
 
     /**
      * @brief Perform privacy encoding as described in 4.8.2. (Privacy Processing of Outgoing Messages)
-     * @param[in,out] header    Message header to encrypt
-     * @param[in] session_id    Outgoing SessionID
-     * @param[in] payload       Encrypted payload
-     * @param[in] mic       Outgoing Message Integrity Check
+     * @param[in] input         Message header to privacy encrypt
+     * @param[in] nonce         Privacy Nonce = session_id | mic
+     * @param[out] output       Message header obfuscated
      * @return CHIP_ERROR
      */
-    virtual CHIP_ERROR EncryptPrivacy(MutableByteSpan & header, uint16_t session_id, const ByteSpan & payload,
-                                      const ByteSpan & mic) const = 0;
+    virtual CHIP_ERROR PrivacyEncrypt(const ByteSpan & input, const ByteSpan & nonce, MutableByteSpan & output) const = 0;
 
     /**
      * @brief Perform privacy decoding as described in 4.8.3. (Privacy Processing of Incoming Messages)
-     * @param[in,out] header    Message header to decrypt
-     * @param[in] session_id    Incoming SessionID
-     * @param[in] payload       Encrypted payload
-     * @param[in] mic           Outgoing Message Integrity Check
+     * @param[in] input         Message header to privacy decrypt
+     * @param[in] nonce         Privacy Nonce = session_id | mic
+     * @param[out] output       Message header deobfuscated
      * @return CHIP_ERROR
      */
-    virtual CHIP_ERROR DecryptPrivacy(MutableByteSpan & header, uint16_t session_id, const ByteSpan & payload,
-                                      const ByteSpan & mic) const = 0;
+    virtual CHIP_ERROR PrivacyDecrypt(const ByteSpan & input, const ByteSpan & nonce, MutableByteSpan & output) const = 0;
 
     /**
      * @brief Release resources such as dynamic memory used to allocate this instance of the SymmetricKeyContext
@@ -1577,10 +1588,22 @@
  *  @brief Derives the Privacy Group Key using the Key Derivation Function (KDF) from the given epoch key.
  * @param[in] epoch_key  The epoch key. Must be CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES bytes length.
  * @param[out] out_key  Symmetric key used as the privacy key during message processing for group communication.
- The buffer size must be at least CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES bytes length.
+ *                      The buffer size must be at least CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES bytes length.
  * @return Returns a CHIP_NO_ERROR on succcess, or CHIP_ERROR_INTERNAL if the provided key is invalid.
  **/
 CHIP_ERROR DeriveGroupPrivacyKey(const ByteSpan & epoch_key, MutableByteSpan & out_key);
 
+/**
+ *  @brief Derives the complete set of credentials needed for group security.
+ *
+ * This function will derive the Encryption Key, Group Key Hash (Session Id), and Privacy Key
+ * for the given Epoch Key and Compressed Fabric Id.
+ * @param[in] epoch_key  The epoch key. Must be CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES bytes length.
+ * @param[in] compressed_fabric_id The compressed fabric ID for the fabric (big endian byte string)
+ * @param[out] operational_credentials The set of Symmetric keys used during message processing for group communication.
+ * @return Returns a CHIP_NO_ERROR on succcess, or CHIP_ERROR_INTERNAL if the provided key is invalid.
+ **/
+CHIP_ERROR DeriveGroupOperationalCredentials(const ByteSpan & epoch_key, const ByteSpan & compressed_fabric_id,
+                                             GroupOperationalCredentials & operational_credentials);
 } // namespace Crypto
 } // namespace chip
diff --git a/src/crypto/tests/BUILD.gn b/src/crypto/tests/BUILD.gn
index 430540a..fac74ed 100644
--- a/src/crypto/tests/BUILD.gn
+++ b/src/crypto/tests/BUILD.gn
@@ -43,7 +43,10 @@
     "TestCryptoLayer.h",
   ]
 
-  test_sources = [ "TestPersistentStorageOpKeyStore.cpp" ]
+  test_sources = [
+    "TestGroupOperationalCredentials.cpp",
+    "TestPersistentStorageOpKeyStore.cpp",
+  ]
 
   if (chip_device_platform == "esp32" || chip_device_platform == "nrfconnect" ||
       chip_device_platform == "efr32") {
diff --git a/src/crypto/tests/TestGroupOperationalCredentials.cpp b/src/crypto/tests/TestGroupOperationalCredentials.cpp
new file mode 100644
index 0000000..b7493cc
--- /dev/null
+++ b/src/crypto/tests/TestGroupOperationalCredentials.cpp
@@ -0,0 +1,141 @@
+/*
+ *
+ *    Copyright (c) 2022 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed 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.
+ */
+
+#include <inttypes.h>
+
+#include <crypto/CHIPCryptoPAL.h>
+#include <lib/core/CHIPCore.h>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <nlunit-test.h>
+
+using namespace chip;
+using namespace chip::Crypto;
+
+namespace {
+
+constexpr auto KEY_LENGTH = Crypto::CHIP_CRYPTO_SYMMETRIC_KEY_LENGTH_BYTES;
+
+// kFabricId1/kCompressedFabricIdBuffer1 matches the Compressed Fabric Identifier
+// example of spec section `4.3.2.2. Compressed Fabric Identifier`. It is based on
+// the public key in `kExampleOperationalRootPublicKey`.
+static const uint8_t kCompressedFabricIdBuffer1[] = { 0x87, 0xe1, 0xb0, 0x04, 0xe2, 0x35, 0xa1, 0x30 };
+constexpr ByteSpan kCompressedFabricId1(kCompressedFabricIdBuffer1);
+
+static const uint8_t kEpochKeys0[][KEY_LENGTH] = {
+    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
+    { 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f },
+    { 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f }
+};
+
+static GroupOperationalCredentials kGroupKeys0[] = {
+    { .start_time     = 0x0000000000000000,
+      .hash           = 0x479e,
+      .encryption_key = { 0xc5, 0xf2, 0x69, 0x01, 0x87, 0x11, 0x51, 0x50, 0xc3, 0x56, 0xad, 0x93, 0xb3, 0x85, 0xbb, 0x0f },
+      .privacy_key    = { 0xf5, 0xce, 0x81, 0x88, 0x1d, 0x11, 0x18, 0xc5, 0xc8, 0x91, 0xc9, 0x06, 0x0b, 0xc7, 0x70, 0x09 } },
+    { .start_time     = 0x1111111111111111,
+      .hash           = 0xa512,
+      .encryption_key = { 0xae, 0xd9, 0x56, 0x95, 0xf3, 0x75, 0xd2, 0xce, 0x78, 0x55, 0x6a, 0x41, 0x73, 0x0c, 0x3f, 0x43 },
+      .privacy_key    = { 0xdc, 0x90, 0xdb, 0x2e, 0x61, 0x76, 0x5a, 0x42, 0x60, 0x7f, 0xaf, 0xaf, 0x16, 0x37, 0x40, 0x0a } },
+    { .start_time     = 0x2222222222222222,
+      .hash           = 0xd800,
+      .encryption_key = { 0x35, 0xca, 0x34, 0x6e, 0x5e, 0x24, 0xbb, 0xbe, 0x88, 0x9c, 0xf4, 0xd3, 0x5c, 0x5e, 0x82, 0x0a },
+      .privacy_key    = { 0xb1, 0xfb, 0x3e, 0x79, 0xa2, 0xff, 0x27, 0xf3, 0x70, 0x4d, 0x2b, 0xe9, 0x19, 0x2d, 0xb6, 0x07 } },
+};
+
+struct GroupKeySetTestEntry
+{
+    const uint8_t * epochKey;                ///< Raw epoch key
+    GroupOperationalCredentials * groupKeys; ///< Struct of encryption key, privacy key, and group key hash (sessionId)
+};
+
+struct GroupKeySetTestEntry theGroupKeySetTestVector[] = {
+    {
+        .epochKey  = kEpochKeys0[0],
+        .groupKeys = &kGroupKeys0[0],
+    },
+    {
+        .epochKey  = kEpochKeys0[1],
+        .groupKeys = &kGroupKeys0[1],
+    },
+    {
+        .epochKey  = kEpochKeys0[2],
+        .groupKeys = &kGroupKeys0[2],
+    },
+};
+
+const uint16_t theGroupKeySetTestVectorLength = sizeof(theGroupKeySetTestVector) / sizeof(theGroupKeySetTestVector[0]);
+
+void TestDeriveGroupOperationalCredentials(nlTestSuite * apSuite, void * apContext)
+{
+    GroupOperationalCredentials opCreds;
+
+    for (unsigned i = 0; i < theGroupKeySetTestVectorLength; i++)
+    {
+        const ByteSpan epochKey(theGroupKeySetTestVector[i].epochKey, KEY_LENGTH);
+        NL_TEST_ASSERT(apSuite,
+                       CHIP_NO_ERROR == Crypto::DeriveGroupOperationalCredentials(epochKey, kCompressedFabricId1, opCreds));
+
+        NL_TEST_ASSERT(apSuite, opCreds.hash == theGroupKeySetTestVector[i].groupKeys->hash);
+        NL_TEST_ASSERT(apSuite,
+                       0 == memcmp(opCreds.encryption_key, theGroupKeySetTestVector[i].groupKeys->encryption_key, KEY_LENGTH));
+        NL_TEST_ASSERT(apSuite, 0 == memcmp(opCreds.privacy_key, theGroupKeySetTestVector[i].groupKeys->privacy_key, KEY_LENGTH));
+    }
+}
+
+/**
+ *   Test Suite. It lists all the test functions.
+ */
+const nlTest sTests[] = { NL_TEST_DEF("TestDeriveGroupOperationalCredentials", TestDeriveGroupOperationalCredentials),
+                          NL_TEST_SENTINEL() };
+
+/**
+ *  Set up the test suite.
+ */
+int Test_Setup(void * inContext)
+{
+    CHIP_ERROR error = chip::Platform::MemoryInit();
+    VerifyOrReturnError(error == CHIP_NO_ERROR, FAILURE);
+    return SUCCESS;
+}
+
+/**
+ *  Tear down the test suite.
+ */
+int Test_Teardown(void * inContext)
+{
+    chip::Platform::MemoryShutdown();
+    return SUCCESS;
+}
+
+} // namespace
+
+/**
+ *  Main
+ */
+int TestGroupOperationalCredentials()
+{
+    nlTestSuite theSuite = { "TestGroupOperationalCredentials", &sTests[0], Test_Setup, Test_Teardown };
+
+    // Run test suite againt one context.
+    nlTestRunner(&theSuite, nullptr);
+    return nlTestRunnerStats(&theSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestGroupOperationalCredentials)
diff --git a/src/transport/CryptoContext.cpp b/src/transport/CryptoContext.cpp
index 1150f7a..98c448a 100644
--- a/src/transport/CryptoContext.cpp
+++ b/src/transport/CryptoContext.cpp
@@ -147,6 +147,16 @@
     return bbuf.Fit() ? CHIP_NO_ERROR : CHIP_ERROR_NO_MEMORY;
 }
 
+CHIP_ERROR CryptoContext::BuildPrivacyNonce(NonceView nonce, uint16_t sessionId, const MessageAuthenticationCode & mac)
+{
+    const uint8_t * micFragment = &mac.GetTag()[kPrivacyNonceMicFragmentOffset];
+    Encoding::BigEndian::BufferWriter bbuf(nonce.data(), nonce.size());
+
+    bbuf.Put16(sessionId);
+    bbuf.Put(micFragment, kPrivacyNonceMicFragmentLength);
+    return bbuf.Fit() ? CHIP_NO_ERROR : CHIP_ERROR_NO_MEMORY;
+}
+
 CHIP_ERROR CryptoContext::GetAdditionalAuthData(const PacketHeader & header, uint8_t * aad, uint16_t & len)
 {
     VerifyOrReturnError(len >= header.EncodeSizeBytes(), CHIP_ERROR_INVALID_ARGUMENT);
@@ -187,7 +197,7 @@
         MutableByteSpan ciphertext(output, input_length);
         MutableByteSpan mic(tag, taglen);
 
-        ReturnErrorOnFailure(mKeyContext->EncryptMessage(plaintext, ByteSpan(AAD, aadLen), nonce, mic, ciphertext));
+        ReturnErrorOnFailure(mKeyContext->MessageEncrypt(plaintext, ByteSpan(AAD, aadLen), nonce, mic, ciphertext));
     }
     else
     {
@@ -231,7 +241,7 @@
         MutableByteSpan plaintext(output, input_length);
         ByteSpan mic(tag, taglen);
 
-        CHIP_ERROR err = mKeyContext->DecryptMessage(ciphertext, ByteSpan(AAD, aadLen), nonce, mic, plaintext);
+        CHIP_ERROR err = mKeyContext->MessageDecrypt(ciphertext, ByteSpan(AAD, aadLen), nonce, mic, plaintext);
         ReturnErrorOnFailure(err);
     }
     else
@@ -253,4 +263,40 @@
     return CHIP_NO_ERROR;
 }
 
+CHIP_ERROR CryptoContext::PrivacyEncrypt(const uint8_t * input, size_t input_length, uint8_t * output, PacketHeader & header,
+                                         MessageAuthenticationCode & mac) const
+{
+    VerifyOrReturnError(input != nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrReturnError(input_length > 0, CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrReturnError(output != nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+
+    // Confirm group key is available. Privacy obfuscation is not supported on unicast session keys.
+    VerifyOrReturnError(mKeyContext != nullptr, CHIP_ERROR_INVALID_USE_OF_SESSION_KEY);
+
+    ByteSpan plaintext(input, input_length);
+    MutableByteSpan privacytext(output, input_length);
+    CryptoContext::NonceStorage privacyNonce;
+    CryptoContext::BuildPrivacyNonce(privacyNonce, header.GetSessionId(), mac);
+
+    return mKeyContext->PrivacyEncrypt(plaintext, privacyNonce, privacytext);
+}
+
+CHIP_ERROR CryptoContext::PrivacyDecrypt(const uint8_t * input, size_t input_length, uint8_t * output, const PacketHeader & header,
+                                         const MessageAuthenticationCode & mac) const
+{
+    VerifyOrReturnError(input != nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrReturnError(input_length > 0, CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrReturnError(output != nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+
+    // Confirm group key is available. Privacy obfuscation is not supported on session keys.
+    VerifyOrReturnError(mKeyContext != nullptr, CHIP_ERROR_INVALID_USE_OF_SESSION_KEY);
+
+    const ByteSpan privacytext(input, input_length);
+    MutableByteSpan plaintext(output, input_length);
+    CryptoContext::NonceStorage privacyNonce;
+    CryptoContext::BuildPrivacyNonce(privacyNonce, header.GetSessionId(), mac);
+
+    return mKeyContext->PrivacyDecrypt(privacytext, privacyNonce, plaintext);
+}
+
 } // namespace chip
diff --git a/src/transport/CryptoContext.h b/src/transport/CryptoContext.h
index 0aeae73..9ab7b13 100644
--- a/src/transport/CryptoContext.h
+++ b/src/transport/CryptoContext.h
@@ -35,10 +35,12 @@
 class DLL_EXPORT CryptoContext
 {
 public:
-    static constexpr size_t kAESCCMNonceLen = 13;
-    using NonceStorage                      = std::array<uint8_t, kAESCCMNonceLen>;
-    using NonceView                         = FixedSpan<uint8_t, kAESCCMNonceLen>;
-    using ConstNonceView                    = FixedSpan<const uint8_t, kAESCCMNonceLen>;
+    static constexpr size_t kPrivacyNonceMicFragmentOffset = 5;
+    static constexpr size_t kPrivacyNonceMicFragmentLength = 11;
+    static constexpr size_t kAESCCMNonceLen                = 13;
+    using NonceStorage                                     = std::array<uint8_t, kAESCCMNonceLen>;
+    using NonceView                                        = FixedSpan<uint8_t, kAESCCMNonceLen>;
+    using ConstNonceView                                   = FixedSpan<const uint8_t, kAESCCMNonceLen>;
 
     CryptoContext();
     ~CryptoContext();
@@ -94,6 +96,9 @@
     /** @brief Build a Nonce buffer using given parameters for encrypt or decrypt. */
     static CHIP_ERROR BuildNonce(NonceView nonce, uint8_t securityFlags, uint32_t messageCounter, NodeId nodeId);
 
+    /** @brief Build a Nonce buffer using given parameters for encrypt or decrypt. */
+    static CHIP_ERROR BuildPrivacyNonce(NonceView nonce, uint16_t sessionId, const MessageAuthenticationCode & mac);
+
     /**
      * @brief
      *   Encrypt the input data using keys established in the secure channel
@@ -125,6 +130,12 @@
     CHIP_ERROR Decrypt(const uint8_t * input, size_t input_length, uint8_t * output, ConstNonceView nonce,
                        const PacketHeader & header, const MessageAuthenticationCode & mac) const;
 
+    CHIP_ERROR PrivacyEncrypt(const uint8_t * input, size_t input_length, uint8_t * output, PacketHeader & header,
+                              MessageAuthenticationCode & mac) const;
+
+    CHIP_ERROR PrivacyDecrypt(const uint8_t * input, size_t input_length, uint8_t * output, const PacketHeader & header,
+                              const MessageAuthenticationCode & mac) const;
+
     ByteSpan GetAttestationChallenge() const { return ByteSpan(mKeys[kAttestationChallengeKey], Crypto::kAES_CCM128_Key_Length); }
 
     /**
diff --git a/src/transport/tests/BUILD.gn b/src/transport/tests/BUILD.gn
index 26087e6..ce7579b 100644
--- a/src/transport/tests/BUILD.gn
+++ b/src/transport/tests/BUILD.gn
@@ -33,6 +33,7 @@
   output_name = "libTransportLayerTests"
 
   test_sources = [
+    "TestCryptoContext.cpp",
     "TestGroupMessageCounter.cpp",
     "TestPeerConnections.cpp",
     "TestPeerMessageCounter.cpp",
diff --git a/src/transport/tests/TestCryptoContext.cpp b/src/transport/tests/TestCryptoContext.cpp
new file mode 100644
index 0000000..a88bace
--- /dev/null
+++ b/src/transport/tests/TestCryptoContext.cpp
@@ -0,0 +1,108 @@
+/*
+ *
+ *    Copyright (c) 2022 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed 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.
+ */
+
+#include <inttypes.h>
+#include <nlunit-test.h>
+
+#include <crypto/CHIPCryptoPAL.h>
+#include <lib/core/CHIPCore.h>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <transport/CryptoContext.h>
+
+using namespace chip;
+
+namespace {
+
+constexpr size_t MIC_LENGTH   = chip::Crypto::CHIP_CRYPTO_AEAD_MIC_LENGTH_BYTES;
+constexpr size_t NONCE_LENGTH = CryptoContext::kAESCCMNonceLen;
+
+struct PrivacyNonceTestEntry
+{
+    uint16_t sessionId;
+    uint8_t mic[MIC_LENGTH];
+    uint8_t privacyNonce[NONCE_LENGTH];
+};
+
+struct PrivacyNonceTestEntry thePrivacyNonceTestVector[] = {
+    {
+        .sessionId    = 0x002a,
+        .mic          = { 0xc5, 0xa0, 0x06, 0x3a, 0xd5, 0xd2, 0x51, 0x81, 0x91, 0x40, 0x0d, 0xd6, 0x8c, 0x5c, 0x16, 0x3b },
+        .privacyNonce = { 0x00, 0x2a, 0xd2, 0x51, 0x81, 0x91, 0x40, 0x0d, 0xd6, 0x8c, 0x5c, 0x16, 0x3b },
+    },
+};
+
+const uint16_t thePrivacyNonceTestVectorLength = sizeof(thePrivacyNonceTestVector) / sizeof(thePrivacyNonceTestVector[0]);
+
+void TestBuildPrivacyNonce(nlTestSuite * apSuite, void * apContext)
+{
+    for (unsigned i = 0; i < thePrivacyNonceTestVectorLength; i++)
+    {
+        MessageAuthenticationCode mic;
+        uint16_t sessionId = thePrivacyNonceTestVector[i].sessionId;
+        const ByteSpan expectedPrivacyNonce(thePrivacyNonceTestVector[i].privacyNonce, NONCE_LENGTH);
+        CryptoContext::NonceStorage privacyNonce;
+        CryptoContext::ConstNonceView privacyNonceView(privacyNonce);
+
+        mic.SetTag(nullptr, thePrivacyNonceTestVector[i].mic, MIC_LENGTH);
+
+        NL_TEST_ASSERT(apSuite, CHIP_NO_ERROR == chip::CryptoContext::BuildPrivacyNonce(privacyNonce, sessionId, mic));
+        NL_TEST_ASSERT(apSuite, 0 == memcmp(privacyNonceView.data(), expectedPrivacyNonce.data(), NONCE_LENGTH));
+    }
+}
+
+/**
+ *   Test Suite. It lists all the test functions.
+ */
+const nlTest sTests[] = { NL_TEST_DEF("TestBuildPrivacyNonce", TestBuildPrivacyNonce), NL_TEST_SENTINEL() };
+
+/**
+ *  Set up the test suite.
+ */
+int Test_Setup(void * inContext)
+{
+    CHIP_ERROR error = chip::Platform::MemoryInit();
+    VerifyOrReturnError(error == CHIP_NO_ERROR, FAILURE);
+    return SUCCESS;
+}
+
+/**
+ *  Tear down the test suite.
+ */
+int Test_Teardown(void * inContext)
+{
+    chip::Platform::MemoryShutdown();
+    return SUCCESS;
+}
+
+} // namespace
+
+/**
+ *  Main
+ */
+int TestGroupCryptoContext()
+{
+    nlTestSuite theSuite = { "TestGroupCryptoContext", &sTests[0], Test_Setup, Test_Teardown };
+
+    // Run test suite againt one context.
+    nlTestRunner(&theSuite, nullptr);
+    return nlTestRunnerStats(&theSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestGroupCryptoContext)