// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Formats.Asn1; using System.Runtime.InteropServices; using System.Security.Cryptography.Asn1; using System.Security.Cryptography.Pkcs; using System.Text; using Internal.Cryptography; namespace System.Security.Cryptography { internal static class PasswordBasedEncryption { private readonly ref struct OptionalReadOnlySpan<T> { public OptionalReadOnlySpan(ReadOnlySpan<T> value, bool hasValue) { Value = value; HasValue = hasValue; } public ReadOnlySpan<T> Value { get; } public bool HasValue { get; } } internal const int IterationLimit = 600000; private static CryptographicException AlgorithmKdfRequiresChars(string algId) { return new CryptographicException(SR.Cryptography_AlgKdfRequiresChars, algId); } internal static void ValidatePbeParameters( PbeParameters pbeParameters, ReadOnlySpan<char> password, ReadOnlySpan<byte> passwordBytes) { // Leave the ArgumentNullException in the public entrypoints. Debug.Assert(pbeParameters != null); // Constructor promise. Debug.Assert(pbeParameters.IterationCount > 0); PbeEncryptionAlgorithm encryptionAlgorithm = pbeParameters.EncryptionAlgorithm; switch (encryptionAlgorithm) { case PbeEncryptionAlgorithm.Aes128Cbc: case PbeEncryptionAlgorithm.Aes192Cbc: case PbeEncryptionAlgorithm.Aes256Cbc: return; case PbeEncryptionAlgorithm.TripleDes3KeyPkcs12: { if (pbeParameters.HashAlgorithm != HashAlgorithmName.SHA1) { throw new CryptographicException( SR.Cryptography_UnknownHashAlgorithm, pbeParameters.HashAlgorithm.Name); } if (passwordBytes.Length > 0 && password.Length == 0) { throw AlgorithmKdfRequiresChars( encryptionAlgorithm.ToString()); } return; } } throw new CryptographicException( SR.Cryptography_UnknownAlgorithmIdentifier, encryptionAlgorithm.ToString()); } internal static unsafe int Decrypt( in AlgorithmIdentifierAsn algorithmIdentifier, ReadOnlySpan<char> password, ReadOnlySpan<byte> passwordBytes, ReadOnlySpan<byte> encryptedData, Span<byte> destination) { return Decrypt( algorithmIdentifier.AsValueAlgorithmIdentifierAsn(), password, passwordBytes, encryptedData, destination); } [SuppressMessage("Microsoft.Security", "CA5350", Justification = "3DES used when specified by the input data")] [SuppressMessage("Microsoft.Security", "CA5351", Justification = "DES used when specified by the input data")] internal static unsafe int Decrypt( in ValueAlgorithmIdentifierAsn algorithmIdentifier, ReadOnlySpan<char> password, ReadOnlySpan<byte> passwordBytes, ReadOnlySpan<byte> encryptedData, Span<byte> destination) { Debug.Assert(destination.Length >= encryptedData.Length); if (!Helpers.HasSymmetricEncryption) { throw new CryptographicException( SR.Format( SR.Cryptography_UnknownAlgorithmIdentifier, algorithmIdentifier.Algorithm)); } // Don't check that algorithmIdentifier.Parameters is set here. // Maybe some future PBES3 will have one with a default. HashAlgorithmName digestAlgorithmName; SymmetricAlgorithm? cipher = null; bool pkcs12 = false; switch (algorithmIdentifier.Algorithm) { case Oids.PbeWithMD5AndDESCBC: digestAlgorithmName = HashAlgorithmName.MD5; cipher = DES.Create(); break; case Oids.PbeWithMD5AndRC2CBC: digestAlgorithmName = HashAlgorithmName.MD5; cipher = CreateRC2(); break; case Oids.PbeWithSha1AndDESCBC: digestAlgorithmName = HashAlgorithmName.SHA1; cipher = DES.Create(); break; case Oids.PbeWithSha1AndRC2CBC: digestAlgorithmName = HashAlgorithmName.SHA1; cipher = CreateRC2(); break; case Oids.Pkcs12PbeWithShaAnd3Key3Des: digestAlgorithmName = HashAlgorithmName.SHA1; cipher = TripleDES.Create(); pkcs12 = true; break; case Oids.Pkcs12PbeWithShaAnd2Key3Des: digestAlgorithmName = HashAlgorithmName.SHA1; cipher = TripleDES.Create(); cipher.KeySize = 128; pkcs12 = true; break; case Oids.Pkcs12PbeWithShaAnd128BitRC2: digestAlgorithmName = HashAlgorithmName.SHA1; cipher = CreateRC2(); cipher.KeySize = 128; pkcs12 = true; break; case Oids.Pkcs12PbeWithShaAnd40BitRC2: digestAlgorithmName = HashAlgorithmName.SHA1; cipher = CreateRC2(); cipher.KeySize = 40; pkcs12 = true; break; case Oids.PasswordBasedEncryptionScheme2: return Pbes2Decrypt( new OptionalReadOnlySpan<byte>(algorithmIdentifier.Parameters, algorithmIdentifier.HasParameters), password, passwordBytes, encryptedData, destination); default: throw new CryptographicException( SR.Format( SR.Cryptography_UnknownAlgorithmIdentifier, algorithmIdentifier.Algorithm)); } Debug.Assert(digestAlgorithmName.Name != null); Debug.Assert(cipher != null); using (cipher) { if (pkcs12) { if (password.IsEmpty && passwordBytes.Length > 0) { throw AlgorithmKdfRequiresChars(algorithmIdentifier.Algorithm); } return Pkcs12PbeDecrypt( algorithmIdentifier, password, digestAlgorithmName, cipher, encryptedData, destination); } using (IncrementalHash hasher = IncrementalHash.CreateHash(digestAlgorithmName)) { Span<byte> buf = stackalloc byte[128]; scoped ReadOnlySpan<byte> effectivePasswordBytes = default; byte[]? rented = null; System.Text.Encoding? encoding = null; if (passwordBytes.Length > 0 || password.Length == 0) { effectivePasswordBytes = passwordBytes; } else { encoding = System.Text.Encoding.UTF8; int byteCount = encoding.GetByteCount(password); if (byteCount > buf.Length) { rented = CryptoPool.Rent(byteCount); buf = rented.AsSpan(0, byteCount); } else { buf = buf.Slice(0, byteCount); } } fixed (byte* maybeRentedPtr = &MemoryMarshal.GetReference(buf)) { if (encoding != null) { int written = encoding.GetBytes(password, buf); Debug.Assert(written == buf.Length); buf = buf.Slice(0, written); effectivePasswordBytes = buf; } try { return Pbes1Decrypt( new OptionalReadOnlySpan<byte>(algorithmIdentifier.Parameters, algorithmIdentifier.HasParameters), effectivePasswordBytes, hasher, cipher, encryptedData, destination); } finally { CryptographicOperations.ZeroMemory(buf); if (rented != null) { CryptoPool.Return(rented, clearSize: 0); } } } } } } [SuppressMessage("Microsoft.Security", "CA5350", Justification = "3DES used when specified by the input data")] internal static void InitiateEncryption( PbeParameters pbeParameters, out SymmetricAlgorithm cipher, out string hmacOid, out string encryptionAlgorithmOid, out bool isPkcs12) { Debug.Assert(pbeParameters != null); if (!Helpers.HasSymmetricEncryption) { throw new CryptographicException( SR.Format( SR.Cryptography_UnknownAlgorithmIdentifier, pbeParameters.EncryptionAlgorithm)); } isPkcs12 = false; switch (pbeParameters.EncryptionAlgorithm) { case PbeEncryptionAlgorithm.Aes128Cbc: cipher = Aes.Create(); cipher.KeySize = 128; encryptionAlgorithmOid = Oids.Aes128Cbc; break; case PbeEncryptionAlgorithm.Aes192Cbc: cipher = Aes.Create(); cipher.KeySize = 192; encryptionAlgorithmOid = Oids.Aes192Cbc; break; case PbeEncryptionAlgorithm.Aes256Cbc: cipher = Aes.Create(); cipher.KeySize = 256; encryptionAlgorithmOid = Oids.Aes256Cbc; break; case PbeEncryptionAlgorithm.TripleDes3KeyPkcs12: cipher = TripleDES.Create(); cipher.KeySize = 192; encryptionAlgorithmOid = Oids.Pkcs12PbeWithShaAnd3Key3Des; isPkcs12 = true; break; default: throw new CryptographicException( SR.Format( SR.Cryptography_UnknownAlgorithmIdentifier, pbeParameters.EncryptionAlgorithm)); } HashAlgorithmName prf = pbeParameters.HashAlgorithm; if (prf == HashAlgorithmName.SHA256) { hmacOid = Oids.HmacWithSha256; } else if (prf == HashAlgorithmName.SHA384) { hmacOid = Oids.HmacWithSha384; } else if (prf == HashAlgorithmName.SHA512) { hmacOid = Oids.HmacWithSha512; } else if (prf == HashAlgorithmName.SHA1) { hmacOid = Oids.HmacWithSha1; } else { cipher.Dispose(); throw new CryptographicException(SR.Cryptography_UnknownHashAlgorithm, prf.Name); } // PKCS12-PBE should have been verified to be using SHA-1 already. Debug.Assert(hmacOid == Oids.HmacWithSha1 || !isPkcs12); } [SuppressMessage("Microsoft.Security", "CA5379", Justification = "SHA1 used if specified by argument")] internal static unsafe int Encrypt( ReadOnlySpan<char> password, ReadOnlySpan<byte> passwordBytes, SymmetricAlgorithm cipher, bool isPkcs12, AsnWriter source, PbeParameters pbeParameters, ReadOnlySpan<byte> salt, byte[] destination, Span<byte> ivDest) { byte[]? pwdTmpBytes = null; byte[] derivedKey; byte[] iv = cipher.IV; int sourceLength = source.GetEncodedLength(); byte[] sourceRent = CryptoPool.Rent(sourceLength); int keySizeBytes = cipher.KeySize / 8; int iterationCount = pbeParameters.IterationCount; HashAlgorithmName prf = pbeParameters.HashAlgorithm; System.Text.Encoding encoding = System.Text.Encoding.UTF8; if (!isPkcs12) { if (passwordBytes.Length == 0 && password.Length > 0) { pwdTmpBytes = new byte[encoding.GetByteCount(password)]; } else if (passwordBytes.Length == 0) { pwdTmpBytes = Array.Empty<byte>(); } else { pwdTmpBytes = new byte[passwordBytes.Length]; } } fixed (byte* pkcs8RentPin = sourceRent) fixed (byte* pwdTmpBytesPtr = pwdTmpBytes) { if (isPkcs12) { // Verified by ValidatePbeParameters, which should be called at entrypoints. Debug.Assert(password.Length > 0 || passwordBytes.IsEmpty); Debug.Assert(pbeParameters.HashAlgorithm == HashAlgorithmName.SHA1); derivedKey = new byte[keySizeBytes]; Pkcs12Kdf.DeriveCipherKey( password, prf, iterationCount, salt, derivedKey); Pkcs12Kdf.DeriveIV( password, prf, iterationCount, salt, iv); ivDest.Clear(); } else { if (passwordBytes.Length > 0) { Debug.Assert(pwdTmpBytes!.Length == passwordBytes.Length); passwordBytes.CopyTo(pwdTmpBytes); } else if (password.Length > 0) { int length = encoding.GetBytes(password, pwdTmpBytes); if (length != pwdTmpBytes!.Length) { Debug.Fail($"UTF-8 encoding size changed between GetByteCount and GetBytes"); throw new CryptographicException(); } } else { Debug.Assert(pwdTmpBytes!.Length == 0); } derivedKey = DeriveKey(pwdTmpBytes, salt, iterationCount, prf, keySizeBytes); iv.CopyTo(ivDest); } fixed (byte* keyPtr = derivedKey) { CryptographicOperations.ZeroMemory(pwdTmpBytes); using (ICryptoTransform encryptor = cipher.CreateEncryptor(derivedKey, iv)) { Debug.Assert(encryptor.CanTransformMultipleBlocks); int blockSizeBytes = (cipher.BlockSize / 8); int remaining = sourceLength % blockSizeBytes; int fullBlocksLength = sourceLength - remaining; try { if (!source.TryEncode(sourceRent, out _)) { Debug.Fail("TryEncode failed with a pre-allocated buffer"); throw new CryptographicException(); } int written = 0; if (fullBlocksLength != 0) { written = encryptor.TransformBlock( sourceRent, 0, fullBlocksLength, destination, 0); } byte[] lastBlock = encryptor.TransformFinalBlock( sourceRent, written, remaining); lastBlock.AsSpan().CopyTo(destination.AsSpan(written)); return written + lastBlock.Length; } finally { CryptoPool.Return(sourceRent, sourceLength); } } } } } private static unsafe int Pbes2Decrypt( OptionalReadOnlySpan<byte> algorithmParameters, ReadOnlySpan<char> password, ReadOnlySpan<byte> passwordBytes, ReadOnlySpan<byte> encryptedData, Span<byte> destination) { Span<byte> buf = stackalloc byte[128]; scoped ReadOnlySpan<byte> effectivePasswordBytes = default; byte[]? rented = null; System.Text.Encoding? encoding = null; if (passwordBytes.Length > 0 || password.Length == 0) { effectivePasswordBytes = passwordBytes; } else { encoding = System.Text.Encoding.UTF8; int byteCount = encoding.GetByteCount(password); if (byteCount > buf.Length) { rented = CryptoPool.Rent(byteCount); buf = rented.AsSpan(0, byteCount); } else { buf = buf.Slice(0, byteCount); } } fixed (byte* maybeRentedPtr = &MemoryMarshal.GetReference(buf)) { if (encoding != null) { int written = encoding.GetBytes(password, buf); Debug.Assert(written == buf.Length); buf = buf.Slice(0, written); effectivePasswordBytes = buf; } try { return Pbes2Decrypt( algorithmParameters, effectivePasswordBytes, encryptedData, destination); } finally { if (rented != null) { CryptoPool.Return(rented, buf.Length); } } } } private static unsafe int Pbes2Decrypt( OptionalReadOnlySpan<byte> algorithmParameters, ReadOnlySpan<byte> password, ReadOnlySpan<byte> encryptedData, Span<byte> destination) { if (!algorithmParameters.HasValue) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } ValuePBES2Params.Decode(algorithmParameters.Value, AsnEncodingRules.BER, out ValuePBES2Params pbes2Params); if (pbes2Params.KeyDerivationFunc.Algorithm != Oids.Pbkdf2) { throw new CryptographicException( SR.Format( SR.Cryptography_UnknownAlgorithmIdentifier, pbes2Params.EncryptionScheme.Algorithm)); } Rfc2898DeriveBytes pbkdf2 = OpenPbkdf2( password, new OptionalReadOnlySpan<byte>(pbes2Params.KeyDerivationFunc.Parameters, pbes2Params.KeyDerivationFunc.HasParameters), out int? requestedKeyLength); using (pbkdf2) { // The biggest block size (for IV) we support is AES (128-bit / 16 byte) Span<byte> iv = stackalloc byte[16]; SymmetricAlgorithm cipher = OpenCipher( pbes2Params.EncryptionScheme, requestedKeyLength, ref iv); using (cipher) { byte[] key = pbkdf2.GetBytes(cipher.KeySize / 8); fixed (byte* keyPtr = key) { try { return Decrypt(cipher, key, iv, encryptedData, destination); } finally { CryptographicOperations.ZeroMemory(key); } } } } } [SuppressMessage("Microsoft.Security", "CA5350", Justification = "3DES used when specified by the input data")] [SuppressMessage("Microsoft.Security", "CA5351", Justification = "DES used when specified by the input data")] private static SymmetricAlgorithm OpenCipher( in ValueAlgorithmIdentifierAsn encryptionScheme, int? requestedKeyLength, ref Span<byte> iv) { string? algId = encryptionScheme.Algorithm; if (!Helpers.HasSymmetricEncryption) { throw new CryptographicException( SR.Format(SR.Cryptography_AlgorithmNotSupported, algId)); } if (algId == Oids.Aes128Cbc || algId == Oids.Aes192Cbc || algId == Oids.Aes256Cbc) { // https://tools.ietf.org/html/rfc8018#appendix-B.2.5 int correctKeySize; switch (algId) { case Oids.Aes128Cbc: correctKeySize = 16; break; case Oids.Aes192Cbc: correctKeySize = 24; break; case Oids.Aes256Cbc: correctKeySize = 32; break; default: Debug.Fail("Key-sized OID included in the if, but not the switch"); throw new CryptographicException(); } if (requestedKeyLength != null && requestedKeyLength != correctKeySize) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } // The parameters field ... shall have type OCTET STRING (SIZE(16)) // specifying the initialization vector ... ReadIvParameter( new OptionalReadOnlySpan<byte>(encryptionScheme.Parameters, encryptionScheme.HasParameters), 16, ref iv); Aes aes = Aes.Create(); aes.KeySize = correctKeySize * 8; return aes; } if (algId == Oids.TripleDesCbc) { // https://tools.ietf.org/html/rfc8018#appendix-B.2.2 // ... has a 24-octet encryption key ... if (requestedKeyLength != null && requestedKeyLength != 24) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } // The parameters field associated with this OID ... shall have type // OCTET STRING (SIZE(8)) specifying the initialization vector ... ReadIvParameter( new OptionalReadOnlySpan<byte>(encryptionScheme.Parameters, encryptionScheme.HasParameters), 8, ref iv); return TripleDES.Create(); } if (algId == Oids.Rc2Cbc) { // https://tools.ietf.org/html/rfc8018#appendix-B.2.3 if (!encryptionScheme.HasParameters) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } // RC2 has a variable key size. RFC 8018 does not define a default, // so of PBKDF2 didn't provide it, fail. if (requestedKeyLength == null) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } ValueRc2CbcParameters.Decode( encryptionScheme.Parameters, AsnEncodingRules.BER, out ValueRc2CbcParameters rc2Parameters); // iv is the eight-octet initialization vector if (rc2Parameters.Iv.Length != 8) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } RC2 rc2 = CreateRC2(); rc2.KeySize = requestedKeyLength.Value * 8; rc2.EffectiveKeySize = rc2Parameters.GetEffectiveKeyBits(); rc2Parameters.Iv.CopyTo(iv); iv = iv.Slice(0, rc2Parameters.Iv.Length); return rc2; } if (algId == Oids.DesCbc) { // https://tools.ietf.org/html/rfc8018#appendix-B.2.1 // ... has an eight-octet encryption key ... if (requestedKeyLength != null && requestedKeyLength != 8) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } // The parameters field associated with this OID ... shall have type // OCTET STRING (SIZE(8)) specifying the initialization vector ... ReadIvParameter( new OptionalReadOnlySpan<byte>(encryptionScheme.Parameters, encryptionScheme.HasParameters), 8, ref iv); return DES.Create(); } throw new CryptographicException(SR.Cryptography_UnknownAlgorithmIdentifier, algId); } private static void ReadIvParameter( OptionalReadOnlySpan<byte> encryptionSchemeParameters, int length, ref Span<byte> iv) { if (!encryptionSchemeParameters.HasValue) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } try { ReadOnlySpan<byte> source = encryptionSchemeParameters.Value; bool gotIv = AsnDecoder.TryReadOctetString( source, iv, AsnEncodingRules.BER, out int consumed, out int bytesWritten); if (!gotIv || bytesWritten != length || consumed != source.Length) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } iv = iv.Slice(0, bytesWritten); } catch (AsnContentException e) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); } } [SuppressMessage("Microsoft.Security", "CA5379", Justification = "SHA1 used if specified by argument")] private static unsafe Rfc2898DeriveBytes OpenPbkdf2( ReadOnlySpan<byte> password, OptionalReadOnlySpan<byte> parameters, out int? requestedKeyLength) { if (!parameters.HasValue) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } ValuePbkdf2Params.Decode(parameters.Value, AsnEncodingRules.BER, out ValuePbkdf2Params pbkdf2Params); // No OtherSource is defined in RFC 2898 or RFC 8018, so whatever // algorithm was requested isn't one we know. if (pbkdf2Params.Salt.HasOtherSource) { throw new CryptographicException( SR.Format( SR.Cryptography_UnknownAlgorithmIdentifier, pbkdf2Params.Salt.OtherSource.Algorithm)); } if (!pbkdf2Params.Salt.HasSpecified) { Debug.Fail($"No Specified Salt value is present, indicating a new choice was unhandled"); throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } HashAlgorithmName prf = pbkdf2Params.Prf.Algorithm switch { Oids.HmacWithSha1 => HashAlgorithmName.SHA1, Oids.HmacWithSha256 => HashAlgorithmName.SHA256, Oids.HmacWithSha384 => HashAlgorithmName.SHA384, Oids.HmacWithSha512 => HashAlgorithmName.SHA512, _ => throw new CryptographicException( SR.Format( SR.Cryptography_UnknownAlgorithmIdentifier, pbkdf2Params.Prf.Algorithm)), }; // All of the PRFs that we know about have NULL parameters, so check that now that we know // it's not just that we don't know the algorithm. if (!pbkdf2Params.Prf.HasNullEquivalentParameters()) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } int iterationCount = NormalizeIterationCount(pbkdf2Params.IterationCount); ReadOnlySpan<byte> saltSpan = pbkdf2Params.Salt.Specified; byte[] tmpPassword = new byte[password.Length]; byte[] tmpSalt = new byte[saltSpan.Length]; fixed (byte* tmpPasswordPtr = tmpPassword) fixed (byte* tmpSaltPtr = tmpSalt) { password.CopyTo(tmpPassword); saltSpan.CopyTo(tmpSalt); try { requestedKeyLength = pbkdf2Params.KeyLength; return OpenPbkdf2( tmpPassword, tmpSalt, iterationCount, prf); } catch (ArgumentException e) { // Salt too small is the most likely candidate. throw new CryptographicException(SR.Argument_InvalidValue, e); } finally { CryptographicOperations.ZeroMemory(tmpPassword); CryptographicOperations.ZeroMemory(tmpSalt); } } } private static unsafe int Pbes1Decrypt( OptionalReadOnlySpan<byte> algorithmParameters, ReadOnlySpan<byte> password, IncrementalHash hasher, SymmetricAlgorithm cipher, ReadOnlySpan<byte> encryptedData, Span<byte> destination) { // https://tools.ietf.org/html/rfc2898#section-6.1.2 // 1. Obtain the eight-octet salt S and iteration count c. if (!algorithmParameters.HasValue) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } ValuePBEParameter.Decode(algorithmParameters.Value, AsnEncodingRules.BER, out ValuePBEParameter pbeParameters); if (pbeParameters.Salt.Length != 8) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } if (pbeParameters.IterationCount < 1) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } int iterationCount = NormalizeIterationCount(pbeParameters.IterationCount); // 2. Apply PBKDF1<hash>(P, S, c, 16) to produce a derived key DK of length 16 octets Span<byte> dk = stackalloc byte[16]; try { Pbkdf1(hasher, password, pbeParameters.Salt, iterationCount, dk); // 3. Separate the derived key DK into an encryption key K consisting of the // first eight octets of DK and an initialization vector IV consisting of the // next 8. Span<byte> k = dk.Slice(0, 8); Span<byte> iv = dk.Slice(8, 8); // 4 & 5 together are "use CBC with what eventually became called PKCS7 padding" return Decrypt(cipher, k, iv, encryptedData, destination); } finally { CryptographicOperations.ZeroMemory(dk); } } private static unsafe int Pkcs12PbeDecrypt( in ValueAlgorithmIdentifierAsn algorithmIdentifier, ReadOnlySpan<char> password, HashAlgorithmName hashAlgorithm, SymmetricAlgorithm cipher, ReadOnlySpan<byte> encryptedData, Span<byte> destination) { // https://tools.ietf.org/html/rfc7292#appendix-C if (!algorithmIdentifier.HasParameters) { throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } // 3DES, "two-key" 3DES, RC2-128 and RC2-40 are the only ciphers that should be here. // That means 64-bit block sizes and 192-bit keys (3DES-3). So stack allocated key/IV are safe. if (cipher.KeySize > 256 || cipher.BlockSize > 256) { Debug.Fail( $"Unexpected cipher characteristics by {cipher.GetType().FullName}, KeySize={cipher.KeySize}, BlockSize={cipher.BlockSize}"); throw new CryptographicException(); } ValuePBEParameter.Decode( algorithmIdentifier.Parameters, AsnEncodingRules.BER, out ValuePBEParameter pbeParameters); int iterationCount = NormalizeIterationCount(pbeParameters.IterationCount, IterationLimit); Span<byte> iv = stackalloc byte[cipher.BlockSize / 8]; Span<byte> key = stackalloc byte[cipher.KeySize / 8]; ReadOnlySpan<byte> saltSpan = pbeParameters.Salt; try { Pkcs12Kdf.DeriveIV( password, hashAlgorithm, iterationCount, saltSpan, iv); Pkcs12Kdf.DeriveCipherKey( password, hashAlgorithm, iterationCount, saltSpan, key); return Decrypt(cipher, key, iv, encryptedData, destination); } finally { CryptographicOperations.ZeroMemory(key); CryptographicOperations.ZeroMemory(iv); } } private static unsafe int Decrypt( SymmetricAlgorithm cipher, ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> encryptedData, Span<byte> destination) { // When we define a Span-based decryption API this should be changed to use it. byte[] tmpKey = new byte[key.Length]; byte[] tmpIv = new byte[iv.Length]; byte[] rentedEncryptedData = CryptoPool.Rent(encryptedData.Length); byte[] rentedDestination = CryptoPool.Rent(destination.Length); // Keep all the arrays pinned so they can be correctly cleared fixed (byte* tmpKeyPtr = tmpKey) fixed (byte* tmpIvPtr = tmpIv) fixed (byte* rentedEncryptedDataPtr = rentedEncryptedData) fixed (byte* rentedDestinationPtr = rentedDestination) { try { key.CopyTo(tmpKey); iv.CopyTo(tmpIv); using (ICryptoTransform decryptor = cipher.CreateDecryptor(tmpKey, tmpIv)) { Debug.Assert(decryptor.CanTransformMultipleBlocks); encryptedData.CopyTo(rentedEncryptedData); int writeOffset = decryptor.TransformBlock( rentedEncryptedData, 0, encryptedData.Length, rentedDestination, 0); rentedDestination.AsSpan(0, writeOffset).CopyTo(destination); byte[] tmpEnd = decryptor.TransformFinalBlock(Array.Empty<byte>(), 0, 0); fixed (byte* tmpEndPtr = tmpEnd) { Span<byte> tmpEndSpan = tmpEnd.AsSpan(); tmpEndSpan.CopyTo(destination.Slice(writeOffset)); CryptographicOperations.ZeroMemory(tmpEndSpan); } return writeOffset + tmpEnd.Length; } } finally { CryptographicOperations.ZeroMemory(tmpKey); CryptographicOperations.ZeroMemory(tmpIv); CryptoPool.Return(rentedEncryptedData, encryptedData.Length); CryptoPool.Return(rentedDestination, destination.Length); } } } private static unsafe void Pbkdf1( IncrementalHash hasher, ReadOnlySpan<byte> password, ReadOnlySpan<byte> salt, int iterationCount, Span<byte> dk) { // The only two hashes that will call into this implementation are // MD5 (16 bytes) and SHA-1 (20 bytes). Span<byte> t = stackalloc byte[20]; // https://tools.ietf.org/html/rfc2898#section-5.1 // T_1 = Hash(P || S) hasher.AppendData(password); hasher.AppendData(salt); if (!hasher.TryGetHashAndReset(t, out int tLength)) { Debug.Fail("TryGetHashAndReset failed with pre-allocated input"); throw new CryptographicException(); } t = t.Slice(0, tLength); // T_i = H(T_(i-1)) for (int i = 1; i < iterationCount; i++) { hasher.AppendData(t); if (!hasher.TryGetHashAndReset(t, out tLength) || tLength != t.Length) { Debug.Fail("TryGetHashAndReset failed with pre-allocated input"); throw new CryptographicException(); } } // DK = T_c<0..dkLen-1> t.Slice(0, dk.Length).CopyTo(dk); CryptographicOperations.ZeroMemory(t); } internal static void WritePbeAlgorithmIdentifier( AsnWriter writer, bool isPkcs12, string encryptionAlgorithmOid, Span<byte> salt, int iterationCount, string hmacOid, Span<byte> iv) { writer.PushSequence(); if (isPkcs12) { writer.WriteObjectIdentifierForCrypto(encryptionAlgorithmOid); // pkcs-12PbeParams { writer.PushSequence(); writer.WriteOctetString(salt); writer.WriteInteger(iterationCount); writer.PopSequence(); } } else { writer.WriteObjectIdentifierForCrypto(Oids.PasswordBasedEncryptionScheme2); // PBES2-params { writer.PushSequence(); // keyDerivationFunc { writer.PushSequence(); writer.WriteObjectIdentifierForCrypto(Oids.Pbkdf2); // PBKDF2-params { writer.PushSequence(); writer.WriteOctetString(salt); writer.WriteInteger(iterationCount); // prf if (hmacOid != Oids.HmacWithSha1) { writer.PushSequence(); writer.WriteObjectIdentifierForCrypto(hmacOid); writer.WriteNull(); writer.PopSequence(); } writer.PopSequence(); } writer.PopSequence(); } // encryptionScheme { writer.PushSequence(); writer.WriteObjectIdentifierForCrypto(encryptionAlgorithmOid); writer.WriteOctetString(iv); writer.PopSequence(); } writer.PopSequence(); } } writer.PopSequence(); } internal static int NormalizeIterationCount(int iterationCount, int? iterationLimit = null) { if (iterationCount <= 0 || (iterationLimit.HasValue && iterationCount > iterationLimit.Value)) { throw new CryptographicException(SR.Argument_InvalidValue); } return iterationCount; } [SuppressMessage("Microsoft.Security", "CA5351", Justification = "RC2 used when specified by the input data")] private static RC2 CreateRC2() { if (!Helpers.IsRC2Supported) { throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(RC2))); } return RC2.Create(); } private static byte[] DeriveKey( byte[] password, ReadOnlySpan<byte> salt, int iterationCount, HashAlgorithmName prf, int outputLength) { #if NET return Rfc2898DeriveBytes.Pbkdf2(password, salt, iterationCount, prf, outputLength); #else using (Rfc2898DeriveBytes pbkdf2 = OpenPbkdf2(password, salt.ToArray(), iterationCount, prf)) { return pbkdf2.GetBytes(outputLength); } #endif } private static Rfc2898DeriveBytes OpenPbkdf2( byte[] password, byte[] salt, int iterationCount, HashAlgorithmName prf) { #if NET || NETSTANDARD2_1_OR_GREATER || NET472_OR_GREATER #pragma warning disable CA5379 #pragma warning disable SYSLIB0060 return new Rfc2898DeriveBytes( password, salt, iterationCount, prf); #pragma warning restore SYSLIB0060 #pragma warning restore CA5379 #else if (prf == HashAlgorithmName.SHA1) { #pragma warning disable CA5379 #pragma warning disable SYSLIB0060 return new Rfc2898DeriveBytes(password, salt, iterationCount); #pragma warning restore SYSLIB0060 #pragma warning restore CA5379 } try { return (Rfc2898DeriveBytes)Activator.CreateInstance( typeof(Rfc2898DeriveBytes), password, salt, iterationCount, prf); } catch (MissingMethodException e) { throw new CryptographicException( SR.Format(SR.Cryptography_UnknownHashAlgorithm, prf.Name), e); } #endif } } }