Infineon: Add cyw20829 platform, shared slot feature, json memory map, psoc6 xip

Based in 1.8.0 release of MCUBoot library

This commit adds CYW20829 Infineon platform support with following capabilities:
1. Overwrite and swap upgrade mode support
2. Multi-image with up to 4 images
3. Hardware security counter is supported for CYW20829 platform

Add XIP support for PSOC6 platform - place BOOT slot in external memory and execute it in place using SMIF in XIP mode

and some new features for Infineon devices.

1. Shared upgrade slot feature - use one shared area for upgrade slots of multiple images
2. Memory map defined using JSON file - define memory regions for bootloader and user app in conventional way using JSON file
diff --git a/scripts/imgtool/encrypt_mxs40sv2.py b/scripts/imgtool/encrypt_mxs40sv2.py
new file mode 100644
index 0000000..f95a3a5
--- /dev/null
+++ b/scripts/imgtool/encrypt_mxs40sv2.py
@@ -0,0 +1,110 @@
+"""
+Copyright (c) 2021 Cypress Semiconductor Corporation
+
+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.
+"""
+import os
+import struct
+from cryptography.hazmat.primitives.ciphers import (
+    Cipher, algorithms, modes
+)
+
+NONCE_SIZE = 12
+
+class EncryptorMXS40Sv2:
+    def __init__(self, key, nonce, initial_counter=0):
+        # with open(key_path, 'rb') as f:
+        #     self.key = f.read()
+        self.key = key
+        self.nonce = nonce
+        self.counter = 0
+        self.initial_counter = initial_counter
+        cipher = Cipher(algorithms.AES(key), modes.ECB())
+        self.encryptor = cipher.encryptor()
+
+    def _load(self, image_path):
+        with open(image_path, 'rb') as f:
+            image = f.read()
+        return image
+
+    def _save(self, data, output_path):
+        with open(output_path, 'wb') as f:
+            f.write(data)
+
+    def update(self, data):
+        """
+        Encrypts a byte array using a customized AES-CTR mode
+        where a counter is incremented by 16 per block.
+        A nonce format is (128 bit):
+            bits 0...31 - counter + initial values
+            bits 32...127 - random nonce
+        """
+        chunk_size = 16
+        counter = self.counter
+        ciphertext = bytes()
+        for i in range(0, len(image), chunk_size):
+            indata = struct.pack('<I', initial_counter + counter) + nonce
+            counter += chunk_size
+            cipher_block = self.encryptor.update(indata)
+            chunk = image[i:i + chunk_size]
+            ciphertext += bytes(a ^ b for a, b in zip(chunk, cipher_block))
+        self.counter = counter
+        return ciphertext
+
+    def encrypt(self, image, initial_counter=0):
+        """
+        Encrypts a byte array using a customized AES-CTR mode
+        where a counter is incremented by 16 per block.
+        A nonce format is (128 bit):
+            bits 0...31 - counter + initial values
+            bits 32...127 - random nonce
+        """
+        chunk_size = 16
+        counter = 0
+        # self.initial_counter = initial_counter
+        # counter = 0 if initial_counter is None else int(initial_counter, 0)
+        # counter = initial_counter
+        ciphertext = bytes()
+        for i in range(0, len(image), chunk_size):
+            indata = struct.pack('<I', initial_counter + counter) + self.nonce[:12]
+            counter += chunk_size
+            cipher_block = self.encryptor.update(indata)
+            chunk = image[i:i + chunk_size]
+            ciphertext += bytes(a ^ b for a, b in zip(chunk, cipher_block))
+        self.encryptor.finalize()
+        # return ciphertext, nonce
+        return ciphertext
+
+    def encrypt_image(self, input_path, initial_counter=None, output_path=None, nonce_path=None):
+        """
+        Encrypts an image each time using new random nonce.
+        Saves the nonce and the encrypted image to specified locations.
+        If the output locations are not given the output files are saved
+        in the same location as the input image with predefined names.
+        """
+        image = self._load(input_path)
+
+        nonce = os.urandom(NONCE_SIZE)
+        init = 0 if initial_counter is None else int(initial_counter, 0)
+        ciphertext, nonce = self._encrypt(image, nonce, init)
+
+        if output_path is None:
+            output_path = '{0}_{2}{1}'.format(*os.path.splitext(input_path) + ('encrypted',))
+
+        if nonce_path is None:
+            nonce_path = '{0}_{2}{1}'.format(*os.path.splitext(input_path) + ('nonce',))
+
+        self._save(ciphertext, output_path)
+        self._save(nonce, nonce_path)
+
+        return output_path, nonce_path