<commit_before>﻿using System;
using System.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using D2L.Security.OAuth2.Keys.Local.Data;
using D2L.Security.OAuth2.Utilities;

namespace D2L.Security.OAuth2.Keys.Local.Default {
	internal sealed class PrivateKeyProvider : IPrivateKeyProvider {

		private readonly IPublicKeyDataProvider m_publicKeyDataProvider;
		private readonly IDateTimeProvider m_dateTimeProvider;
		private readonly TimeSpan m_keyLifetime;
		private readonly TimeSpan m_keyRotationPeriod;
		private readonly bool m_savePrivateBits;

		private readonly SemaphoreSlim m_privateKeyLock = new SemaphoreSlim( initialCount: 1 );

		private PrivateKey m_privateKey;

		public PrivateKeyProvider(
			IPublicKeyDataProvider publicKeyDataProvider,
			IDateTimeProvider dateTimeProvider,
			TimeSpan keyLifetime,
			TimeSpan keyRotationPeriod,
			bool savePrivateBits = false // TODO: this is for LMS 10.5.1. Remove this option for 10.5.2+
		) {
			m_publicKeyDataProvider = publicKeyDataProvider;
			m_dateTimeProvider = dateTimeProvider;
			m_keyLifetime = keyLifetime;
			m_keyRotationPeriod = keyRotationPeriod;
			m_savePrivateBits = savePrivateBits;
		}

		private bool NeedFreshPrivateKey( PrivateKey key ) {
			return key == null || m_dateTimeProvider.UtcNow >= key.ValidTo - m_keyRotationPeriod;
		}
		
		async Task<D2LSecurityToken> IPrivateKeyProvider.GetSigningCredentialsAsync() {

			// Hold a local reference so that we know we are talking about the same key
			// after even if another thread changed m_privateKey (race condition when we
			// are using a key very close to the rotation time.)
			PrivateKey privateKey = m_privateKey;

			if( NeedFreshPrivateKey( privateKey ) ) {
				await m_privateKeyLock.WaitAsync().SafeAsync();
				try {
					privateKey = m_privateKey;

					if( NeedFreshPrivateKey( privateKey ) ) {
						DelayDisposeOfRsaSecurityKey( m_privateKey );
						m_privateKey = await CreatePrivateKeyAsync().SafeAsync();
						privateKey = m_privateKey;
					}

				} finally {
					m_privateKeyLock.Release();
				}
			}

			return new D2LSecurityToken(
				privateKey.Id,
				privateKey.ValidFrom,
				privateKey.ValidTo,
				privateKey.RsaSecurityKey
				);
		}

		private void DelayDisposeOfRsaSecurityKey( PrivateKey privateKey ) {

			// If no key has been assigned before, this will be null, so just return.
			if( privateKey == null ) {
				return;
			}

			RsaSecurityKey rsaSecurityKey = privateKey.RsaSecurityKey;

			// After a period of time, dispose of the key. The generous delay ensures any in-flight 
			// requests have time to finish.
			Task.Run(
				() => {
					Task.Delay( TimeSpan.FromSeconds( 5 ) );
					rsaSecurityKey.GetAsymmetricAlgorithm( string.Empty, true ).Dispose();
				}
			);
		}

		private async Task<PrivateKey> CreatePrivateKeyAsync() {
			DateTime now = m_dateTimeProvider.UtcNow;
			DateTime expiresAt = now + m_keyLifetime;
			using( RSACryptoServiceProvider csp = new RSACryptoServiceProvider( Constants.KEY_SIZE ) ) {
				csp.PersistKeyInCsp = false;

				// TODO: remove m_savePrivateBits hack after 10.5.1!
				RSAParameters publicKey = csp.ExportParameters( includePrivateParameters: m_savePrivateBits );

				RSAParameters privateKey = csp.ExportParameters( includePrivateParameters: true );

				Guid keyId = Guid.NewGuid();

				var jwk = new RsaJsonWebKey( keyId, expiresAt, publicKey );

				await m_publicKeyDataProvider.SaveAsync( jwk ).SafeAsync();

				RsaSecurityKey rsaSecurityKey = GetRsaSecurityKey( privateKey );

				return new PrivateKey( keyId, now, expiresAt, rsaSecurityKey );
			}
		}

		private RsaSecurityKey GetRsaSecurityKey( RSAParameters rsaParameters ) {

			var csp = new RSACryptoServiceProvider( Constants.KEY_SIZE ) {
				PersistKeyInCsp = false
			};

			csp.ImportParameters( rsaParameters );
			return new RsaSecurityKey( csp );
		}

		internal sealed class PrivateKey {
			private readonly Guid m_id;
			private readonly DateTime m_validFrom;
			private readonly DateTime m_validTo;
			private readonly RsaSecurityKey m_rsaSecurityKey;

			public PrivateKey(
				Guid id,
				DateTime validFrom,
				DateTime validTo,
				RsaSecurityKey rsaSecurityKey
			) {
				m_id = id;
				m_validFrom = validFrom;
				m_validTo = validTo;
				m_rsaSecurityKey = rsaSecurityKey;
			}

			public Guid Id {
				get { return m_id; }
			}

			public DateTime ValidFrom {
				get { return m_validFrom; }
			}

			public DateTime ValidTo {
				get { return m_validTo; }
			}

			public RsaSecurityKey RsaSecurityKey {
				get { return m_rsaSecurityKey; }
			}
		}
	}
}<commit_msg>US53682 Fixed code review suggestions - use ContinueWith on Task.Delay to ensure proper delay is observed before disposal; moved disposability to the PrivateKey<commit_after>﻿using System;
using System.IdentityModel.Tokens;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using D2L.Security.OAuth2.Keys.Local.Data;
using D2L.Security.OAuth2.Utilities;

