// DES privacy provider. // Copyright (C) 2008-2010 Malcolm Crowe, Lex Li, and other contributors. // // 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. using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; namespace GSF.Net.Snmp.Security { /// /// Privacy provider for 3DES. /// /// Ported from SNMP#NET Privacy3DES class. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "DES", Justification = "definition")] public sealed class TripleDESPrivacyProvider : IPrivacyProvider { private readonly SaltGenerator _salt = new SaltGenerator(); private readonly OctetString _phrase; /// /// Initializes a new instance of the class. /// /// The phrase. /// The authentication provider. public TripleDESPrivacyProvider(OctetString phrase, IAuthenticationProvider auth) { if (auth == null) { throw new ArgumentNullException(nameof(auth)); } // IMPORTANT: in this way privacy cannot be non-default. if (auth == DefaultAuthenticationProvider.Instance) { throw new ArgumentException("If authentication is off, then privacy cannot be used.", nameof(auth)); } _phrase = phrase ?? throw new ArgumentNullException(nameof(phrase)); AuthenticationProvider = auth; } /// /// Corresponding . /// public IAuthenticationProvider AuthenticationProvider { get; private set; } [Obsolete("Use EngineIds instead.")] public OctetString EngineId { get; set; } /// /// Engine IDs. /// /// This is an optional field, and only used by TRAP v2 authentication. public ICollection EngineIds { get; set; } /// /// Encrypt scoped PDU using DES encryption protocol /// /// Unencrypted scoped PDU byte array /// Encryption key. Key has to be at least 32 bytes is length /// Privacy parameters out buffer. This field will be filled in with information /// required to decrypt the information. Output length of this field is 8 bytes and space has to be reserved /// in the USM header to store this information /// Encrypted byte array /// Thrown when encryption key is null or length of the encryption key is too short. public static byte[] Encrypt(byte[] unencryptedData, byte[] key, byte[] privacyParameters) { if (unencryptedData == null) { throw new ArgumentNullException(nameof(unencryptedData)); } if (key == null) { throw new ArgumentNullException(nameof(key)); } if (privacyParameters == null) { throw new ArgumentNullException(nameof(privacyParameters)); } if (key.Length < MinimumKeyLength) { throw new ArgumentException($"Encryption key length has to 32 bytes or more. Current: {key.Length}.", nameof(key)); } //privacyParameters = GetSalt(engineBoots); //byte[] privParamHash = authDigest.ComputeHash(privacyParameters, 0, privacyParameters.Length); //privacyParameters = new byte[8]; //Buffer.BlockCopy(privParamHash, 0, privacyParameters, 0, 8); byte[] iv = GetIV(key, privacyParameters); // DES uses 8 byte keys but we need 16 to encrypt ScopedPdu. Get first 8 bytes and use them as encryption key byte[] outKey = GetKey(key); byte[] encryptedData = null; using (TripleDES des = TripleDES.Create()) { des.Mode = CipherMode.CBC; des.Padding = PaddingMode.None; using (ICryptoTransform transform = des.CreateEncryptor(outKey, iv)) { if ((unencryptedData.Length % 8) == 0) { encryptedData = transform.TransformFinalBlock(unencryptedData, 0, unencryptedData.Length); } else { byte[] tmpbuffer = new byte[8 * ((unencryptedData.Length / 8) + 1)]; Buffer.BlockCopy(unencryptedData, 0, tmpbuffer, 0, unencryptedData.Length); encryptedData = transform.TransformFinalBlock(tmpbuffer, 0, tmpbuffer.Length); } } } return encryptedData; } /// /// Decrypt DES encrypted scoped PDU. /// /// Source data buffer /// Decryption key. Key length has to be 32 bytes in length or longer (bytes beyond 32 bytes are ignored). /// Privacy parameters extracted from USM header /// Decrypted byte array /// Thrown when encrypted data is null or length == 0 /// Thrown when encryption key length is less then 32 byte or if privacy parameters /// argument is null or length other then 8 bytes public static byte[] Decrypt(byte[] encryptedData, byte[] key, byte[] privacyParameters) { if (encryptedData == null) { throw new ArgumentNullException(nameof(encryptedData)); } if (key == null) { throw new ArgumentNullException(nameof(key)); } if (privacyParameters == null) { throw new ArgumentNullException(nameof(privacyParameters)); } if (encryptedData.Length == 0) { throw new ArgumentException("Empty encrypted data.", nameof(encryptedData)); } if ((encryptedData.Length % 8) != 0) { throw new ArgumentException("Encrypted data buffer has to be divisible by 8.", nameof(encryptedData)); } if (privacyParameters.Length != PrivacyParametersLength) { throw new ArgumentOutOfRangeException(nameof(privacyParameters), "Privacy parameters argument has to be 8 bytes long."); } if (key.Length < MinimumKeyLength) { throw new ArgumentOutOfRangeException(nameof(key), "Decryption key has to be at least 16 bytes long."); } byte[] iv = GetIV(key, privacyParameters); using (TripleDES des = TripleDES.Create()) { des.Mode = CipherMode.CBC; des.Padding = PaddingMode.Zeros; // normalize key - generated key is 32 bytes long, we need 24 bytes to encrypt byte[] outKey = new byte[24]; Buffer.BlockCopy(key, 0, outKey, 0, outKey.Length); using (ICryptoTransform transform = des.CreateDecryptor(outKey, iv)) { byte[] decryptedData = transform.TransformFinalBlock(encryptedData, 0, encryptedData.Length); return decryptedData; } } } /// /// Generate IV from the privacy key and salt value returned by GetSalt method. /// /// 32 byte privacy key /// Salt value returned by GetSalt method /// IV value used in the encryption process private static byte[] GetIV(IList privacyKey, IList salt) { if (privacyKey.Count < MinimumKeyLength) { throw new ArgumentException("Invalid privacy key length", nameof(privacyKey)); } byte[] iv = new byte[8]; for (int i = 0; i < iv.Length; i++) { iv[i] = (byte)(salt[i] ^ privacyKey[24 + i]); } return iv; } /// /// Extract and return DES encryption key. /// Privacy password is 16 bytes in length. Only the first 8 bytes are used as DES password. Remaining /// 8 bytes are used as pre-IV value. /// /// 16 byte privacy password /// 8 byte DES encryption password private static byte[] GetKey(byte[] privacyPassword) { if (privacyPassword.Length < 32) { throw new ArgumentException("Invalid privacy key length.", nameof(privacyPassword)); } byte[] key = new byte[24]; Buffer.BlockCopy(privacyPassword, 0, key, 0, key.Length); return key; } /// /// Returns the length of privacyParameters USM header field. For DES, field length is 8. /// public static int PrivacyParametersLength { get { return 8; } } /// /// Returns minimum encryption/decryption key length. For DES, returned value is 16. /// /// DES protocol itself requires an 8 byte key. Additional 8 bytes are used for generating the /// encryption IV. For encryption itself, first 8 bytes of the key are used. /// public static int MinimumKeyLength { get { return MaximumKeyLength; } } /// /// Return maximum encryption/decryption key length. For DES, returned value is 16. /// /// DES protocol itself requires an 8 byte key. Additional 8 bytes are used for generating the /// encryption IV. For encryption itself, first 8 bytes of the key are used. /// public static int MaximumKeyLength { get { return 32; } } #region IPrivacyProvider Members /// /// Decrypts the specified data. /// /// The data. /// The parameters. /// public ISnmpData Decrypt(ISnmpData data, SecurityParameters parameters) { if (data == null) { throw new ArgumentNullException(nameof(data)); } if (parameters == null) { throw new ArgumentNullException(nameof(parameters)); } SnmpType code = data.TypeCode; if (code != SnmpType.OctetString) { throw new ArgumentException($"Cannot decrypt the scope data: {code}.", nameof(data)); } OctetString octets = (OctetString)data; byte[] bytes = octets.GetRaw(); byte[] pkey = PasswordToKey(_phrase.GetRaw(), parameters.EngineId.GetRaw()); try { // decode encrypted packet byte[] decrypted = Decrypt(bytes, pkey, parameters.PrivacyParameters.GetRaw()); ISnmpData result = DataFactory.CreateSnmpData(decrypted); if (result.TypeCode != SnmpType.Sequence) { DecryptionException newException = new DecryptionException("DES decryption failed"); newException.SetBytes(bytes); throw newException; } return result; } catch (Exception ex) { DecryptionException newException = new DecryptionException("DES decryption failed", ex); newException.SetBytes(bytes); throw newException; } } /// /// Encrypts the specified scope. /// /// The scope data. /// The parameters. /// public ISnmpData Encrypt(ISnmpData data, SecurityParameters parameters) { if (data == null) { throw new ArgumentNullException(nameof(data)); } if (parameters == null) { throw new ArgumentNullException(nameof(parameters)); } if (data.TypeCode != SnmpType.Sequence && !(data is ISnmpPdu)) { throw new ArgumentException("Invalid data type.", nameof(data)); } byte[] pkey = PasswordToKey(_phrase.GetRaw(), parameters.EngineId.GetRaw()); byte[] bytes = data.ToBytes(); int reminder = bytes.Length % 8; int count = reminder == 0 ? 0 : 8 - reminder; using (MemoryStream stream = new MemoryStream()) { stream.Write(bytes, 0, bytes.Length); for (int i = 0; i < count; i++) { stream.WriteByte(1); } bytes = stream.ToArray(); } byte[] encrypted = Encrypt(bytes, pkey, parameters.PrivacyParameters.GetRaw()); return new OctetString(encrypted); } /// /// Gets the salt. /// /// The salt. public OctetString Salt { get { return new OctetString(_salt.GetSaltBytes()); } } /// /// Passwords to key. /// /// The secret. /// The engine identifier. /// public byte[] PasswordToKey(byte[] secret, byte[] engineId) { byte[] encryptionKey = AuthenticationProvider.PasswordToKey(secret, engineId); if (encryptionKey.Length < MinimumKeyLength) { encryptionKey = ExtendShortKey(encryptionKey, secret, engineId, AuthenticationProvider); } return encryptionKey; } private byte[] ExtendShortKey(byte[] shortKey, byte[] password, byte[] engineID, IAuthenticationProvider authProtocol) { int length = shortKey.Length; byte[] extendedKey = new byte[MinimumKeyLength]; Buffer.BlockCopy(shortKey, 0, extendedKey, 0, shortKey.Length); while (length < MinimumKeyLength) { byte[] key = authProtocol.PasswordToKey(shortKey, engineID); int copyBytes = Math.Min(MaximumKeyLength - length, authProtocol.DigestLength); Buffer.BlockCopy(key, 0, extendedKey, length, copyBytes); length += copyBytes; } return extendedKey; } #endregion /// /// Returns a string that represents this object. /// /// public override string ToString() { return "3DES privacy provider"; } } }