// 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";
}
}
}