namespace D2L.Security.OAuth2.Keys.Local.Default {
	internal sealed class PrivateKeyProvider : IPrivateKeyProvider {

		private readonly IPublicKeyDataProvider m_publicKeyDataProvider;
		private readonly IDateTimeProvider m_dateTimeProvider;
		private readonly TimeSpan m_keyLifetime;
		private readonly TimeSpan m_keyRotationPeriod;
		private readonly bool m_savePrivateBits;

		private readonly SemaphoreSlim m_privateKeyLock = new SemaphoreSlim( initialCount: 1 );

		private PrivateKey m_privateKey;

		public PrivateKeyProvider(
			IPublicKeyDataProvider publicKeyDataProvider,
			IDateTimeProvider dateTimeProvider,
			TimeSpan keyLifetime,
			TimeSpan keyRotationPeriod,
			bool savePrivateBits = false // TODO: this is for LMS 10.5.1. Remove this option for 10.5.2+
		) {
			m_publicKeyDataProvider = publicKeyDataProvider;
			m_dateTimeProvider = dateTimeProvider;
			m_keyLifetime = keyLifetime;
			m_keyRotationPeriod = keyRotationPeriod;
			m_savePrivateBits = savePrivateBits;
		}

		private bool NeedFreshPrivateKey( PrivateKey key ) {
			return key == null || m_dateTimeProvider.UtcNow >= key.ValidTo - m_keyRotationPeriod;
		}
		
		async Task<D2LSecurityToken> IPrivateKeyProvider.GetSigningCredentialsAsync() {

			// Hold a local reference so that we know we are talking about the same key
			// after even if another thread changed m_privateKey (race condition when we
			// are using a key very close to the rotation time.)
			PrivateKey privateKey = m_privateKey;

			if( NeedFreshPrivateKey( privateKey ) ) {
				await m_privateKeyLock.WaitAsync().SafeAsync();
				try {
					privateKey = m_privateKey;

					if( NeedFreshPrivateKey( privateKey ) ) {
						DelayDisposeOfRsaSecurityKey( m_privateKey );
						m_privateKey = await CreatePrivateKeyAsync().SafeAsync();
						privateKey = m_privateKey;
					}

				} finally {
					m_privateKeyLock.Release();
				}
			}

			return new D2LSecurityToken(
				privateKey.Id,
				privateKey.ValidFrom,
				privateKey.ValidTo,
				privateKey.RsaSecurityKey
				);
		}

		private void DelayDisposeOfRsaSecurityKey( PrivateKey privateKey ) {

			// If no key has been assigned before, this will be null, so just return.
			if( privateKey == null ) {
				return;
			}

			// After a period of time, dispose of the key. The generous delay ensures any in-flight 
			// requests have time to finish.
			Task.Run(
				() => {
					Task.Delay( TimeSpan.FromSeconds( 5 ) )
						.ContinueWith( t => privateKey.Dispose() );
				}
			);
		}

		private async Task<PrivateKey> CreatePrivateKeyAsync() {
			DateTime now = m_dateTimeProvider.UtcNow;
			DateTime expiresAt = now + m_keyLifetime;
			using( RSACryptoServiceProvider csp = new RSACryptoServiceProvider( Constants.KEY_SIZE ) ) {
				csp.PersistKeyInCsp = false;

				// TODO: remove m_savePrivateBits hack after 10.5.1!
				RSAParameters publicKey = csp.ExportParameters( includePrivateParameters: m_savePrivateBits );

				RSAParameters privateKey = csp.ExportParameters( includePrivateParameters: true );

				Guid keyId = Guid.NewGuid();

				var jwk = new RsaJsonWebKey( keyId, expiresAt, publicKey );

				await m_publicKeyDataProvider.SaveAsync( jwk ).SafeAsync();

				RsaSecurityKey rsaSecurityKey = GetRsaSecurityKey( privateKey );

				return new PrivateKey( keyId, now, expiresAt, rsaSecurityKey );
			}
		}

		private RsaSecurityKey GetRsaSecurityKey( RSAParameters rsaParameters ) {

			var csp = new RSACryptoServiceProvider( Constants.KEY_SIZE ) {
				PersistKeyInCsp = false
			};

			csp.ImportParameters( rsaParameters );
			return new RsaSecurityKey( csp );
		}

		internal sealed class PrivateKey : IDisposable {
			private readonly Guid m_id;
			private readonly DateTime m_validFrom;
			private readonly DateTime m_validTo;
			private readonly RsaSecurityKey m_rsaSecurityKey;

			public PrivateKey(
				Guid id,
				DateTime validFrom,
				DateTime validTo,
				RsaSecurityKey rsaSecurityKey
			) {
				m_id = id;
				m_validFrom = validFrom;
				m_validTo = validTo;
				m_rsaSecurityKey = rsaSecurityKey;
			}

			public Guid Id {
				get { return m_id; }
			}

			public DateTime ValidFrom {
				get { return m_validFrom; }
			}

			public DateTime ValidTo {
				get { return m_validTo; }
			}

			public RsaSecurityKey RsaSecurityKey {
				get { return m_rsaSecurityKey; }
			}

			public void Dispose() {
				RsaSecurityKey.GetAsymmetricAlgorithm( string.Empty, true ).Dispose();
			}
		}
	}
}
{'fraction_non_alphanumeric': 0.0731244064577398, 'fraction_numerical': 0.0051704125778199855, 'mean_word_length': 3.777217741935484, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 12, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4890783', 'n_tokens_mistral': 3470, 'n_tokens_neox': 2830, 'n_words': 745}