Ok. Quick primer.
Encryption is a transformation that takes sensitive data ("plaintext") and makes it statistically indistinguishable from random data ("ciphertext") in a way that is reversible for those in possession of a small secret. This allows encryption to provide secrecy: if you encrypt data, it is not supposed to be possible for anyone with access to the ciphertext to determine what was inside it.
However, encryption does not validate data integrity. This means that attackers can change your ciphertext and this will not be detectable by the encryption scheme. After all, to be indistinguishable from random data it must be possible for any cipher combined with any key to potentially produce all possible ciphertext blocks: therefore, all possible ciphertext blocks must be validly decrypt able.
For AES-CBC (what you're using here), the malleability of the ciphertext is well understood. An attacker that flips a bit in the ciphertext will deterministically flip the corresponding bit in the plaintext in the next block, as well as lead to gibberish in the block whose bit was flipped.
This is an attack that can be used both to render data useless (by just wildly flipping bits) as well as to launch highly targeted attacks. For example, if the attacker is familiar with what some of your weights are likely to be, they can happily change those weights to anything else, at the cost of turning some other weights to gibberish.
As a first-order assumption, any data you cared enough about to want to encrypt should also be authenticated. This defends against the above attacks by ensuring that the data you're operating on is the same as the data you encrypted in the first place.
CryptoKit and SwiftCrypto only offer authenticated encryption modes (AES-GCM and ChaCha20-Poly1305). Unfortunately for you, CommonCrypto does not offer any, so you need to build one yourself.
The way to do this is to combine your encryption with a MAC (Message Authentication Code). A MAC is a function that takes two parameters, a shared secret key and the data to authenticate, and produces a public authentication "tag". The other end of the connection can then take a tag, the same shared secret, and the data to validate, and confirm that this data was used with this key to generate this tag.
The most common and easiest-to-use MAC is HMAC (Hash-based Message Authentication Code). This is simply a fancy way to apply a hash function to some data. It's extremely fast, extremely well-understood, and well-trusted.
To build this crypto system properly you need to bear in mind the Cryptographic Doom Principle: when combining MACing and encryption there is only one right way to do it. Specifically, on the encryption side you must do this:
- Generate your shared keys. Different ones for encryption and MAC please, though you can derive these keys from the same shared secret using a key derivation function.
- Encrypt the plaintext data P using the encryption key Ke to produce ciphertext C.
- Generate the MAC tag M by applying HMAC to the ciphertext C with MAC key KM.
- Serialize the result by concatenating C and M to form your result data R.
Then when you decrypt, you do this:
- Receive the result data R'.
- Trim off the tag from the end of R', leaving you with ciphertext C' and tag M'. You will know how big the tag should be (they're fixed width), so you should just unconditionally use that many bytes. Do not allow your data R' to somehow encode the length of the tag M', this is a violation of the doom principle.
- Verify the tag M' is valid for the ciphertext C' using the pre-arranged MAC key Km. If the tag fails to validate, go no further: the ciphertext cannot be trusted. Report a generic error.
- Only now may you decrypt the ciphertext C' using the pre-arranged encryption key Ke.
I appreciate that this is a pain in the neck: this is why high level libraries like CryptoKit exist. But I stress that you really must do this: there are to a first-order approximation no threat models in which it is sensible to encrypt data that you don't also authenticate.