/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. The source code in this file is covered under a dual-license scenario: - RCL: for OPC Foundation Corporate Members in good-standing - GPL V2: everybody else RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ GNU General Public License as published by the Free Software Foundation; version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 This source code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ using System; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Opc.Ua.Security.Certificates; #if CURVE25519 using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.X509; using Org.BouncyCastle.Security; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Signers; using Org.BouncyCastle.Crypto.Agreement; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Digests; #endif #nullable enable namespace Opc.Ua { /// /// Defines functions to implement ECC cryptography. /// public static class CryptoUtils { /// /// The name of the NIST P-256 curve. /// public const string NistP256 = nameof(NistP256); /// /// The name of the NIST P-384 curve. /// public const string NistP384 = nameof(NistP384); /// /// The name of the BrainpoolP256r1 curve. /// public const string BrainpoolP256r1 = nameof(BrainpoolP256r1); /// /// The name of the BrainpoolP384r1 curve. /// public const string BrainpoolP384r1 = nameof(BrainpoolP384r1); internal const string NistP256KeyParameters = "06-08-2A-86-48-CE-3D-03-01-07"; internal const string NistP384KeyParameters = "06-05-2B-81-04-00-22"; internal const string BrainpoolP256r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-07"; internal const string BrainpoolP384r1KeyParameters = "06-09-2B-24-03-03-02-08-01-01-0B"; /// /// Returns true if the certificate is an ECC certificate. /// public static bool IsEccPolicy(string securityPolicyUri) { SecurityPolicyInfo? info = SecurityPolicies.GetInfo(securityPolicyUri); if (info != null) { return info.CertificateKeyFamily == CertificateKeyFamily.ECC; } return false; } /// /// Returns the NodeId for the certificate type for the specified certificate. /// public static NodeId GetEccCertificateTypeId(Certificate certificate) { string keyAlgorithm = certificate.GetKeyAlgorithm(); if (keyAlgorithm != Oids.ECPublicKey) { return NodeId.Null; } PublicKey encodedPublicKey = certificate.PublicKey; if (encodedPublicKey.EncodedParameters is null) { return NodeId.Null; } switch (BitConverter.ToString(encodedPublicKey.EncodedParameters.RawData)) { // nistP256 case NistP256KeyParameters: return ObjectTypeIds.EccNistP256ApplicationCertificateType; // nistP384 case NistP384KeyParameters: return ObjectTypeIds.EccNistP384ApplicationCertificateType; // brainpoolP256r1 case BrainpoolP256r1KeyParameters: return ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType; // brainpoolP384r1 case BrainpoolP384r1KeyParameters: return ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType; default: return NodeId.Null; } } /// /// returns an ECCCurve if there is a matching supported curve for the provided /// certificate type id. if no supported ECC curve is found null is returned. /// /// the application certificatate type node id /// the ECCCurve, null if certificatate type id has no matching supported ECC curve public static ECCurve? GetCurveFromCertificateTypeId(NodeId certificateType) { ECCurve? curve = null; if (certificateType == ObjectTypeIds.EccApplicationCertificateType || certificateType == ObjectTypeIds.EccNistP256ApplicationCertificateType) { curve = ECCurve.NamedCurves.nistP256; } else if (certificateType == ObjectTypeIds.EccNistP384ApplicationCertificateType) { curve = ECCurve.NamedCurves.nistP384; } else if (certificateType == ObjectTypeIds.EccBrainpoolP256r1ApplicationCertificateType) { curve = ECCurve.NamedCurves.brainpoolP256r1; } else if (certificateType == ObjectTypeIds.EccBrainpoolP384r1ApplicationCertificateType) { curve = ECCurve.NamedCurves.brainpoolP384r1; } #if CURVE25519 else if (certificateType == ObjectTypeIds.EccCurve25519ApplicationCertificateType) { curve = default(ECCurve); } else if (certificateType == ObjectTypeIds.EccCurve448ApplicationCertificateType) { curve = default(ECCurve); } #endif return curve; } /// /// Returns the signature algorithm for the specified certificate. /// public static string GetECDsaQualifier(Certificate certificate) { if (X509Utils.IsECDsaSignature(certificate)) { const string signatureQualifier = "ECDsa"; PublicKey encodedPublicKey = certificate.PublicKey; if (encodedPublicKey.EncodedParameters is null) { return string.Empty; } // New values can be determined by running the dotted-decimal OID value // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); switch (BitConverter.ToString(encodedPublicKey.EncodedParameters!.RawData!)) { case NistP256KeyParameters: return NistP256; case NistP384KeyParameters: return NistP384; case BrainpoolP256r1KeyParameters: return BrainpoolP256r1; case BrainpoolP384r1KeyParameters: return BrainpoolP384r1; default: return signatureQualifier; } } return string.Empty; } /// /// Returns the public key for the specified certificate. /// public static ECDsa? GetPublicKey(Certificate certificate) { return GetPublicKey(certificate, out string[]? _); } /// /// Returns the public key for the specified certificate and outputs the security policy uris. /// /// /// public static ECDsa? GetPublicKey( Certificate certificate, out string[]? securityPolicyUris) { securityPolicyUris = null; if (certificate == null) { return null; } string keyAlgorithm = certificate.GetKeyAlgorithm(); if (keyAlgorithm != Oids.ECPublicKey) { return null; } const X509KeyUsageFlags kSufficientFlags = X509KeyUsageFlags.KeyAgreement | X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.NonRepudiation | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign; foreach (X509Extension extension in certificate.Extensions) { if (extension.Oid?.Value == "2.5.29.15") { var kuExt = (X509KeyUsageExtension)extension; if ((kuExt.KeyUsages & kSufficientFlags) == 0) { return null; } } } PublicKey encodedPublicKey = certificate.PublicKey; if (encodedPublicKey.EncodedParameters is null) { return null; } string keyParameters = BitConverter.ToString( encodedPublicKey.EncodedParameters!.RawData!); byte[] keyValue = encodedPublicKey.EncodedKeyValue.RawData; var ecParameters = default(ECParameters); if (keyValue[0] != 0x04) { throw new InvalidOperationException("Only uncompressed points are supported"); } byte[] x = new byte[(keyValue.Length - 1) / 2]; byte[] y = new byte[x.Length]; Buffer.BlockCopy(keyValue, 1, x, 0, x.Length); Buffer.BlockCopy(keyValue, 1 + x.Length, y, 0, y.Length); ecParameters.Q.X = x; ecParameters.Q.Y = y; // New values can be determined by running the dotted-decimal OID value // through BitConverter.ToString(CryptoConfig.EncodeOID(dottedDecimal)); switch (keyParameters) { case NistP256KeyParameters: ecParameters.Curve = ECCurve.NamedCurves.nistP256; securityPolicyUris = [SecurityPolicies.ECC_nistP256]; break; case NistP384KeyParameters: ecParameters.Curve = ECCurve.NamedCurves.nistP384; securityPolicyUris = [SecurityPolicies.ECC_nistP384, SecurityPolicies .ECC_nistP256]; break; case BrainpoolP256r1KeyParameters: ecParameters.Curve = ECCurve.NamedCurves.brainpoolP256r1; securityPolicyUris = [SecurityPolicies.ECC_brainpoolP256r1]; break; case BrainpoolP384r1KeyParameters: ecParameters.Curve = ECCurve.NamedCurves.brainpoolP384r1; securityPolicyUris = [SecurityPolicies.ECC_brainpoolP384r1, SecurityPolicies .ECC_brainpoolP256r1]; break; default: throw new NotImplementedException(keyParameters); } return ECDsa.Create(ecParameters); } /// /// Returns the length of a ECDsa signature of a digest. /// /// public static int GetSignatureLength(Certificate signingCertificate) { if (signingCertificate == null) { throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "No public key for certificate."); } if (signingCertificate.GetRSAPublicKey() != null) { return RsaUtils.GetSignatureLength(signingCertificate); } using ECDsa publicKey = GetPublicKey(signingCertificate) ?? throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "No public key for certificate."); return publicKey.KeySize / 4; } /// /// Computes a signature. /// /// /// cannot be resolved to a /// known security policy. /// public static byte[]? Sign( ArraySegment dataToSign, Certificate signingCertificate, string securityPolicyUri) { SecurityPolicyInfo info = SecurityPolicies.GetInfo(securityPolicyUri) ?? throw new ArgumentException( $"Cannot resolve SecurityPolicy '{securityPolicyUri}'.", nameof(securityPolicyUri)); return Sign(dataToSign, signingCertificate, info.AsymmetricSignatureAlgorithm); } /// /// Computes a signature. /// /// /// public static byte[]? Sign( ArraySegment dataToSign, Certificate signingCertificate, AsymmetricSignatureAlgorithm algorithm) { switch (algorithm) { case AsymmetricSignatureAlgorithm.None: return null; case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: return RsaUtils.Rsa_Sign( dataToSign, signingCertificate, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: return RsaUtils.Rsa_Sign( dataToSign, signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); case AsymmetricSignatureAlgorithm.RsaPssSha256: return RsaUtils.Rsa_Sign( dataToSign, signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); case AsymmetricSignatureAlgorithm.EcdsaSha256: case AsymmetricSignatureAlgorithm.EcdsaSha384: break; default: throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected); } // get the algorithm used for the signature. HashAlgorithmName hashAlgorithm; switch (algorithm) { case AsymmetricSignatureAlgorithm.EcdsaSha384: hashAlgorithm = HashAlgorithmName.SHA384; break; case AsymmetricSignatureAlgorithm.EcdsaSha256: hashAlgorithm = HashAlgorithmName.SHA256; break; default: throw new NotSupportedException($"AsymmetricSignatureAlgorithm not supported: {algorithm}"); } ECDsa senderPrivateKey = signingCertificate.GetECDsaPrivateKey() ?? throw new ServiceResultException( StatusCodes.BadCertificateInvalid, "Missing private key needed for create a signature."); byte[] arrayToSign = dataToSign.Array ?? throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Data to sign must not be empty."); using (senderPrivateKey) { return senderPrivateKey.SignData( arrayToSign, dataToSign.Offset, dataToSign.Count, hashAlgorithm); } } /// /// Verifies a signature. /// /// public static bool Verify( ArraySegment dataToVerify, byte[] signature, Certificate signingCertificate, string securityPolicyUri) { SecurityPolicyInfo info = SecurityPolicies.GetInfo(securityPolicyUri) ?? throw new ServiceResultException( StatusCodes.BadSecurityChecksFailed, $"Unknown security policy: {securityPolicyUri}"); return Verify( dataToVerify, signature, signingCertificate, info.AsymmetricSignatureAlgorithm); } /// /// Verifies a signature. /// /// /// public static bool Verify( ArraySegment dataToVerify, byte[] signature, Certificate signingCertificate, AsymmetricSignatureAlgorithm algorithm) { switch (algorithm) { case AsymmetricSignatureAlgorithm.None: return true; case AsymmetricSignatureAlgorithm.RsaPkcs15Sha1: return RsaUtils.Rsa_Verify( dataToVerify, signature, signingCertificate, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1); case AsymmetricSignatureAlgorithm.RsaPkcs15Sha256: return RsaUtils.Rsa_Verify( dataToVerify, signature, signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); case AsymmetricSignatureAlgorithm.RsaPssSha256: return RsaUtils.Rsa_Verify( dataToVerify, signature, signingCertificate, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); case AsymmetricSignatureAlgorithm.EcdsaSha256: case AsymmetricSignatureAlgorithm.EcdsaSha384: break; default: return false; } // get the algorithm used for the signature. HashAlgorithmName hashAlgorithm; switch (algorithm) { case AsymmetricSignatureAlgorithm.EcdsaSha384: hashAlgorithm = HashAlgorithmName.SHA384; break; case AsymmetricSignatureAlgorithm.EcdsaSha256: hashAlgorithm = HashAlgorithmName.SHA256; break; default: throw new NotSupportedException($"AsymmetricSignatureAlgorithm not supported: {algorithm}."); } using ECDsa ecdsa = GetPublicKey(signingCertificate) ?? throw new ServiceResultException(StatusCodes.BadCertificateInvalid, "Missing ECC public key for signature verification."); byte[] arrayToVerify = dataToVerify.Array ?? throw new ServiceResultException(StatusCodes.BadInvalidArgument, "Data to verify must not be empty."); return ecdsa.VerifyData( arrayToVerify, dataToVerify.Offset, dataToVerify.Count, signature, hashAlgorithm); } /// /// Adds padding to a buffer. Input: buffer with unencrypted data starting at 0; /// plaintext data starting at offset; no padding. /// /// buffer with unencrypted data starting at 0; plaintext data /// starting at offset; no padding. /// /// Additional bytes that will be appended after /// padding (e.g., HMAC) and must be considered for block alignment. /// Output: buffer with unencrypted data starting at 0; plaintext data /// starting at offset; padding added. /// private static ArraySegment AddPadding(ArraySegment data, int blockSize, int trailingBytes = 0) { byte[] dataArray = data.Array ?? throw new ArgumentNullException(nameof(data), "Data array must not be null."); int paddingByteSize = blockSize > byte.MaxValue ? 2 : 1; int paddingSize = blockSize - ((data.Count + paddingByteSize + trailingBytes) % blockSize); paddingSize %= blockSize; int endOfData = data.Offset + data.Count; int endOfPaddedData = data.Offset + data.Count + paddingSize + paddingByteSize; for (int ii = endOfData; ii < endOfPaddedData - paddingByteSize && ii < dataArray.Length; ii++) { dataArray[ii] = (byte)(paddingSize & 0xFF); } dataArray[endOfData + paddingSize] = (byte)(paddingSize & 0xFF); if (blockSize > byte.MaxValue) { dataArray[endOfData + paddingSize + 1] = (byte)((paddingSize & 0xFF) >> 8); } return new ArraySegment(dataArray, data.Offset, data.Count + paddingSize + paddingByteSize); } /// /// Removes padding from a buffer. Input: buffer with unencrypted data starting at 0; /// plaintext including padding starting at offset; signature removed. /// /// Input: buffer with unencrypted data starting at 0; plaintext /// including padding starting at offset; signature removed. /// /// Output: buffer with unencrypted data starting at 0; plaintext starting /// at offset; padding excluded. /// /// private static ArraySegment RemovePadding(ArraySegment data, int blockSize) { byte[] dataArray = data.Array ?? throw new ArgumentNullException(nameof(data), "Data array must not be null."); int paddingSize = dataArray[data.Offset + data.Count - 1]; int paddingByteSize = 1; if (blockSize > byte.MaxValue) { paddingSize <<= 8; paddingSize += dataArray[data.Offset + data.Count - 2]; paddingByteSize = 2; } int notvalid = paddingSize < data.Count ? 0 : 1; int start = data.Offset + data.Count - paddingSize - paddingByteSize; for (int ii = data.Offset; ii < data.Count - paddingByteSize && ii < paddingSize; ii++) { if (start < 0 || start + ii >= data.Count) { notvalid |= 1; continue; } notvalid |= dataArray[start + ii] ^ (paddingSize & 0xFF); } if (notvalid != 0) { throw new CryptographicException("Invalid padding."); } return new ArraySegment(dataArray, 0, data.Offset + data.Count - paddingSize - paddingByteSize); } /// /// Encrypts the buffer using the algorithm specified by the security policy. /// /// /// /// public static ArraySegment SymmetricEncryptAndSign( ArraySegment data, SecurityPolicyInfo securityPolicy, byte[] encryptingKey, byte[] iv, byte[]? signingKey = null, HMAC? hmac = null, bool signOnly = false, uint tokenId = 0, uint lastSequenceNumber = 0) { SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm; if (algorithm == SymmetricEncryptionAlgorithm.None) { return data; } if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm) { #if NET8_0_OR_GREATER return EncryptWithAesGcm(data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); #else throw new NotSupportedException("AES-GCM requires .NET 8 or greater."); #endif } if (algorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305) { #if NET8_0_OR_GREATER return EncryptWithChaCha20Poly1305( data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); #else throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater."); #endif } int hashLength = 0; if (signingKey != null) { if (hmac == null) { throw new CryptographicException("Missing HMAC for symmetric signing."); } hashLength = hmac.HashSize / 8; } if (!signOnly) { data = AddPadding(data, iv.Length, hashLength); } // The buffer originates from BufferManager so the backing array is non-null. byte[] dataArray = data.GetArray(); if (signingKey != null) { byte[] hash = hmac!.ComputeHash(dataArray, 0, data.Offset + data.Count); Buffer.BlockCopy( hash, 0, dataArray, data.Offset + data.Count, hash.Length); data = new ArraySegment( dataArray, data.Offset, data.Count + hash.Length); } if (!signOnly) { #pragma warning disable CA5401 // Symmetric encryption uses non-default initialization vector using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.None; aes.Key = encryptingKey; aes.IV = iv; using ICryptoTransform encryptor = aes.CreateEncryptor(); #pragma warning restore CA5401 encryptor.TransformBlock( dataArray, data.Offset, data.Count, dataArray, data.Offset); } return new ArraySegment(dataArray, 0, data.Offset + data.Count); } #if NET8_0_OR_GREATER private static byte[] ApplyAeadMask(uint tokenId, uint lastSequenceNumber, byte[] iv) { byte[] copy = new byte[iv.Length]; Buffer.BlockCopy(iv, 0, copy, 0, iv.Length); copy[0] ^= (byte)(tokenId & 0x000000FF); copy[1] ^= (byte)((tokenId & 0x0000FF00) >> 8); copy[2] ^= (byte)((tokenId & 0x00FF0000) >> 16); copy[3] ^= (byte)((tokenId & 0xFF000000) >> 24); copy[4] ^= (byte)(lastSequenceNumber & 0x000000FF); copy[5] ^= (byte)((lastSequenceNumber & 0x0000FF00) >> 8); copy[6] ^= (byte)((lastSequenceNumber & 0x00FF0000) >> 16); copy[7] ^= (byte)((lastSequenceNumber & 0xFF000000) >> 24); return copy; } private const int kChaChaPolyIvLength = 12; private const int kChaChaPolyTagLength = 16; private static ArraySegment EncryptWithChaCha20Poly1305( ArraySegment data, byte[] encryptingKey, byte[] iv, bool signOnly, uint tokenId, uint lastSequenceNumber) { if (encryptingKey == null || encryptingKey.Length != 32) { throw new ArgumentException("ChaCha20-Poly1305 requires a 256-bit (32-byte) key.", nameof(encryptingKey)); } if (iv == null || iv.Length != kChaChaPolyIvLength) { throw new ArgumentException("ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv)); } byte[] ciphertext = new byte[signOnly ? 0 : data.Count]; byte[] tag = new byte[kChaChaPolyTagLength]; // ChaCha20-Poly1305/AES-GCM uses 128-bit authentication tag // Buffer originates from BufferManager so the backing array is non-null. byte[] dataArray = data.GetArray(); var extraData = new ReadOnlySpan( dataArray, 0, signOnly ? data.Offset + data.Count : data.Offset); using var chacha = new ChaCha20Poly1305(encryptingKey); iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); chacha.Encrypt( iv, signOnly ? Array.Empty() : data, ciphertext, tag, extraData); // Return layout: [associated data | ciphertext | tag] if (!signOnly) { Buffer.BlockCopy(ciphertext, 0, dataArray, data.Offset, ciphertext.Length); } Buffer.BlockCopy(tag, 0, dataArray, data.Offset + data.Count, tag.Length); return new ArraySegment( dataArray, 0, data.Offset + data.Count + kChaChaPolyTagLength); } private static ArraySegment DecryptWithChaCha20Poly1305( ArraySegment data, byte[] encryptingKey, byte[] iv, bool signOnly, uint tokenId, uint lastSequenceNumber) { if (encryptingKey == null || encryptingKey.Length != 32) { throw new ArgumentException( "ChaCha20-Poly1305 requires a 256-bit (32-byte) key.", nameof(encryptingKey)); } if (iv == null || iv.Length != kChaChaPolyIvLength) { throw new ArgumentException( "ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce.", nameof(iv)); } if (data.Count < kChaChaPolyTagLength) // Must at least contain tag { throw new ArgumentException( "Ciphertext too short.", nameof(data)); } byte[] plaintext = new byte[data.Count - kChaChaPolyTagLength]; // Buffer originates from BufferManager so the backing array is non-null. byte[] dataArray = data.GetArray(); var encryptedData = new ArraySegment( dataArray, data.Offset, signOnly ? 0 : data.Count - kChaChaPolyTagLength); var tag = new ArraySegment( dataArray, data.Offset + data.Count - kChaChaPolyTagLength, kChaChaPolyTagLength); var extraData = new ReadOnlySpan( dataArray, 0, signOnly ? data.Offset + data.Count - kChaChaPolyTagLength : data.Offset); using var chacha = new ChaCha20Poly1305(encryptingKey); iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); chacha.Decrypt( iv, encryptedData, tag, signOnly ? [] : plaintext, extraData); // Return layout: [associated data | plaintext] if (!signOnly) { Buffer.BlockCopy(plaintext, 0, dataArray, data.Offset, encryptedData.Count); } return new ArraySegment(dataArray, 0, data.Offset + data.Count - kChaChaPolyTagLength); } private const int kAesGcmIvLength = 12; private const int kAesGcmTagLength = 16; private static ArraySegment EncryptWithAesGcm( ArraySegment data, byte[] encryptingKey, byte[] iv, bool signOnly, uint tokenId, uint lastSequenceNumber) { if (encryptingKey == null) { throw new ArgumentNullException(nameof(encryptingKey)); } if (iv == null || iv.Length != kAesGcmIvLength) { throw new ArgumentException("AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv)); } byte[] ciphertext = new byte[signOnly ? 0 : data.Count]; byte[] tag = new byte[kAesGcmTagLength]; // AES-GCM uses 128-bit authentication tag // Buffer originates from BufferManager so the backing array is non-null. byte[] dataArray = data.GetArray(); var extraData = new ReadOnlySpan( dataArray, 0, signOnly ? data.Offset + data.Count : data.Offset); using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength); iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); aesGcm.Encrypt( iv, signOnly ? Array.Empty() : data, ciphertext, tag, extraData); // Return layout: [associated data | ciphertext | tag] if (!signOnly) { Buffer.BlockCopy(ciphertext, 0, dataArray, data.Offset, ciphertext.Length); } Buffer.BlockCopy(tag, 0, dataArray, data.Offset + data.Count, tag.Length); return new ArraySegment( dataArray, 0, data.Offset + data.Count + kAesGcmTagLength); } private static ArraySegment DecryptWithAesGcm( ArraySegment data, byte[] encryptingKey, byte[] iv, bool signOnly, uint tokenId, uint lastSequenceNumber) { if (encryptingKey == null) { throw new ArgumentNullException(nameof(encryptingKey)); } if (iv == null || iv.Length != kAesGcmIvLength) { throw new ArgumentException( "AES-GCM requires a 96-bit (12-byte) IV/nonce.", nameof(iv)); } if (data.Count < kAesGcmTagLength) // Must at least contain tag { throw new ArgumentException( "Ciphertext too short.", nameof(data)); } byte[] plaintext = new byte[data.Count - kAesGcmTagLength]; // Buffer originates from BufferManager so the backing array is non-null. byte[] dataArray = data.GetArray(); var encryptedData = new ArraySegment( dataArray, data.Offset, signOnly ? 0 : data.Count - kAesGcmTagLength); var tag = new ArraySegment( dataArray, data.Offset + data.Count - kAesGcmTagLength, kAesGcmTagLength); var extraData = new ReadOnlySpan( dataArray, 0, signOnly ? data.Offset + data.Count - kAesGcmTagLength : data.Offset); using var aesGcm = new AesGcm(encryptingKey, kAesGcmTagLength); iv = ApplyAeadMask(tokenId, lastSequenceNumber, iv); aesGcm.Decrypt( iv, encryptedData, tag, signOnly ? [] : plaintext, extraData); // Return layout: [associated data | plaintext] if (!signOnly) { Buffer.BlockCopy(plaintext, 0, dataArray, data.Offset, encryptedData.Count); } return new ArraySegment(dataArray, 0, data.Offset + data.Count - kAesGcmTagLength); } #endif /// /// Decrypts the buffer using the algorithm specified by the security policy. /// /// /// /// /// /// The signature HMAC could not be created. /// public static ArraySegment SymmetricDecryptAndVerify( ArraySegment data, SecurityPolicyInfo securityPolicy, byte[] encryptingKey, byte[] iv, byte[]? signingKey = null, bool signOnly = false, uint tokenId = 0, uint lastSequenceNumber = 0) { SymmetricEncryptionAlgorithm algorithm = securityPolicy.SymmetricEncryptionAlgorithm; if (algorithm == SymmetricEncryptionAlgorithm.None) { return data; } if (algorithm is SymmetricEncryptionAlgorithm.Aes128Gcm or SymmetricEncryptionAlgorithm.Aes256Gcm) { #if NET8_0_OR_GREATER return DecryptWithAesGcm(data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); #else throw new NotSupportedException("AES-GCM requires .NET 8 or greater."); #endif } if (algorithm == SymmetricEncryptionAlgorithm.ChaCha20Poly1305) { #if NET8_0_OR_GREATER return DecryptWithChaCha20Poly1305( data, encryptingKey, iv, signOnly, tokenId, lastSequenceNumber); #else throw new NotSupportedException("ChaCha20Poly1305 requires .NET 8 or greater."); #endif } // The buffer originates from BufferManager so the backing array is non-null. byte[] dataArray = data.GetArray(); if (!signOnly) { using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.None; aes.Key = encryptingKey; aes.IV = iv; using ICryptoTransform decryptor = aes.CreateDecryptor(); decryptor.TransformBlock( dataArray, data.Offset, data.Count, dataArray, data.Offset); } int isNotValid = 0; if (signingKey != null) { using HMAC hmac = securityPolicy.CreateSignatureHmac(signingKey) ?? throw new ServiceResultException( StatusCodes.BadSecurityChecksFailed, "Could not create signature HMAC."); byte[] hash = hmac.ComputeHash(dataArray, 0, data.Offset + data.Count - (hmac.HashSize / 8)); for (int ii = 0; ii < hash.Length; ii++) { int index = data.Offset + data.Count - hash.Length + ii; isNotValid |= dataArray[index] != hash[ii] ? 1 : 0; } data = new ArraySegment( dataArray, data.Offset, data.Count - hash.Length); } if (!signOnly) { data = RemovePadding(data, iv.Length); } if (isNotValid != 0) { throw new CryptographicException("Invalid signature."); } return new ArraySegment(dataArray, 0, data.Offset + data.Count); } } }