Files
Marc Schier eb40a59566 More format and diagnostic fixes (#3780)
Drive the solution to zero warnings / zero errors with analyzers on,
promote the consistently-fixed rules to error, and lock in coverage
on the only line of new logic.
2026-05-26 06:06:09 +02:00

1079 lines
40 KiB
C#

/* 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
{
/// <summary>
/// Defines functions to implement ECC cryptography.
/// </summary>
public static class CryptoUtils
{
/// <summary>
/// The name of the NIST P-256 curve.
/// </summary>
public const string NistP256 = nameof(NistP256);
/// <summary>
/// The name of the NIST P-384 curve.
/// </summary>
public const string NistP384 = nameof(NistP384);
/// <summary>
/// The name of the BrainpoolP256r1 curve.
/// </summary>
public const string BrainpoolP256r1 = nameof(BrainpoolP256r1);
/// <summary>
/// The name of the BrainpoolP384r1 curve.
/// </summary>
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";
/// <summary>
/// Returns true if the certificate is an ECC certificate.
/// </summary>
public static bool IsEccPolicy(string securityPolicyUri)
{
SecurityPolicyInfo? info = SecurityPolicies.GetInfo(securityPolicyUri);
if (info != null)
{
return info.CertificateKeyFamily == CertificateKeyFamily.ECC;
}
return false;
}
/// <summary>
/// Returns the NodeId for the certificate type for the specified certificate.
/// </summary>
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;
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="certificateType">the application certificatate type node id</param>
/// <returns>the ECCCurve, null if certificatate type id has no matching supported ECC curve</returns>
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;
}
/// <summary>
/// Returns the signature algorithm for the specified certificate.
/// </summary>
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;
}
/// <summary>
/// Returns the public key for the specified certificate.
/// </summary>
public static ECDsa? GetPublicKey(Certificate certificate)
{
return GetPublicKey(certificate, out string[]? _);
}
/// <summary>
/// Returns the public key for the specified certificate and outputs the security policy uris.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="NotImplementedException"></exception>
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);
}
/// <summary>
/// Returns the length of a ECDsa signature of a digest.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
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;
}
/// <summary>
/// Computes a signature.
/// </summary>
/// <exception cref="ArgumentException">
/// <paramref name="securityPolicyUri"/> cannot be resolved to a
/// known security policy.
/// </exception>
public static byte[]? Sign(
ArraySegment<byte> 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);
}
/// <summary>
/// Computes a signature.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
/// <exception cref="NotSupportedException"></exception>
public static byte[]? Sign(
ArraySegment<byte> 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);
}
}
/// <summary>
/// Verifies a signature.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
public static bool Verify(
ArraySegment<byte> 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);
}
/// <summary>
/// Verifies a signature.
/// </summary>
/// <exception cref="NotSupportedException"></exception>
/// <exception cref="ServiceResultException"></exception>
public static bool Verify(
ArraySegment<byte> 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);
}
/// <summary>
/// Adds padding to a buffer. Input: buffer with unencrypted data starting at 0;
/// plaintext data starting at offset; no padding.
/// </summary>
/// <param name="data">buffer with unencrypted data starting at 0; plaintext data
/// starting at offset; no padding.</param>
/// <param name="blockSize"></param>
/// <param name="trailingBytes">Additional bytes that will be appended after
/// padding (e.g., HMAC) and must be considered for block alignment.</param>
/// <returns>Output: buffer with unencrypted data starting at 0; plaintext data
/// starting at offset; padding added.</returns>
/// <exception cref="ArgumentNullException"></exception>
private static ArraySegment<byte> AddPadding(ArraySegment<byte> 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<byte>(dataArray, data.Offset, data.Count + paddingSize + paddingByteSize);
}
/// <summary>
/// Removes padding from a buffer. Input: buffer with unencrypted data starting at 0;
/// plaintext including padding starting at offset; signature removed.
/// </summary>
/// <param name="data">Input: buffer with unencrypted data starting at 0; plaintext
/// including padding starting at offset; signature removed.</param>
/// <param name="blockSize"></param>
/// <returns>Output: buffer with unencrypted data starting at 0; plaintext starting
/// at offset; padding excluded.</returns>
/// <exception cref="CryptographicException"></exception>
/// <exception cref="ArgumentNullException"></exception>
private static ArraySegment<byte> RemovePadding(ArraySegment<byte> 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<byte>(dataArray, 0, data.Offset + data.Count - paddingSize - paddingByteSize);
}
/// <summary>
/// Encrypts the buffer using the algorithm specified by the security policy.
/// </summary>
/// <exception cref="NotSupportedException"></exception>
/// <exception cref="CryptographicException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public static ArraySegment<byte> SymmetricEncryptAndSign(
ArraySegment<byte> 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<byte>(
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<byte>(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<byte> EncryptWithChaCha20Poly1305(
ArraySegment<byte> 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<byte>(
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<byte>() : 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<byte>(
dataArray,
0,
data.Offset + data.Count + kChaChaPolyTagLength);
}
private static ArraySegment<byte> DecryptWithChaCha20Poly1305(
ArraySegment<byte> 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<byte>(
dataArray,
data.Offset,
signOnly ? 0 : data.Count - kChaChaPolyTagLength);
var tag = new ArraySegment<byte>(
dataArray,
data.Offset + data.Count - kChaChaPolyTagLength,
kChaChaPolyTagLength);
var extraData = new ReadOnlySpan<byte>(
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<byte>(dataArray, 0, data.Offset + data.Count - kChaChaPolyTagLength);
}
private const int kAesGcmIvLength = 12;
private const int kAesGcmTagLength = 16;
private static ArraySegment<byte> EncryptWithAesGcm(
ArraySegment<byte> 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<byte>(
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<byte>() : 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<byte>(
dataArray,
0,
data.Offset + data.Count + kAesGcmTagLength);
}
private static ArraySegment<byte> DecryptWithAesGcm(
ArraySegment<byte> 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<byte>(
dataArray,
data.Offset,
signOnly ? 0 : data.Count - kAesGcmTagLength);
var tag = new ArraySegment<byte>(
dataArray,
data.Offset + data.Count - kAesGcmTagLength,
kAesGcmTagLength);
var extraData = new ReadOnlySpan<byte>(
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<byte>(dataArray, 0, data.Offset + data.Count - kAesGcmTagLength);
}
#endif
/// <summary>
/// Decrypts the buffer using the algorithm specified by the security policy.
/// </summary>
/// <exception cref="CryptographicException"></exception>
/// <exception cref="NotSupportedException"></exception>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ServiceResultException">
/// The signature HMAC could not be created.
/// </exception>
public static ArraySegment<byte> SymmetricDecryptAndVerify(
ArraySegment<byte> 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<byte>(
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<byte>(dataArray, 0, data.Offset + data.Count);
}
}
}