/* ======================================================================== * Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Security.Cryptography; using Opc.Ua.Bindings; namespace Opc.Ua.Bindings.Pcap.KeyLog { /// /// Immutable snapshot of the key material associated with a single /// activation. Decoupled from /// so it can be serialized to and from a /// keylog file and used to drive an offline OfflineSecureChannel. /// /// /// /// The snapshot contains both the client- and server-side derived keys /// because we need to be able to decrypt traffic in both directions /// regardless of whether the tap that produced the snapshot was on /// the client or the server side. /// /// /// All byte arrays are defensive copies of the originals so the /// snapshot is safe to keep alive long after the originating /// has been disposed. /// /// public sealed class ChannelKeyMaterial : IDisposable { /// /// Constructs an immutable snapshot. Pass null for any /// key/iv/nonce that is not present (e.g. when the security mode is /// ). /// /// /// is null. /// public ChannelKeyMaterial( uint channelId, uint tokenId, string securityPolicyUri, MessageSecurityMode securityMode, DateTime createdAt, int lifetime, byte[]? clientNonce, byte[]? serverNonce, byte[]? clientSigningKey, byte[]? clientEncryptingKey, byte[]? clientInitializationVector, byte[]? serverSigningKey, byte[]? serverEncryptingKey, byte[]? serverInitializationVector) { ArgumentNullException.ThrowIfNull(securityPolicyUri); ChannelId = channelId; TokenId = tokenId; SecurityPolicyUri = securityPolicyUri; SecurityMode = securityMode; CreatedAt = createdAt; Lifetime = lifetime; ClientNonce = Copy(clientNonce); ServerNonce = Copy(serverNonce); ClientSigningKey = Copy(clientSigningKey); ClientEncryptingKey = Copy(clientEncryptingKey); ClientInitializationVector = Copy(clientInitializationVector); ServerSigningKey = Copy(serverSigningKey); ServerEncryptingKey = Copy(serverEncryptingKey); ServerInitializationVector = Copy(serverInitializationVector); } /// /// The OPC UA secure-channel id this token belongs to. /// public uint ChannelId { get; } /// /// The token id within the channel. /// public uint TokenId { get; } /// /// The security policy URI in effect for this token (e.g. /// http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss). /// public string SecurityPolicyUri { get; } /// /// The message security mode in effect for this token. /// public MessageSecurityMode SecurityMode { get; } /// /// When the token was created (UTC, server clock). /// public DateTime CreatedAt { get; } /// /// The lifetime of the token in milliseconds. /// public int Lifetime { get; } /// /// The nonce provided by the client during OpenSecureChannel. /// public byte[]? ClientNonce { get; } /// /// The nonce provided by the server during OpenSecureChannel. /// public byte[]? ServerNonce { get; } /// /// The key used to sign messages sent by the client. /// public byte[]? ClientSigningKey { get; } /// /// The key used to encrypt messages sent by the client. /// public byte[]? ClientEncryptingKey { get; } /// /// The initialization vector used by the client when encrypting. /// public byte[]? ClientInitializationVector { get; } /// /// The key used to sign messages sent by the server. /// public byte[]? ServerSigningKey { get; } /// /// The key used to encrypt messages sent by the server. /// public byte[]? ServerEncryptingKey { get; } /// /// The initialization vector used by the server when encrypting. /// public byte[]? ServerInitializationVector { get; } /// /// Creates a snapshot from a live . /// /// /// is null. /// public static ChannelKeyMaterial From(ChannelToken token) { ArgumentNullException.ThrowIfNull(token); return new ChannelKeyMaterial( channelId: token.ChannelId, tokenId: token.TokenId, securityPolicyUri: token.SecurityPolicy?.Uri ?? SecurityPolicies.None, securityMode: InferSecurityMode(token), createdAt: token.CreatedAt, lifetime: token.Lifetime, clientNonce: token.ClientNonce, serverNonce: token.ServerNonce, clientSigningKey: token.ClientSigningKey, clientEncryptingKey: token.ClientEncryptingKey, clientInitializationVector: token.ClientInitializationVector, serverSigningKey: token.ServerSigningKey, serverEncryptingKey: token.ServerEncryptingKey, serverInitializationVector: token.ServerInitializationVector); } /// /// Securely clears all key, nonce, and IV byte arrays held by /// this instance using /// . /// /// /// Call this as soon as the material is no longer needed. After /// disposal the properties continue to point to the (now zeroed) /// arrays; subsequent reads will see only zero bytes. /// public void Dispose() { if (m_disposed) { return; } ZeroIfNotNull(ClientNonce); ZeroIfNotNull(ServerNonce); ZeroIfNotNull(ClientSigningKey); ZeroIfNotNull(ClientEncryptingKey); ZeroIfNotNull(ClientInitializationVector); ZeroIfNotNull(ServerSigningKey); ZeroIfNotNull(ServerEncryptingKey); ZeroIfNotNull(ServerInitializationVector); m_disposed = true; GC.SuppressFinalize(this); } private static byte[]? Copy(byte[]? value) { if (value is null) { return null; } byte[] copy = new byte[value.Length]; Buffer.BlockCopy(value, 0, copy, 0, value.Length); return copy; } private static void ZeroIfNotNull(byte[]? buffer) { if (buffer is { Length: > 0 }) { CryptographicOperations.ZeroMemory(buffer); } } private static MessageSecurityMode InferSecurityMode(ChannelToken token) { // ChannelToken doesn't carry the security mode directly. We infer // it from the presence of derived encryption keys: None when no // signing key is present at all, Sign when only signing keys are // present, SignAndEncrypt when encryption keys are present. if (token.ClientSigningKey is null && token.ServerSigningKey is null) { return MessageSecurityMode.None; } if (token.ClientEncryptingKey is null && token.ServerEncryptingKey is null) { return MessageSecurityMode.Sign; } return MessageSecurityMode.SignAndEncrypt; } private bool m_disposed; #if DEBUG /// /// Debug-only finalizer that warns through /// when a /// instance is collected without /// having been called. Helps catch leaks /// in tests; the finalizer is a no-op in Release builds. /// ~ChannelKeyMaterial() { if (!m_disposed) { System.Diagnostics.Trace.TraceWarning( "ChannelKeyMaterial finalized without Dispose; " + "key material may have persisted in heap. " + "Always Dispose ChannelKeyMaterial instances when done."); } } #endif } }