From 0ecb73d3ff6e14ce2060bede2a0d0ff419e1231c Mon Sep 17 00:00:00 2001 From: Benjamin Titeux Date: Sun, 21 Aug 2022 14:08:55 +0200 Subject: [PATCH 1/7] added jwt encoder and jwt decoder signature verification --- src/Directory.Build.props | 1 + src/dev/impl/DevToys/DevToys.csproj | 27 + src/dev/impl/DevToys/Helpers/Base64Helper.cs | 76 +++ src/dev/impl/DevToys/LanguageManager.cs | 205 +++++- src/dev/impl/DevToys/Models/JwtAlgorithm.cs | 18 + .../DevToys/Models/JwtAlgorithmDisplayPair.cs | 32 + src/dev/impl/DevToys/Models/ValidationBase.cs | 11 + .../Strings/en-US/JwtDecoderEncoder.resw | 129 +++- .../JwtDecoderControlViewModel.cs | 241 +++++++ .../JwtDecoderEncoderSettings.cs | 161 +++++ .../JwtDecoderEncoderToolProvider.cs | 4 +- .../JwtDecoderEncoderToolViewModel.cs | 126 +--- .../JwtDecoderEncoderViewModel.cs | 597 ++++++++++++++++++ .../JwtEncoderControlViewModel.cs | 310 +++++++++ .../JwtDecoderEncoder/JwtJobAddedMessage.cs | 11 + .../JwtDecoderEncoder/JwtPayloadConverter.cs | 99 +++ .../JwtDecoderEncoder/JwtToolJobItem.cs | 6 + .../JwtToolSwitchedMessage.cs | 8 + .../JwtDecoderEncoder/JwtDecoderControl.xaml | 199 ++++++ .../JwtDecoderControl.xaml.cs | 34 + .../JwtDecoderEncoderToolPage.xaml | 63 +- .../JwtDecoderEncoderToolPage.xaml.cs | 3 +- .../JwtDecoderEncoder/JwtEncoderControl.xaml | 295 +++++++++ .../JwtEncoderControl.xaml.cs | 34 + 24 files changed, 2526 insertions(+), 164 deletions(-) create mode 100644 src/dev/impl/DevToys/Helpers/Base64Helper.cs create mode 100644 src/dev/impl/DevToys/Models/JwtAlgorithm.cs create mode 100644 src/dev/impl/DevToys/Models/JwtAlgorithmDisplayPair.cs create mode 100644 src/dev/impl/DevToys/Models/ValidationBase.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControlViewModel.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderSettings.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderViewModel.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControlViewModel.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtJobAddedMessage.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtPayloadConverter.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolJobItem.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolSwitchedMessage.cs create mode 100644 src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControl.xaml create mode 100644 src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControl.xaml.cs create mode 100644 src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControl.xaml create mode 100644 src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControl.xaml.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 16c0495139..125d3ec8fe 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -37,5 +37,6 @@ 11.2.1 6.13.1 1.1.0 + 1.9.0 \ No newline at end of file diff --git a/src/dev/impl/DevToys/DevToys.csproj b/src/dev/impl/DevToys/DevToys.csproj index 0c93d4b5de..6ddc20bdc4 100644 --- a/src/dev/impl/DevToys/DevToys.csproj +++ b/src/dev/impl/DevToys/DevToys.csproj @@ -30,6 +30,7 @@ + @@ -74,12 +75,15 @@ + + + FileSelector.xaml @@ -114,6 +118,14 @@ + + + + + + + + @@ -173,6 +185,12 @@ GZipEncoderDecoderToolPage.xaml + + JwtDecoderControl.xaml + + + JwtEncoderControl.xaml + XmlFormatterToolPage.xaml @@ -553,6 +571,14 @@ Designer + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + MSBuild:Compile Designer @@ -742,6 +768,7 @@ + diff --git a/src/dev/impl/DevToys/Helpers/Base64Helper.cs b/src/dev/impl/DevToys/Helpers/Base64Helper.cs new file mode 100644 index 0000000000..8d0b4fa262 --- /dev/null +++ b/src/dev/impl/DevToys/Helpers/Base64Helper.cs @@ -0,0 +1,76 @@ +#nullable enable + +using System; +using System.Text; +using System.Text.RegularExpressions; + +namespace DevToys.Helpers +{ + internal static class Base64Helper + { + internal static bool IsBase64DataStrict(string? data) + { + if (string.IsNullOrWhiteSpace(data)) + { + return false; + } + + data = data!.Trim(); + + if (data.Length % 4 != 0) + { + return false; + } + + if (new Regex(@"[^A-Z0-9+/=]", RegexOptions.IgnoreCase).IsMatch(data)) + { + return false; + } + + int equalIndex = data.IndexOf('='); + int length = data.Length; + + if (!(equalIndex == -1 || equalIndex == length - 1 || (equalIndex == length - 2 && data[length - 1] == '='))) + { + return false; + } + + string? decoded; + + try + { + byte[]? decodedData = Convert.FromBase64String(data); + decoded = Encoding.UTF8.GetString(decodedData); + } + catch (Exception) + { + return false; + } + + //check for special chars that you know should not be there + char current; + for (int i = 0; i < decoded.Length; i++) + { + current = decoded[i]; + if (current == 65533) + { + return false; + } + +#pragma warning disable IDE0078 // Use pattern matching + if (!(current == 0x9 + || current == 0xA + || current == 0xD + || (current >= 0x20 && current <= 0xD7FF) + || (current >= 0xE000 && current <= 0xFFFD) + || (current >= 0x10000 && current <= 0x10FFFF))) +#pragma warning restore IDE0078 // Use pattern matching + { + return false; + } + } + + return true; + } + } +} diff --git a/src/dev/impl/DevToys/LanguageManager.cs b/src/dev/impl/DevToys/LanguageManager.cs index 31ac99e863..b816a8a5ab 100644 --- a/src/dev/impl/DevToys/LanguageManager.cs +++ b/src/dev/impl/DevToys/LanguageManager.cs @@ -1577,14 +1577,14 @@ public class JwtDecoderEncoderStrings : ObservableObject public string AccessibleName => _resources.GetString("AccessibleName"); /// - /// Gets the resource MenuDisplayName. + /// Gets the resource DisplayName. /// - public string MenuDisplayName => _resources.GetString("MenuDisplayName"); + public string DisplayName => _resources.GetString("DisplayName"); /// - /// Gets the resource HeaderLabel. + /// Gets the resource JwtHeaderLabel. /// - public string HeaderLabel => _resources.GetString("HeaderLabel"); + public string JwtHeaderLabel => _resources.GetString("JwtHeaderLabel"); /// /// Gets the resource JwtTokenLabel. @@ -1592,9 +1592,9 @@ public class JwtDecoderEncoderStrings : ObservableObject public string JwtTokenLabel => _resources.GetString("JwtTokenLabel"); /// - /// Gets the resource PayloadLabel. + /// Gets the resource JwtPayloadLabel. /// - public string PayloadLabel => _resources.GetString("PayloadLabel"); + public string JwtPayloadLabel => _resources.GetString("JwtPayloadLabel"); /// /// Gets the resource Description. @@ -1602,14 +1602,199 @@ public class JwtDecoderEncoderStrings : ObservableObject public string Description => _resources.GetString("Description"); /// - /// Gets the resource SearchDisplayName. + /// Gets the resource SearchKeywords. /// - public string SearchDisplayName => _resources.GetString("SearchDisplayName"); + public string SearchKeywords => _resources.GetString("SearchKeywords"); /// - /// Gets the resource SearchKeywords. + /// Gets the resource Algorithm. /// - public string SearchKeywords => _resources.GetString("SearchKeywords"); + public string Algorithm => _resources.GetString("Algorithm"); + + /// + /// Gets the resource DecodeSwitchModeLabel. + /// + public string DecodeSwitchModeLabel => _resources.GetString("DecodeSwitchModeLabel"); + + /// + /// Gets the resource DecodeValidateTokenLabel. + /// + public string DecodeValidateTokenLabel => _resources.GetString("DecodeValidateTokenLabel"); + + /// + /// Gets the resource DecodeValidateTokenNoLabel. + /// + public string DecodeValidateTokenNoLabel => _resources.GetString("DecodeValidateTokenNoLabel"); + + /// + /// Gets the resource DecodeValidateTokenYesLabel. + /// + public string DecodeValidateTokenYesLabel => _resources.GetString("DecodeValidateTokenYesLabel"); + + /// + /// Gets the resource EncodeExpirationTitle. + /// + public string EncodeExpirationTitle => _resources.GetString("EncodeExpirationTitle"); + + /// + /// Gets the resource EncodeExpirationMonthLabel. + /// + public string EncodeExpirationMonthLabel => _resources.GetString("EncodeExpirationMonthLabel"); + + /// + /// Gets the resource EncodeExpirationYearLabel. + /// + public string EncodeExpirationYearLabel => _resources.GetString("EncodeExpirationYearLabel"); + + /// + /// Gets the resource EncodeHashingTitle. + /// + public string EncodeHashingTitle => _resources.GetString("EncodeHashingTitle"); + + /// + /// Gets the resource EncodeSettingsTitle. + /// + public string EncodeSettingsTitle => _resources.GetString("EncodeSettingsTitle"); + + /// + /// Gets the resource EncodeSwitchModeLabel. + /// + public string EncodeSwitchModeLabel => _resources.GetString("EncodeSwitchModeLabel"); + + /// + /// Gets the resource SettingsSwitchModeLabel. + /// + public string SettingsSwitchModeLabel => _resources.GetString("SettingsSwitchModeLabel"); + + /// + /// Gets the resource SettingsTitle. + /// + public string SettingsTitle => _resources.GetString("SettingsTitle"); + + /// + /// Gets the resource EncodeDefaultTimeTitle. + /// + public string EncodeDefaultTimeTitle => _resources.GetString("EncodeDefaultTimeTitle"); + + /// + /// Gets the resource EncodeExpirationDaysLabel. + /// + public string EncodeExpirationDaysLabel => _resources.GetString("EncodeExpirationDaysLabel"); + + /// + /// Gets the resource EncodeExpirationHoursLabel. + /// + public string EncodeExpirationHoursLabel => _resources.GetString("EncodeExpirationHoursLabel"); + + /// + /// Gets the resource EncodeExpirationMinutesLabel. + /// + public string EncodeExpirationMinutesLabel => _resources.GetString("EncodeExpirationMinutesLabel"); + + /// + /// Gets the resource PrivateKeyLabel. + /// + public string PrivateKeyLabel => _resources.GetString("PrivateKeyLabel"); + + /// + /// Gets the resource SignatureLabel. + /// + public string SignatureLabel => _resources.GetString("SignatureLabel"); + + /// + /// Gets the resource JwtIsValidMessage. + /// + public string JwtIsValidMessage => _resources.GetString("JwtIsValidMessage"); + + /// + /// Gets the resource InvalidPublicKeyError. + /// + public string InvalidPublicKeyError => _resources.GetString("InvalidPublicKeyError"); + + /// + /// Gets the resource JwtInValidMessage. + /// + public string JwtInValidMessage => _resources.GetString("JwtInValidMessage"); + + /// + /// Gets the resource DecodeValidateActorLabel. + /// + public string DecodeValidateActorLabel => _resources.GetString("DecodeValidateActorLabel"); + + /// + /// Gets the resource DecodeValidateAudienceLabel. + /// + public string DecodeValidateAudienceLabel => _resources.GetString("DecodeValidateAudienceLabel"); + + /// + /// Gets the resource DecodeValidateIssuerLabel. + /// + public string DecodeValidateIssuerLabel => _resources.GetString("DecodeValidateIssuerLabel"); + + /// + /// Gets the resource DecodeValidateLifetimeLabel. + /// + public string DecodeValidateLifetimeLabel => _resources.GetString("DecodeValidateLifetimeLabel"); + + /// + /// Gets the resource ValidAudiencesLabel. + /// + public string ValidAudiencesLabel => _resources.GetString("ValidAudiencesLabel"); + + /// + /// Gets the resource ValidIssuersLabel. + /// + public string ValidIssuersLabel => _resources.GetString("ValidIssuersLabel"); + + /// + /// Gets the resource DecodeValidationSettingsDescription. + /// + public string DecodeValidationSettingsDescription => _resources.GetString("DecodeValidationSettingsDescription"); + + /// + /// Gets the resource DecodeValidationSettingsTitle. + /// + public string DecodeValidationSettingsTitle => _resources.GetString("DecodeValidationSettingsTitle"); + + /// + /// Gets the resource ValidAudiencesError. + /// + public string ValidAudiencesError => _resources.GetString("ValidAudiencesError"); + + /// + /// Gets the resource ValidIssuersError. + /// + public string ValidIssuersError => _resources.GetString("ValidIssuersError"); + + /// + /// Gets the resource PublicKeyIsPrivateKeyError. + /// + public string PublicKeyIsPrivateKeyError => _resources.GetString("PublicKeyIsPrivateKeyError"); + + /// + /// Gets the resource EncodeAudienceLabel. + /// + public string EncodeAudienceLabel => _resources.GetString("EncodeAudienceLabel"); + + /// + /// Gets the resource EncodeIssuerLabel. + /// + public string EncodeIssuerLabel => _resources.GetString("EncodeIssuerLabel"); + + /// + /// Gets the resource EncodeSettingsDescription. + /// + public string EncodeSettingsDescription => _resources.GetString("EncodeSettingsDescription"); + + /// + /// Gets the resource InvalidPrivateKeyError. + /// + public string InvalidPrivateKeyError => _resources.GetString("InvalidPrivateKeyError"); + + /// + /// Gets the resource PublicKeyLabel. + /// + public string PublicKeyLabel => _resources.GetString("PublicKeyLabel"); } public class LoremIpsumGeneratorStrings : ObservableObject diff --git a/src/dev/impl/DevToys/Models/JwtAlgorithm.cs b/src/dev/impl/DevToys/Models/JwtAlgorithm.cs new file mode 100644 index 0000000000..49ad9edf17 --- /dev/null +++ b/src/dev/impl/DevToys/Models/JwtAlgorithm.cs @@ -0,0 +1,18 @@ +namespace DevToys.Models +{ + public enum JwtAlgorithm + { + HS256, + HS384, + HS512, + RS256, + RS384, + RS512, + ES256, + ES384, + ES512, + PS256, + PS384, + PS512 + } +} diff --git a/src/dev/impl/DevToys/Models/JwtAlgorithmDisplayPair.cs b/src/dev/impl/DevToys/Models/JwtAlgorithmDisplayPair.cs new file mode 100644 index 0000000000..7bae9f7df6 --- /dev/null +++ b/src/dev/impl/DevToys/Models/JwtAlgorithmDisplayPair.cs @@ -0,0 +1,32 @@ +using System; + +namespace DevToys.Models +{ + public sealed class JwtAlgorithmDisplayPair : IEquatable + { + public static readonly JwtAlgorithmDisplayPair HS256 = new(nameof(HS256), JwtAlgorithm.HS256); + public static readonly JwtAlgorithmDisplayPair HS384 = new(nameof(HS384), JwtAlgorithm.HS384); + public static readonly JwtAlgorithmDisplayPair HS512 = new(nameof(HS512), JwtAlgorithm.HS512); + public static readonly JwtAlgorithmDisplayPair RS256 = new(nameof(RS256), JwtAlgorithm.RS256); + public static readonly JwtAlgorithmDisplayPair RS384 = new(nameof(RS384), JwtAlgorithm.RS384); + public static readonly JwtAlgorithmDisplayPair RS512 = new(nameof(RS512), JwtAlgorithm.RS512); + public static readonly JwtAlgorithmDisplayPair ES256 = new(nameof(ES256), JwtAlgorithm.ES256); + public static readonly JwtAlgorithmDisplayPair ES384 = new(nameof(ES384), JwtAlgorithm.ES384); + public static readonly JwtAlgorithmDisplayPair ES512 = new(nameof(ES512), JwtAlgorithm.ES512); + public static readonly JwtAlgorithmDisplayPair PS256 = new(nameof(PS256), JwtAlgorithm.PS256); + public static readonly JwtAlgorithmDisplayPair PS384 = new(nameof(PS384), JwtAlgorithm.PS384); + public static readonly JwtAlgorithmDisplayPair PS512 = new(nameof(PS512), JwtAlgorithm.PS512); + + public string DisplayName { get; } + + public JwtAlgorithm Value { get; } + + private JwtAlgorithmDisplayPair(string displayName, JwtAlgorithm value) + { + DisplayName = displayName; + Value = value; + } + + public bool Equals(JwtAlgorithmDisplayPair other) => other.Value == Value; + } +} diff --git a/src/dev/impl/DevToys/Models/ValidationBase.cs b/src/dev/impl/DevToys/Models/ValidationBase.cs new file mode 100644 index 0000000000..2546222d14 --- /dev/null +++ b/src/dev/impl/DevToys/Models/ValidationBase.cs @@ -0,0 +1,11 @@ +#nullable enable + +namespace DevToys.Models +{ + public record ValidationBase + { + internal bool IsValid { get; set; } + + internal string? ErrorMessage { get; set; } + } +} diff --git a/src/dev/impl/DevToys/Strings/en-US/JwtDecoderEncoder.resw b/src/dev/impl/DevToys/Strings/en-US/JwtDecoderEncoder.resw index 678c56cb68..5cd60dc18f 100644 --- a/src/dev/impl/DevToys/Strings/en-US/JwtDecoderEncoder.resw +++ b/src/dev/impl/DevToys/Strings/en-US/JwtDecoderEncoder.resw @@ -118,27 +118,138 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - JWT Decoder tool + JWT Encoder / Decoder tool - - JWT Decoder + + JWT Encoder / Decoder - + Header - Jwt Token + Token - + Payload Decode a JWT header, payload and signature - - JWT Decoder - + + Algorithm + + + Decode + + + Validate Token + + + No + + + Yes + + + Token has expirations + + + Expire in month(s) + + + Expire in year(s) + + + Token hashing algorithm + + + Settings + + + Encode + + + Encode / Decode + + + Configuration + + + Token has default time + + + Expire in day(s) + + + Expire in hour(s) + + + Expire in minute(s) + + + Private Key + + + Signature + + + Signature Verified + + + Invalid Public Key + + + Invalid Signature + + + Validate actor + + + Validate audience + + + Validate issuer + + + Validate lifetime + + + Valid audiences + + + Valid issuers + + + Select which token paramters to validate + + + Token validation settings + + + Valid audiences are empty + + + Valid issuers are empty + + + The public key provided is a private key + + + Token has audience + + + Token has issuer + + + Select token parameters + + + Invalid Private Key + + + Public Key + \ No newline at end of file diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControlViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControlViewModel.cs new file mode 100644 index 0000000000..40880ea157 --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControlViewModel.cs @@ -0,0 +1,241 @@ +#nullable enable + +using System; +using System.Composition; +using System.IdentityModel.Tokens.Jwt; +using System.Threading.Tasks; +using DevToys.Api.Core; +using DevToys.Api.Core.Settings; +using DevToys.Api.Tools; +using DevToys.Core.Threading; +using DevToys.Helpers.JsonYaml; +using DevToys.Models; +using DevToys.Shared.Core.Threading; +using DevToys.Views.Tools.EncodersDecoders.JwtDecoderEncoder; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Toolkit.Mvvm.Messaging; + +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + [Export(typeof(JwtDecoderControlViewModel))] + public sealed class JwtDecoderControlViewModel : JwtDecoderEncoderViewModel, IToolViewModel, IRecipient + { + private bool _validateSignature; + private bool _validateIssuer; + private bool _validateActor; + private bool _validateAudience; + private bool _validateLifetime; + + internal bool ValidateSignature + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateSignature); + set + { + if (_validateSignature != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateSignature, value); + SetProperty(ref _validateSignature, value); + ShowValidation = value; + QueueNewTokenJob(); + } + } + } + + internal bool ValidateIssuer + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateIssuer); + set + { + if (_validateIssuer != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateIssuer, value); + SetProperty(ref _validateIssuer, value); + QueueNewTokenJob(); + } + } + } + + internal bool ValidateActor + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateActor); + set + { + if (_validateActor != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateActor, value); + SetProperty(ref _validateActor, value); + QueueNewTokenJob(); + } + } + } + + internal bool ValidateAudience + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateAudience); + set + { + if (_validateAudience != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateAudience, value); + SetProperty(ref _validateAudience, value); + QueueNewTokenJob(); + } + } + } + + internal bool ValidateLifetime + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateLifetime); + set + { + if (_validateLifetime != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateLifetime, value); + SetProperty(ref _validateLifetime, value); + QueueNewTokenJob(); + } + } + } + + public Type View { get; } = typeof(JwtDecoderControl); + + [ImportingConstructor] + public JwtDecoderControlViewModel( + ISettingsProvider settingsProvider, + IMarketingService marketingService) + : base(settingsProvider, marketingService) + { + IsActive = true; + ShowValidation = ValidateSignature; + } + + public void Receive(JwtJobAddedMessage message) + { + TreatQueueAsync().Forget(); + } + + private async Task TreatQueueAsync() + { + if (WorkInProgress) + { + return; + } + + WorkInProgress = true; + + await TaskScheduler.Default; + + while (JobQueue.TryDequeue(out _)) + { + DecodeToken(out string? header, out string? payload, out object? tokenAlgorithm); + ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => + { + Header = header; + Payload = payload; + + if (tokenAlgorithm is not null) + { + RequireSignature = true; + if ((JwtAlgorithm)tokenAlgorithm is + not Models.JwtAlgorithm.HS256 and + not Models.JwtAlgorithm.HS384 and + not Models.JwtAlgorithm.HS512) + { + RequireSignature = false; + } + } + + if (ValidateSignature && !string.IsNullOrWhiteSpace(Token)) + { + DisplayValidationInfoBar(); + } + + if (!ToolSuccessfullyWorked) + { + ToolSuccessfullyWorked = true; + MarketingService.NotifyToolSuccessfullyWorked(); + } + }).ForgetSafely(); + } + + WorkInProgress = false; + } + + private void DecodeToken(out string header, out string payload, out object? tokenAlgorithm) + { + if (string.IsNullOrWhiteSpace(Token)) + { + header = string.Empty; + payload = string.Empty; + tokenAlgorithm = null; + return; + } + + try + { + IdentityModelEventSource.ShowPII = true; + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtSecurityToken = handler.ReadJwtToken(Token); + header = JsonHelper.Format(jwtSecurityToken.Header.SerializeToJson(), Indentation.TwoSpaces, false); + payload = JsonHelper.Format(jwtSecurityToken.Payload.SerializeToJson(), Indentation.TwoSpaces, false); + Enum.TryParse(typeof(JwtAlgorithm), jwtSecurityToken.SignatureAlgorithm, out tokenAlgorithm); + + if (ValidateSignature) + { + SigningCredentials? signingCredentials = GetValidationCredentials((JwtAlgorithm)tokenAlgorithm); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = signingCredentials.Key, + TryAllIssuerSigningKeys = true, + ValidateActor = ValidateActor, + ValidateLifetime = ValidateLifetime, + ValidateIssuer = ValidateIssuer, + ValidateAudience = ValidateAudience + }; + + if (ValidateIssuer) + { + if (string.IsNullOrWhiteSpace(ValidIssuers)) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.ValidIssuersError; + return; + } + validationParameters.ValidIssuers = ValidIssuers!.Split(','); + } + + if (ValidateAudience) + { + if (string.IsNullOrWhiteSpace(ValidAudiences)) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.ValidAudiencesError; + return; + } + validationParameters.ValidAudiences = ValidAudiences!.Split(','); + } + + try + { + handler.ValidateToken(Token, validationParameters, out _); + JwtValidation.IsValid = true; + } + catch (Exception exception) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = exception.Message; + } + } + } + catch (Exception ex) + { + header = ex.Message; + payload = ex.Message; + tokenAlgorithm = null; + } + } + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderSettings.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderSettings.cs new file mode 100644 index 0000000000..a9790ad832 --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderSettings.cs @@ -0,0 +1,161 @@ +using DevToys.Api.Core.Settings; +using DevToys.Models; + +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + internal static class JwtDecoderEncoderSettings + { + /// + /// Define if we need to show the validation parameters / inputs + /// + public static readonly SettingDefinition ShowValidation + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ShowValidation)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if we need to validate the token signature + /// + public static readonly SettingDefinition ValidateSignature + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ValidateSignature)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if we need to validate the token issuer + /// + public static readonly SettingDefinition ValidateIssuer + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ValidateIssuer)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if we need to validate the token actor + /// + public static readonly SettingDefinition ValidateActor + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ValidateActor)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if we need to validate the token audience + /// + public static readonly SettingDefinition ValidateAudience + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ValidateAudience)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if we need to validate the token lifetime + /// + public static readonly SettingDefinition ValidateLifetime + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ValidateLifetime)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if we need to validate the token lifetime + /// + public static readonly SettingDefinition JWtToolMode + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(JWtToolMode)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if the token has an expiration + /// + public static readonly SettingDefinition HasExpiration + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(HasExpiration)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if the token has a default time + /// + public static readonly SettingDefinition HasDefaultTime + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(HasDefaultTime)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if the token has a default time + /// + public static readonly SettingDefinition HasAudience + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(HasAudience)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if the token has a default time + /// + public static readonly SettingDefinition HasIssuer + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(HasIssuer)}", + isRoaming: true, + defaultValue: false); + + /// + /// Define if the token expiration year + /// + public static readonly SettingDefinition ExpireYear + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ExpireYear)}", + isRoaming: true, + defaultValue: 0); + + /// + /// Define if the token expiration month + /// + public static readonly SettingDefinition ExpireMonth + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ExpireMonth)}", + isRoaming: true, + defaultValue: 0); + + /// + /// Define if the token expiration day + /// + public static readonly SettingDefinition ExpireDay + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ExpireDay)}", + isRoaming: true, + defaultValue: 0); + + /// + /// Define if the token expiration hours + /// + public static readonly SettingDefinition ExpireHour + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ExpireHour)}", + isRoaming: true, + defaultValue: 0); + + /// + /// Define if the token expiration minutes + /// + public static readonly SettingDefinition ExpireMinute + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ExpireMinute)}", + isRoaming: true, + defaultValue: 0); + + /// + /// Define if the token expiration minutes + /// + public static readonly SettingDefinition JwtAlgorithm + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(JwtAlgorithm)}", + isRoaming: true, + defaultValue: Models.JwtAlgorithm.HS256); + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolProvider.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolProvider.cs index 24c7bfbee5..00057a0772 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolProvider.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolProvider.cs @@ -19,9 +19,9 @@ internal sealed class JwtDecoderEncoderToolProvider : IToolProvider { private readonly IMefProvider _mefProvider; - public string MenuDisplayName => LanguageManager.Instance.JwtDecoderEncoder.MenuDisplayName; + public string MenuDisplayName => LanguageManager.Instance.JwtDecoderEncoder.DisplayName; - public string? SearchDisplayName => LanguageManager.Instance.JwtDecoderEncoder.SearchDisplayName; + public string? SearchDisplayName => LanguageManager.Instance.JwtDecoderEncoder.DisplayName; public string? Description => LanguageManager.Instance.JwtDecoderEncoder.Description; diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs index 63eb8980b2..ef88a79ef4 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs @@ -1,131 +1,53 @@ #nullable enable using System; -using System.Collections.Generic; using System.Composition; -using System.IdentityModel.Tokens.Jwt; -using System.Threading.Tasks; -using DevToys.Api.Core; +using DevToys.Api.Core.Settings; using DevToys.Api.Tools; -using DevToys.Core.Threading; -using DevToys.Shared.Core.Threading; -using DevToys.Models; +using DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder; using DevToys.Views.Tools.JwtDecoderEncoder; using Microsoft.Toolkit.Mvvm.ComponentModel; -using DevToys.Helpers.JsonYaml; namespace DevToys.ViewModels.Tools.JwtDecoderEncoder { [Export(typeof(JwtDecoderEncoderToolViewModel))] public sealed class JwtDecoderEncoderToolViewModel : ObservableRecipient, IToolViewModel { - private readonly IMarketingService _marketingService; - private readonly Queue _decodingQueue = new(); + private readonly ISettingsProvider _settingsProvider; - private bool _toolSuccessfullyWorked; - private bool _decodingInProgress; - private string? _jwtToken; - private string? _jwtHeader; - private string? _jwtPayload; + private bool _jwtToolMode; - public Type View { get; } = typeof(JwtDecoderEncoderToolPage); - - internal JwtDecoderEncoderStrings Strings => LanguageManager.Instance.JwtDecoderEncoder; - - /// - /// Gets or sets the jwt token. - /// - internal string? JwtToken + internal bool JwtToolMode { - get => _jwtToken; + get => _settingsProvider.GetSetting(JwtDecoderEncoderSettings.JWtToolMode); set { - SetProperty(ref _jwtToken, value?.Trim()); - QueueConversion(); + if (_jwtToolMode != value) + { + _settingsProvider.SetSetting(JwtDecoderEncoderSettings.JWtToolMode, value); + SetProperty(ref _jwtToolMode, value); + } } } - /// - /// Gets or sets the jwt header. - /// - internal string? JwtHeader - { - get => _jwtHeader; - set => SetProperty(ref _jwtHeader, value); - } - - /// - /// Gets or sets the jwt payload. - /// - internal string? JwtPayload - { - get => _jwtPayload; - set => SetProperty(ref _jwtPayload, value); - } - - [ImportingConstructor] - public JwtDecoderEncoderToolViewModel(IMarketingService marketingService) - { - _marketingService = marketingService; - } - - private void QueueConversion() - { - _decodingQueue.Enqueue(JwtToken ?? string.Empty); - TreatQueueAsync().Forget(); - } - - private async Task TreatQueueAsync() - { - if (_decodingInProgress) - { - return; - } - - _decodingInProgress = true; - - await TaskScheduler.Default; + internal JwtDecoderEncoderStrings Strings => LanguageManager.Instance.JwtDecoderEncoder; - while (_decodingQueue.TryDequeue(out string? text)) - { - JwtDecode(text, out string? header, out string? payload); - ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => - { - JwtHeader = header; - JwtPayload = payload; + public Type View { get; } = typeof(JwtDecoderEncoderToolPage); - if (!_toolSuccessfullyWorked) - { - _toolSuccessfullyWorked = true; - _marketingService.NotifyToolSuccessfullyWorked(); - } - }).ForgetSafely(); - } + public JwtDecoderControlViewModel DecoderViewModel { get; private set; } - _decodingInProgress = false; - } + public JwtEncoderControlViewModel EncoderViewModel { get; private set; } - private void JwtDecode(string input, out string header, out string payload) + [ImportingConstructor] + public JwtDecoderEncoderToolViewModel( + JwtDecoderControlViewModel decoderControlViewModel, + JwtEncoderControlViewModel encoderControlViewModel, + ISettingsProvider settingsProvider) { - if (string.IsNullOrWhiteSpace(input)) - { - header = string.Empty; - payload = string.Empty; - return; - } - - try - { - var handler = new JwtSecurityTokenHandler(); - JwtSecurityToken jwtSecurityToken = handler.ReadJwtToken(input); - header = JsonHelper.Format(jwtSecurityToken.Header.SerializeToJson(), Indentation.TwoSpaces, sortProperties: false); - payload = JsonHelper.Format(jwtSecurityToken.Payload.SerializeToJson(), Indentation.TwoSpaces, sortProperties: false); - } - catch (Exception ex) - { - header = ex.Message; - payload = ex.Message; - } + DecoderViewModel = decoderControlViewModel; + EncoderViewModel = encoderControlViewModel; + _settingsProvider = settingsProvider; + IsActive = true; } } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderViewModel.cs new file mode 100644 index 0000000000..cb390633b2 --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderViewModel.cs @@ -0,0 +1,597 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using DevToys.Api.Core; +using DevToys.Api.Core.Settings; +using DevToys.Helpers; +using DevToys.Helpers.JsonYaml; +using DevToys.Models; +using DevToys.UI.Controls; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.UI.Xaml.Controls; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Windows.UI.Xaml; +using YamlDotNet.Core.Tokens; +using PemReader = Org.BouncyCastle.OpenSsl.PemReader; + +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + public class JwtDecoderEncoderViewModel : ObservableRecipient + { + private string? _token; + private string? _header; + private string? _payload; + private string? _signature; + private string? _publicKey; + private string? _privateKey; + private string? _validIssuers; + private string? _validAudiences; + private JwtAlgorithmDisplayPair _algorithmSelected; + private bool _showValidation; + private bool _requireSignature; + private InfoBarData? _validationResult; + private const string PublicKeyStart = "-----BEGIN PUBLIC KEY-----"; + private const string PublicKeyEnd = "-----END PUBLIC KEY-----"; + private const string PrivateKeyStart = "-----BEGIN PRIVATE KEY-----"; + private const string PrivateKeyEnd = "-----END PRIVATE KEY-----"; + + protected bool WorkInProgress; + protected bool ToolSuccessfullyWorked; + protected ValidationBase JwtValidation = new(); + protected JwtToolJobItem CurrentJobItem = new(); + protected readonly Queue JobQueue = new(); + protected readonly ISettingsProvider SettingsProvider; + protected readonly IMarketingService MarketingService; + + internal JwtDecoderEncoderStrings LocalizedStrings => LanguageManager.Instance.JwtDecoderEncoder; + + internal RoutedEventHandler InputFocusChanged { get; } + + internal virtual string? Token + { + get => _token; + set + { + if (_token != value) + { + SetProperty(ref _token, value?.Trim()); + QueueNewTokenJob(); + } + } + } + + internal string? Header + { + get => _header; + set + { + if (_header != value) + { + SetProperty(ref _header, value); + QueueNewTokenJob(); + } + } + } + + internal string? Payload + { + get => _payload; + set + { + if (_payload != value) + { + SetProperty(ref _payload, value); + QueueNewTokenJob(); + } + } + } + + internal string? ValidIssuers + { + get => _validIssuers; + set + { + if (_validIssuers != value) + { + SetProperty(ref _validIssuers, value); + QueueNewTokenJob(); + } + } + } + + internal string? ValidAudiences + { + get => _validAudiences; + set + { + if (_validAudiences != value) + { + SetProperty(ref _validAudiences, value); + QueueNewTokenJob(); + } + } + } + + internal string? PublicKey + { + get => _publicKey; + set + { + if (_publicKey != value) + { + SetProperty(ref _publicKey, value); + QueueNewTokenJob(); + } + } + } + + internal string? PrivateKey + { + get => _privateKey; + set + { + if (_privateKey != value) + { + SetProperty(ref _privateKey, value); + QueueNewTokenJob(); + } + } + } + + internal string? Signature + { + get => _signature; + set + { + if (_signature != value) + { + SetProperty(ref _signature, value); + QueueNewTokenJob(); + } + } + } + + internal bool ShowValidation + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ShowValidation); + set + { + if (_showValidation != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ShowValidation, value); + SetProperty(ref _showValidation, value); + QueueNewTokenJob(); + } + } + } + + internal bool RequireSignature + { + get => _requireSignature; + set + { + if (_requireSignature != value) + { + SetProperty(ref _requireSignature, value); + OnPropertyChanged(); + } + } + } + + internal InfoBarData? ValidationResult + { + get => _validationResult; + private set => SetProperty(ref _validationResult, value); + } + + internal JwtAlgorithmDisplayPair AlgorithmMode + { + get + { + JwtAlgorithm settingsValue = SettingsProvider.GetSetting(JwtDecoderEncoderSettings.JwtAlgorithm); + JwtAlgorithmDisplayPair? algorithm = Algorithms.FirstOrDefault(x => x.Value == settingsValue); + Header = JsonHelper.Format(@"{""alg"": """ + algorithm.DisplayName + @""", ""typ"": ""JWT""}", Indentation.TwoSpaces, false); + IsSignatureRequired(algorithm); + return _algorithmSelected ?? JwtAlgorithmDisplayPair.HS256; + } + set + { + if (_algorithmSelected != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.JwtAlgorithm, value.Value); + IsSignatureRequired(value); + SetProperty(ref _algorithmSelected, value); + OnPropertyChanged(); + } + } + } + + internal IReadOnlyList Algorithms = new ObservableCollection { + JwtAlgorithmDisplayPair.HS256, JwtAlgorithmDisplayPair.HS384, JwtAlgorithmDisplayPair.HS512, + JwtAlgorithmDisplayPair.RS256, JwtAlgorithmDisplayPair.RS384, JwtAlgorithmDisplayPair.RS512, + JwtAlgorithmDisplayPair.ES256, JwtAlgorithmDisplayPair.ES384, JwtAlgorithmDisplayPair.ES512, + JwtAlgorithmDisplayPair.PS256, JwtAlgorithmDisplayPair.PS384, JwtAlgorithmDisplayPair.PS512, + }; + + public JwtDecoderEncoderViewModel( + ISettingsProvider settingsProvider, + IMarketingService marketingService) + { + SettingsProvider = settingsProvider; + MarketingService = marketingService; + InputFocusChanged = ControlFocusChanged; + IsSignatureRequired(AlgorithmMode); + } + + internal void QueueNewTokenJob() + { + JobQueue.Enqueue(true); + Messenger.Send(new JwtJobAddedMessage()); + } + + protected void DisplayValidationInfoBar() + { + InfoBarSeverity infoBarSeverity; + string message; + if (!JwtValidation.IsValid) + { + infoBarSeverity = InfoBarSeverity.Error; + message = JwtValidation.ErrorMessage ?? LocalizedStrings.JwtInValidMessage; + } + else + { + infoBarSeverity = InfoBarSeverity.Success; + message = LocalizedStrings.JwtIsValidMessage; + } + + ValidationResult = new InfoBarData(infoBarSeverity, message); + } + + protected void ControlFocusChanged(object source, RoutedEventArgs args) + { + var input = (CustomTextBox)source; + + if (input.Text.Length == 0) + { + return; + } + + QueueNewTokenJob(); + } + + #region Decoding + + protected SigningCredentials GetValidationCredentials(JwtAlgorithm algorithmMode) + { + SigningCredentials? signingCredentials = algorithmMode switch + { + JwtAlgorithm.ES512 => new SigningCredentials(GetECDsaValidationKey(), SecurityAlgorithms.EcdsaSha512Signature), + JwtAlgorithm.ES384 => new SigningCredentials(GetECDsaValidationKey(), SecurityAlgorithms.EcdsaSha384Signature), + JwtAlgorithm.ES256 => new SigningCredentials(GetECDsaValidationKey(), SecurityAlgorithms.EcdsaSha256Signature), + JwtAlgorithm.PS512 => new SigningCredentials(GetRsaShaValidationKey(), SecurityAlgorithms.RsaSsaPssSha512), + JwtAlgorithm.PS384 => new SigningCredentials(GetRsaShaValidationKey(), SecurityAlgorithms.RsaSsaPssSha384), + JwtAlgorithm.PS256 => new SigningCredentials(GetRsaShaValidationKey(), SecurityAlgorithms.RsaSsaPssSha256), + JwtAlgorithm.RS512 => new SigningCredentials(GetRsaShaValidationKey(), SecurityAlgorithms.RsaSha512Signature), + JwtAlgorithm.RS384 => new SigningCredentials(GetRsaShaValidationKey(), SecurityAlgorithms.RsaSha384Signature), + JwtAlgorithm.RS256 => new SigningCredentials(GetRsaShaValidationKey(), SecurityAlgorithms.RsaSha256Signature), + JwtAlgorithm.HS512 => new SigningCredentials(GetHmacShaValidationKey(algorithmMode), SecurityAlgorithms.HmacSha512Signature), + JwtAlgorithm.HS384 => new SigningCredentials(GetHmacShaValidationKey(algorithmMode), SecurityAlgorithms.HmacSha384Signature), + _ => new SigningCredentials(GetHmacShaValidationKey(algorithmMode), SecurityAlgorithms.HmacSha256Signature),// HS256 + }; + + return signingCredentials; + } + + private SymmetricSecurityKey? GetHmacShaValidationKey(JwtAlgorithm algorithmMode) + { + if (string.IsNullOrWhiteSpace(Signature)) + { + return null; + } + + byte[]? signatureByte; + if (Base64Helper.IsBase64DataStrict(Signature)) + { + signatureByte = Convert.FromBase64String(Signature); + } + else + { + signatureByte = Encoding.UTF8.GetBytes(Signature); + } + byte[] byteKey = algorithmMode switch + { + JwtAlgorithm.HS512 => new HMACSHA512(signatureByte).Key, + JwtAlgorithm.HS384 => new HMACSHA384(signatureByte).Key, + _ => new HMACSHA256(signatureByte).Key,// HS256 + }; + return new SymmetricSecurityKey(byteKey); + } + + private RsaSecurityKey? GetRsaShaValidationKey() + { + try + { + AsymmetricKeyParameter? asymmetricKeyParameter = GetPublicAsymmetricKeyParameter(); + if (asymmetricKeyParameter is null) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPublicKeyError; + return null; + } + + var publicKey = (RsaKeyParameters)asymmetricKeyParameter; + if (publicKey.IsPrivate) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.PublicKeyIsPrivateKeyError; + return null; + } + + RSAParameters rsaParameters = new(); + rsaParameters.Modulus = publicKey.Modulus.ToByteArrayUnsigned(); + rsaParameters.Exponent = publicKey.Exponent.ToByteArrayUnsigned(); + return new RsaSecurityKey(rsaParameters); + } + catch (Exception exception) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = $"{LocalizedStrings.InvalidPublicKeyError}: '{exception.Message}'"; + return null; + } + } + + private ECDsaSecurityKey? GetECDsaValidationKey() + { + try + { + AsymmetricKeyParameter? asymmetricKeyParameter = GetPublicAsymmetricKeyParameter(); + if (asymmetricKeyParameter is null) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPublicKeyError; + return null; + } + + var publicKey = (ECPublicKeyParameters)asymmetricKeyParameter; + if (publicKey.IsPrivate) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.PublicKeyIsPrivateKeyError; + return null; + } + + ECParameters ecParameters = new() + { + Curve = ECCurve.NamedCurves.nistP521, + Q = new() + { + X = publicKey.Q.AffineXCoord.GetEncoded(), + Y = publicKey.Q.AffineYCoord.GetEncoded() + } + }; + var ecdSa = ECDsa.Create(ecParameters); + return new ECDsaSecurityKey(ecdSa); + } + catch (Exception exception) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = $"{LocalizedStrings.InvalidPublicKeyError}: '{exception.Message}'"; + return null; + } + } + + private AsymmetricKeyParameter? GetPublicAsymmetricKeyParameter() + { + if (string.IsNullOrWhiteSpace(PublicKey)) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPublicKeyError; + return null; + } + var publicKeyStringBuilder = new StringBuilder(PublicKey!.Trim()); + if (!PublicKey!.StartsWith(PublicKeyStart)) + { + publicKeyStringBuilder.Insert(0, PublicKeyStart); + } + if (!PublicKey.EndsWith(PublicKeyEnd)) + { + publicKeyStringBuilder.Append(PublicKeyEnd); + } + + var pemReader = new PemReader(new StringReader(publicKeyStringBuilder.ToString())); + var asymetricPublicKey = (AsymmetricKeyParameter)pemReader.ReadObject(); + if (asymetricPublicKey is null) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPrivateKeyError; + return null; + } + + return asymetricPublicKey; + } + + #endregion + + #region Encoding + + protected SigningCredentials GetSigningCredentials(JwtAlgorithm algorithmMode) + { + SigningCredentials? signingCredentials = algorithmMode switch + { + JwtAlgorithm.ES512 => new SigningCredentials(GetECDsaSigningKey(), SecurityAlgorithms.EcdsaSha512Signature), + JwtAlgorithm.ES384 => new SigningCredentials(GetECDsaSigningKey(), SecurityAlgorithms.EcdsaSha384Signature), + JwtAlgorithm.ES256 => new SigningCredentials(GetECDsaSigningKey(), SecurityAlgorithms.EcdsaSha256Signature), + JwtAlgorithm.PS512 => new SigningCredentials(GetRsaShaSigningKey(), SecurityAlgorithms.RsaSsaPssSha512), + JwtAlgorithm.PS384 => new SigningCredentials(GetRsaShaSigningKey(), SecurityAlgorithms.RsaSsaPssSha384), + JwtAlgorithm.PS256 => new SigningCredentials(GetRsaShaSigningKey(), SecurityAlgorithms.RsaSsaPssSha256), + JwtAlgorithm.RS512 => new SigningCredentials(GetRsaShaSigningKey(), SecurityAlgorithms.RsaSha512Signature), + JwtAlgorithm.RS384 => new SigningCredentials(GetRsaShaSigningKey(), SecurityAlgorithms.RsaSha384Signature), + JwtAlgorithm.RS256 => new SigningCredentials(GetRsaShaSigningKey(), SecurityAlgorithms.RsaSha256Signature), + JwtAlgorithm.HS512 => new SigningCredentials(GetHmacShaSigningKey(), SecurityAlgorithms.HmacSha512Signature), + JwtAlgorithm.HS384 => new SigningCredentials(GetHmacShaSigningKey(), SecurityAlgorithms.HmacSha384Signature), + _ => new SigningCredentials(GetHmacShaSigningKey(), SecurityAlgorithms.HmacSha256Signature),// HS256 + }; + + return signingCredentials; + } + + private SymmetricSecurityKey? GetHmacShaSigningKey() + { + if (string.IsNullOrWhiteSpace(Signature)) + { + return null; + } + return new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Signature)); + } + + private RsaSecurityKey? GetRsaShaSigningKey() + { + AsymmetricKeyParameter? asymmetricKeyParameter = GetPrivateAsymmetricKeyParameter(); + if (asymmetricKeyParameter is null) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPrivateKeyError; + return null; + } + + var rsaPrivateKeyParameters = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter; + if (!rsaPrivateKeyParameters.IsPrivate) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPrivateKeyError; + return null; + } + + RSAParameters rsaParameters = new(); + rsaParameters.Modulus = rsaPrivateKeyParameters.Modulus.ToByteArrayUnsigned(); + rsaParameters.Exponent = rsaPrivateKeyParameters.PublicExponent.ToByteArrayUnsigned(); + rsaParameters.P = rsaPrivateKeyParameters.P.ToByteArrayUnsigned(); + rsaParameters.Q = rsaPrivateKeyParameters.Q.ToByteArrayUnsigned(); + rsaParameters.D = ConvertRSAParametersField(rsaPrivateKeyParameters.Exponent, rsaParameters.Modulus.Length); + rsaParameters.DP = ConvertRSAParametersField(rsaPrivateKeyParameters.DP, rsaParameters.P.Length); + rsaParameters.DQ = ConvertRSAParametersField(rsaPrivateKeyParameters.DQ, rsaParameters.Q.Length); + rsaParameters.InverseQ = ConvertRSAParametersField(rsaPrivateKeyParameters.QInv, rsaParameters.Q.Length); + + return new RsaSecurityKey(rsaParameters); + } + + private ECDsaSecurityKey? GetECDsaSigningKey() + { + AsymmetricKeyParameter? asymmetricKeyParameter = GetPrivateAsymmetricKeyParameter(); + if (asymmetricKeyParameter is null) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPrivateKeyError; + return null; + } + + var ecPrivateKeyParameters = (ECPrivateKeyParameters)asymmetricKeyParameter!; + if (!ecPrivateKeyParameters.IsPrivate) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPrivateKeyError; + return null; + } + + ECPoint ecPoint = new() + { + X = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded(), + Y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded() + }; + ECParameters ecParameters = new(); + ecParameters.Curve = ECCurve.NamedCurves.nistP521; + ecParameters.Q = ecPoint; + ecParameters.D = ecPrivateKeyParameters.D.ToByteArrayUnsigned(); + + var ecdSa = ECDsa.Create(ecParameters); + return new ECDsaSecurityKey(ecdSa); + } + + private AsymmetricKeyParameter? GetPrivateAsymmetricKeyParameter() + { + if (string.IsNullOrWhiteSpace(PrivateKey)) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPrivateKeyError; + return null; + } + var privateKeyStringBuilder = new StringBuilder(PrivateKey!.Trim()); + if (!PrivateKey!.StartsWith(PrivateKeyStart)) + { + privateKeyStringBuilder.Insert(0, PrivateKeyStart); + } + if (!PrivateKey.EndsWith(PrivateKeyEnd)) + { + privateKeyStringBuilder.Append(PrivateKeyEnd); + } + + var pemReader = new PemReader(new StringReader(privateKeyStringBuilder.ToString())); + object? pemObject = pemReader.ReadObject(); + if (pemObject is null) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPrivateKeyError; + return null; + } + + if (pemObject is AsymmetricKeyParameter) + { + return pemObject as AsymmetricKeyParameter; + } + else if (pemObject is AsymmetricCipherKeyPair) + { + var pair = pemObject as AsymmetricCipherKeyPair; + return pair!.Private; + } + + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = LocalizedStrings.InvalidPrivateKeyError; + return null; + } + + /// + /// Source (https://stackoverflow.com/questions/28370414/import-rsa-key-from-bouncycastle-sometimes-throws-bad-data) + /// + private static byte[] ConvertRSAParametersField(BigInteger n, int size) + { + byte[] bs = n.ToByteArrayUnsigned(); + if (bs.Length == size) + { + return bs; + } + if (bs.Length > size) + { + throw new ArgumentException("Specified size too small", "size"); + } + byte[] padded = new byte[size]; + Array.Copy(bs, 0, padded, size - bs.Length, bs.Length); + return padded; + } + + #endregion + + private void IsSignatureRequired(JwtAlgorithmDisplayPair value) + { + if (value.Value is JwtAlgorithm.HS256 || + value.Value is JwtAlgorithm.HS384 || + value.Value is JwtAlgorithm.HS512) + { + RequireSignature = true; + } + else + { + RequireSignature = false; + } + } + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControlViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControlViewModel.cs new file mode 100644 index 0000000000..c2f589c75e --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControlViewModel.cs @@ -0,0 +1,310 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Composition; +using System.IdentityModel.Tokens.Jwt; +using System.Text.Json; +using System.Threading.Tasks; +using DevToys.Api.Core; +using DevToys.Api.Core.Settings; +using DevToys.Api.Tools; +using DevToys.Core.Threading; +using DevToys.Models; +using DevToys.Shared.Core.Threading; +using DevToys.Views.Tools.EncodersDecoders.JwtDecoderEncoder; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Toolkit.Mvvm.Messaging; + +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + [Export(typeof(JwtEncoderControlViewModel))] + public sealed class JwtEncoderControlViewModel : JwtDecoderEncoderViewModel, IToolViewModel, IRecipient + { + private string? _token; + private bool _hasExpiration; + private bool _hasDefaultTime; + private bool _hasAudience; + private bool _hasIssuer; + private bool _hasError; + private int _expireYear = 0; + private int _expireMonth = 0; + private int _expireDay = 0; + private int _expireHour = 0; + private int _expireMinute = 0; + + internal override string? Token + { + get => _token; + set + { + if (_token != value) + { + SetProperty(ref _token, value?.Trim()); + } + } + } + + internal bool HasExpiration + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.HasExpiration); + set + { + if (_hasExpiration != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasExpiration, value); + SetProperty(ref _hasExpiration, value); + QueueNewTokenJob(); + } + } + } + + internal bool HasAudience + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.HasAudience); + set + { + if (_hasAudience != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasAudience, value); + SetProperty(ref _hasAudience, value); + QueueNewTokenJob(); + } + } + } + + internal bool HasIssuer + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.HasIssuer); + set + { + if (_hasIssuer != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasIssuer, value); + SetProperty(ref _hasIssuer, value); + QueueNewTokenJob(); + } + } + } + + internal bool HasDefaultTime + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.HasDefaultTime); + set + { + if (_hasDefaultTime != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasDefaultTime, value); + SetProperty(ref _hasDefaultTime, value); + QueueNewTokenJob(); + } + } + } + + internal bool HasError + { + get => _hasError; + set + { + SetProperty(ref _hasError, value); + } + } + + internal int ExpireYear + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ExpireYear); + set + { + if (value < 1) + { + return; + } + if (_expireYear != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireYear, value); + SetProperty(ref _expireYear, value); + QueueNewTokenJob(); + } + } + } + + internal int ExpireMonth + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ExpireMonth); + set + { + if (value < 1) + { + return; + } + if (_expireMonth != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireMonth, value); + SetProperty(ref _expireMonth, value); + QueueNewTokenJob(); + } + } + } + + internal int ExpireDay + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ExpireDay); + set + { + if (value < 1) + { + return; + } + if (_expireDay != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireDay, value); + SetProperty(ref _expireDay, value); + QueueNewTokenJob(); + } + } + + } + + internal int ExpireHour + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ExpireHour); + set + { + if (value < 1) + { + return; + } + if (_expireHour != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireHour, value); + SetProperty(ref _expireHour, value); + QueueNewTokenJob(); + } + } + } + + internal int ExpireMinute + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ExpireMinute); + set + { + if (value < 1) + { + return; + } + if (_expireMinute != value) + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireMinute, value); + SetProperty(ref _expireMinute, value); + QueueNewTokenJob(); + } + } + } + + public Type View { get; } = typeof(JwtEncoderControl); + + [ImportingConstructor] + public JwtEncoderControlViewModel( + ISettingsProvider settingsProvider, + IMarketingService marketingService) + : base(settingsProvider, marketingService) + { + IsActive = true; + } + + public void Receive(JwtJobAddedMessage message) + { + TreatQueueAsync().Forget(); + } + + private async Task TreatQueueAsync() + { + if (WorkInProgress) + { + return; + } + + WorkInProgress = true; + + await TaskScheduler.Default; + + while (JobQueue.TryDequeue(out _)) + { + GenerateToken(out string? tokenString, AlgorithmMode.Value); + ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => + { + Token = tokenString; + + if (!ToolSuccessfullyWorked) + { + ToolSuccessfullyWorked = true; + MarketingService.NotifyToolSuccessfullyWorked(); + } + }).ForgetSafely(); + } + + WorkInProgress = false; + } + + private void GenerateToken(out string tokenString, JwtAlgorithm algorithmMode) + { + if (string.IsNullOrWhiteSpace(Header) || string.IsNullOrWhiteSpace(Payload)) + { + tokenString = string.Empty; + return; + } + + try + { + var serializeOptions = new JsonSerializerOptions(); + serializeOptions.Converters.Add(new JwtPayloadConverter()); + Dictionary? payload = JsonSerializer.Deserialize>(Payload!, serializeOptions); + SigningCredentials? signingCredentials = GetSigningCredentials(algorithmMode); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Claims = payload, + SigningCredentials = signingCredentials, + Expires = null + }; + + if (HasExpiration) + { + DateTime expirationDate = DateTime.UtcNow + .AddYears(ExpireYear) + .AddMonths(ExpireMonth) + .AddDays(ExpireDay) + .AddHours(ExpireHour) + .AddMinutes(ExpireMinute); + tokenDescriptor.Expires = expirationDate; + } + + if (HasAudience) + { + tokenDescriptor.Audience = ValidAudiences; + } + + if (HasIssuer) + { + tokenDescriptor.Issuer = ValidIssuers; + tokenDescriptor.IssuedAt = DateTime.UtcNow; + } + + var handler = new JwtSecurityTokenHandler(); + if (!HasDefaultTime) + { + handler.SetDefaultTimesOnTokenCreation = false; + } + + SecurityToken? token = handler.CreateToken(tokenDescriptor); + tokenString = handler.WriteToken(token); + } + catch (Exception exception) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = exception.Message; + tokenString = string.Empty; + } + } + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtJobAddedMessage.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtJobAddedMessage.cs new file mode 100644 index 0000000000..276430957f --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtJobAddedMessage.cs @@ -0,0 +1,11 @@ +using Windows.UI.Xaml.Documents; + +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + public sealed class JwtJobAddedMessage + { + public bool IsEncoding { get; set; } + + public bool IsDecoding { get; set; } + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtPayloadConverter.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtPayloadConverter.cs new file mode 100644 index 0000000000..2f105c78f7 --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtPayloadConverter.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + internal class JwtPayloadConverter : JsonConverter> + { + public override Dictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException(); + } + + var dictionary = new Dictionary(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return dictionary; + } + + if (reader.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException(); + } + + string? propertyName = reader.GetString(); + reader.Read(); + object? value = GetValue(ref reader); + + if (!string.IsNullOrWhiteSpace(propertyName) && value is not null) + { + dictionary.Add(propertyName!, value); + } + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, Dictionary value, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + private object? GetValue(ref Utf8JsonReader reader) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + return reader.GetString(); + case JsonTokenType.False: + return false; + case JsonTokenType.True: + return true; + case JsonTokenType.Null: + return null; + case JsonTokenType.Number: + if (reader.TryGetInt64(out long _long)) + { + return _long; + } + else if (reader.TryGetDecimal(out decimal _dec)) + { + return _dec; + } + throw new JsonException($"Unhandled Number value"); + case JsonTokenType.StartArray: + List array = new(); + while (reader.Read() && + reader.TokenType != JsonTokenType.EndArray) + { + array.Add(GetValue(ref reader)); + } + return array.ToArray(); + case JsonTokenType.StartObject: + var result = new ExpandoObject(); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + JsonTokenType tokenType = reader.TokenType; + string? propertyName = null; + if (reader.TokenType is JsonTokenType.PropertyName) + { + propertyName = reader.GetString(); + } + reader.Read(); + result.TryAdd(propertyName, GetValue(ref reader)); + } + return result; + default: + return JsonDocument.ParseValue(ref reader).RootElement.Clone(); + } + } + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolJobItem.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolJobItem.cs new file mode 100644 index 0000000000..02615a58b2 --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolJobItem.cs @@ -0,0 +1,6 @@ +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + public record JwtToolJobItem + { + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolSwitchedMessage.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolSwitchedMessage.cs new file mode 100644 index 0000000000..f53b8d06a1 --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolSwitchedMessage.cs @@ -0,0 +1,8 @@ +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + public sealed class JwtToolSwitchedMessage + { + public JwtToolSwitchedMessage() + { } + } +} diff --git a/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControl.xaml b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControl.xaml new file mode 100644 index 0000000000..06512d94b7 --- /dev/null +++ b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControl.xaml @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControl.xaml.cs b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControl.xaml.cs new file mode 100644 index 0000000000..3483132c84 --- /dev/null +++ b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControl.xaml.cs @@ -0,0 +1,34 @@ +#nullable enable + +using System.Composition; +using DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace DevToys.Views.Tools.EncodersDecoders.JwtDecoderEncoder +{ + public sealed partial class JwtDecoderControl : UserControl + { + public static readonly DependencyProperty ViewModelProperty + = DependencyProperty.Register( + nameof(ViewModel), + typeof(JwtDecoderControlViewModel), + typeof(JwtDecoderControl), + new PropertyMetadata(default(JwtDecoderControlViewModel))); + + /// + /// Gets the page's view model. + /// + public JwtDecoderControlViewModel ViewModel + { + get => (JwtDecoderControlViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + [ImportingConstructor] + public JwtDecoderControl() + { + InitializeComponent(); + } + } +} diff --git a/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolPage.xaml b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolPage.xaml index 37b76b4039..7d7e3671a0 100644 --- a/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolPage.xaml +++ b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolPage.xaml @@ -5,10 +5,16 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="using:DevToys.UI.Controls" - xmlns:toolkit="using:Microsoft.Toolkit.Uwp.UI.Controls" + xmlns:converters="using:DevToys.UI.Converters" + xmlns:jwt="using:DevToys.Views.Tools.EncodersDecoders.JwtDecoderEncoder" mc:Ignorable="d" NavigationCacheMode="Required"> + + + + + @@ -18,46 +24,23 @@ - - - + - - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolPage.xaml.cs b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolPage.xaml.cs index c3b04f6bdc..9cd0de199b 100644 --- a/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolPage.xaml.cs +++ b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolPage.xaml.cs @@ -46,7 +46,8 @@ protected override void OnNavigatedTo(NavigationEventArgs e) if (!string.IsNullOrWhiteSpace(parameters.ClipBoardContent)) { - ViewModel.JwtToken = parameters.ClipBoardContent; + ViewModel.DecoderViewModel.Token = parameters.ClipBoardContent; + ViewModel.JwtToolMode = false; } base.OnNavigatedTo(e); diff --git a/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControl.xaml b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControl.xaml new file mode 100644 index 0000000000..c469114f4f --- /dev/null +++ b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControl.xaml @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControl.xaml.cs b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControl.xaml.cs new file mode 100644 index 0000000000..0ad130569c --- /dev/null +++ b/src/dev/impl/DevToys/Views/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControl.xaml.cs @@ -0,0 +1,34 @@ +#nullable enable + +using System.Composition; +using DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace DevToys.Views.Tools.EncodersDecoders.JwtDecoderEncoder +{ + public sealed partial class JwtEncoderControl : UserControl + { + public static readonly DependencyProperty ViewModelProperty + = DependencyProperty.Register( + nameof(ViewModel), + typeof(JwtEncoderControlViewModel), + typeof(JwtEncoderControl), + new PropertyMetadata(default(JwtEncoderControlViewModel))); + + /// + /// Gets the page's view model. + /// + public JwtEncoderControlViewModel ViewModel + { + get => (JwtEncoderControlViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + [ImportingConstructor] + public JwtEncoderControl() + { + InitializeComponent(); + } + } +} From 8683da5e98602a4e478350e5d4eff0ac02e3b693 Mon Sep 17 00:00:00 2001 From: Benjamin Titeux Date: Sat, 27 Aug 2022 13:30:16 +0200 Subject: [PATCH 2/7] reworked jwt decoder / encoder --- .../DevToys/Core/QueueWorkerViewModelBase.cs | 18 +- .../Core/QueueWorkerViewModelBaseAsync.cs | 54 ++ src/dev/impl/DevToys/DevToys.csproj | 9 +- src/dev/impl/DevToys/Helpers/JwtHelper.cs | 15 +- src/dev/impl/DevToys/LanguageManager.cs | 14 +- .../JwtDecoderEncoder/DecoderParameters.cs | 20 + .../JwtDecoderEncoder/EncoderParameters.cs | 17 + .../JwtDecoderEncoder/TokenParameters.cs | 37 + .../Models/JwtDecoderEncoder/TokenResult.cs | 22 + .../TokenResultErrorEventArgs.cs | 14 + .../Strings/en-US/JwtDecoderEncoder.resw | 10 +- .../Base64EncoderDecoderToolProvider.cs | 66 +- .../Base64EncoderDecoderToolViewModel.cs | 2 +- .../JwtDecoderEncoder/JwtDecoder.cs | 293 ++++++++ .../JwtDecoderControlViewModel.cs | 236 ++----- .../JwtDecoderEncoderSettings.cs | 49 +- .../JwtDecoderEncoderToolProvider.cs | 4 +- .../JwtDecoderEncoderToolViewModel.cs | 9 +- .../JwtDecoderEncoderViewModel.cs | 154 +++-- .../JwtDecoderEncoder/JwtEncoder.cs | 285 ++++++++ .../JwtEncoderControlViewModel.cs | 215 ++---- .../JwtDecoderEncoder/JwtJobAddedMessage.cs | 5 +- .../JwtToolSwitchedMessage.cs | 8 - .../HashGeneratorToolViewModel.cs | 2 +- .../StringUtilitiesToolViewModel.cs | 2 +- src/tests/DevToys.Tests/DevToys.Tests.csproj | 29 + .../Helpers/Base64HelperTests.cs | 22 + .../DevToys.Tests/Helpers/JwtHelperTests.cs | 24 + .../Providers/TestData/Jwt/BasicPayload.json | 22 + .../TestData/Jwt/ComplexPayload.json | 31 + .../Providers/TestData/Jwt/ES/BasicToken.txt | 1 + .../TestData/Jwt/ES/ComplexToken.txt | 1 + .../Providers/TestData/Jwt/ES/Header.json | 4 + .../Providers/TestData/Jwt/ES/PrivateKey.txt | 5 + .../Providers/TestData/Jwt/ES/PublicKey.txt | 4 + .../Providers/TestData/Jwt/HS/BasicToken.txt | 1 + .../TestData/Jwt/HS/ComplexToken.txt | 1 + .../Providers/TestData/Jwt/HS/Header.json | 4 + .../Providers/TestData/Jwt/HS/Signature.txt | 1 + .../Providers/TestData/Jwt/PS/BasicToken.txt | 1 + .../TestData/Jwt/PS/ComplexToken.txt | 1 + .../Providers/TestData/Jwt/PS/Header.json | 4 + .../Providers/TestData/Jwt/PS/PrivateKey.txt | 28 + .../Providers/TestData/Jwt/PS/PublicKey.txt | 9 + .../Providers/TestData/Jwt/RS/BasicToken.txt | 1 + .../TestData/Jwt/RS/ComplexToken.txt | 1 + .../Providers/TestData/Jwt/RS/Header.json | 4 + .../Providers/TestData/Jwt/RS/PrivateKey.txt | 28 + .../Providers/TestData/Jwt/RS/PublicKey.txt | 9 + .../Providers/Tools/JwtDecoderEncoderTests.cs | 434 ++++++++++++ .../Providers/Tools/JwtDecoderTests.cs | 650 ++++++++++++++++++ .../Providers/Tools/JwtEncoderTests.cs | 521 ++++++++++++++ 52 files changed, 2922 insertions(+), 479 deletions(-) create mode 100644 src/dev/impl/DevToys/Core/QueueWorkerViewModelBaseAsync.cs create mode 100644 src/dev/impl/DevToys/Models/JwtDecoderEncoder/DecoderParameters.cs create mode 100644 src/dev/impl/DevToys/Models/JwtDecoderEncoder/EncoderParameters.cs create mode 100644 src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenParameters.cs create mode 100644 src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenResult.cs create mode 100644 src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenResultErrorEventArgs.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoder.cs create mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoder.cs delete mode 100644 src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolSwitchedMessage.cs create mode 100644 src/tests/DevToys.Tests/Helpers/Base64HelperTests.cs create mode 100644 src/tests/DevToys.Tests/Helpers/JwtHelperTests.cs create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/BasicPayload.json create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/ComplexPayload.json create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/ES/BasicToken.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/ES/ComplexToken.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/ES/Header.json create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/ES/PrivateKey.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/ES/PublicKey.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/HS/BasicToken.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/HS/ComplexToken.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/HS/Header.json create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/HS/Signature.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/PS/BasicToken.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/PS/ComplexToken.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/PS/Header.json create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/PS/PrivateKey.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/PS/PublicKey.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/RS/BasicToken.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/RS/ComplexToken.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/RS/Header.json create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/RS/PrivateKey.txt create mode 100644 src/tests/DevToys.Tests/Providers/TestData/Jwt/RS/PublicKey.txt create mode 100644 src/tests/DevToys.Tests/Providers/Tools/JwtDecoderEncoderTests.cs create mode 100644 src/tests/DevToys.Tests/Providers/Tools/JwtDecoderTests.cs create mode 100644 src/tests/DevToys.Tests/Providers/Tools/JwtEncoderTests.cs diff --git a/src/dev/impl/DevToys/Core/QueueWorkerViewModelBase.cs b/src/dev/impl/DevToys/Core/QueueWorkerViewModelBase.cs index 93df06b86c..f8ff2d3163 100644 --- a/src/dev/impl/DevToys/Core/QueueWorkerViewModelBase.cs +++ b/src/dev/impl/DevToys/Core/QueueWorkerViewModelBase.cs @@ -1,20 +1,18 @@ #nullable enable using System.Collections.Generic; -using System.Threading.Tasks; -using DevToys.Shared.Core.Threading; using Microsoft.Toolkit.Mvvm.ComponentModel; namespace DevToys.Core { - public abstract class QueueWorkerViewModelBase : ObservableRecipient where T : class + public abstract class QueueWorkerViewModelBase : ObservableRecipient { private readonly object _lock = new(); private readonly Queue _computationQueue = new(); private bool _computationInProgress; - internal Task ComputationTask { get; private set; } = Task.CompletedTask; + internal bool ComputationTask { get; private set; } protected void EnqueueComputation(T value) { @@ -25,29 +23,29 @@ protected void EnqueueComputation(T value) if (!_computationInProgress) { _computationInProgress = true; - ComputationTask = TreatComputationQueueAsync(); + ComputationTask = TreatComputationQueue(); } } } - protected abstract Task TreatComputationQueueAsync(T value); + protected abstract void TreatComputationQueue(T value); - private async Task TreatComputationQueueAsync() + private bool TreatComputationQueue() { - await TaskScheduler.Default; - while (_computationQueue.TryDequeue(out T value)) { - await TreatComputationQueueAsync(value).ConfigureAwait(false); + TreatComputationQueue(value); lock (_lock) { if (_computationQueue.Count == 0) { _computationInProgress = false; + return true; } } } + return true; } } } diff --git a/src/dev/impl/DevToys/Core/QueueWorkerViewModelBaseAsync.cs b/src/dev/impl/DevToys/Core/QueueWorkerViewModelBaseAsync.cs new file mode 100644 index 0000000000..a0b92e1a4c --- /dev/null +++ b/src/dev/impl/DevToys/Core/QueueWorkerViewModelBaseAsync.cs @@ -0,0 +1,54 @@ +#nullable enable + +using System.Collections.Generic; +using System.Threading.Tasks; +using DevToys.Shared.Core.Threading; +using Microsoft.Toolkit.Mvvm.ComponentModel; + +namespace DevToys.Core +{ + public abstract class QueueWorkerViewModelBaseAsync : ObservableRecipient where T : class + { + private readonly object _lock = new(); + private readonly Queue _computationQueue = new(); + + private bool _computationInProgress; + + internal Task ComputationTask { get; private set; } = Task.CompletedTask; + + protected void EnqueueComputation(T value) + { + lock (_lock) + { + _computationQueue.Enqueue(value); + + if (!_computationInProgress) + { + _computationInProgress = true; + ComputationTask = TreatComputationQueueAsync(); + } + } + } + + protected abstract Task TreatComputationQueueAsync(T value); + + private async Task TreatComputationQueueAsync() + { + await TaskScheduler.Default; + + while (_computationQueue.TryDequeue(out T value)) + { + await TreatComputationQueueAsync(value).ConfigureAwait(false); + + lock (_lock) + { + if (_computationQueue.Count == 0) + { + _computationInProgress = false; + } + } + } + } + } + +} diff --git a/src/dev/impl/DevToys/DevToys.csproj b/src/dev/impl/DevToys/DevToys.csproj index 6ddc20bdc4..bebf16e04e 100644 --- a/src/dev/impl/DevToys/DevToys.csproj +++ b/src/dev/impl/DevToys/DevToys.csproj @@ -28,6 +28,7 @@ + @@ -77,6 +78,11 @@ + + + + + @@ -118,6 +124,8 @@ + + @@ -125,7 +133,6 @@ - diff --git a/src/dev/impl/DevToys/Helpers/JwtHelper.cs b/src/dev/impl/DevToys/Helpers/JwtHelper.cs index 9dd7bddffe..f9446ab805 100644 --- a/src/dev/impl/DevToys/Helpers/JwtHelper.cs +++ b/src/dev/impl/DevToys/Helpers/JwtHelper.cs @@ -7,6 +7,9 @@ namespace DevToys.Helpers { internal static class JwtHelper { + private static readonly string AuthorizationHeader = "Authorization:"; + private static readonly string BearerScheme = "Bearer"; + /// /// Detects whether the given string is a JWT Token or not. /// @@ -19,10 +22,20 @@ internal static bool IsValid(string? input) input = input!.Trim(); + if (input.StartsWith(AuthorizationHeader)) + { + input = input.Remove(0, AuthorizationHeader.Length).Trim(); + } + + if (input.StartsWith(BearerScheme)) + { + input = input.Remove(0, BearerScheme.Length).Trim(); + } + try { var handler = new JwtSecurityTokenHandler(); - JwtSecurityToken jwtSecurityToken = handler.ReadJwtToken(input); + JwtSecurityToken jwtSecurityToken = handler.ReadJwtToken(input.Trim()); return jwtSecurityToken is not null; } catch (Exception) //some other exception diff --git a/src/dev/impl/DevToys/LanguageManager.cs b/src/dev/impl/DevToys/LanguageManager.cs index b816a8a5ab..ee6eb49d7f 100644 --- a/src/dev/impl/DevToys/LanguageManager.cs +++ b/src/dev/impl/DevToys/LanguageManager.cs @@ -1577,9 +1577,14 @@ public class JwtDecoderEncoderStrings : ObservableObject public string AccessibleName => _resources.GetString("AccessibleName"); /// - /// Gets the resource DisplayName. + /// Gets the resource MenuDisplayName. + /// + public string MenuDisplayName => _resources.GetString("MenuDisplayName"); + + /// + /// Gets the resource SearchDisplayName. /// - public string DisplayName => _resources.GetString("DisplayName"); + public string SearchDisplayName => _resources.GetString("SearchDisplayName"); /// /// Gets the resource JwtHeaderLabel. @@ -1791,6 +1796,11 @@ public class JwtDecoderEncoderStrings : ObservableObject /// public string InvalidPrivateKeyError => _resources.GetString("InvalidPrivateKeyError"); + /// + /// Gets the resource InvalidSignatureError. + /// + public string InvalidSignatureError => _resources.GetString("InvalidSignatureError"); + /// /// Gets the resource PublicKeyLabel. /// diff --git a/src/dev/impl/DevToys/Models/JwtDecoderEncoder/DecoderParameters.cs b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/DecoderParameters.cs new file mode 100644 index 0000000000..1b41a0434f --- /dev/null +++ b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/DecoderParameters.cs @@ -0,0 +1,20 @@ +#nullable enable + +using DevToys; + +namespace DevToys.Models.JwtDecoderEncoder +{ + public class DecoderParameters + { + public bool ValidateSignature { get; set; } + + public bool ValidateActor { get; set; } + + public bool ValidateLifetime { get; set; } + + public bool ValidateIssuer { get; set; } + + public bool ValidateAudience { get; set; } + + } +} diff --git a/src/dev/impl/DevToys/Models/JwtDecoderEncoder/EncoderParameters.cs b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/EncoderParameters.cs new file mode 100644 index 0000000000..54992474aa --- /dev/null +++ b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/EncoderParameters.cs @@ -0,0 +1,17 @@ +#nullable enable + +using DevToys; + +namespace DevToys.Models.JwtDecoderEncoder +{ + public class EncoderParameters + { + public bool HasExpiration { get; set; } + + public bool HasAudience { get; set; } + + public bool HasIssuer { get; set; } + + public bool HasDefaultTime { get; set; } + } +} diff --git a/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenParameters.cs b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenParameters.cs new file mode 100644 index 0000000000..5699496de8 --- /dev/null +++ b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenParameters.cs @@ -0,0 +1,37 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; + +namespace DevToys.Models.JwtDecoderEncoder +{ + public class TokenParameters + { + public string? Token { get; set; } + + public string? Payload { get; set; } + + public string? Signature { get; set; } + + public string? PublicKey { get; set; } + + public string? PrivateKey { get; set; } + + public JwtAlgorithm TokenAlgorithm { get; set; } + + public int ExpirationYear { get; set; } + + public int ExpirationMonth { get; set; } + + public int ExpirationDay { get; set; } + + public int ExpirationHour { get; set; } + + public int ExpirationMinute { get; set; } + + public HashSet ValidIssuers { get; set; } = new HashSet(); + + public HashSet ValidAudiences { get; set; } = new HashSet(); + + } +} diff --git a/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenResult.cs b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenResult.cs new file mode 100644 index 0000000000..eeba1aa86b --- /dev/null +++ b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenResult.cs @@ -0,0 +1,22 @@ +#nullable enable + + +namespace DevToys.Models.JwtDecoderEncoder +{ + public class TokenResult + { + public string? Token { get; set; } + + public string? Header { get; set; } + + public string? Payload { get; set; } + + public string? Signature { get; set; } + + public string? PublicKey { get; set; } + + public string? PrivateKey { get; set; } + + public JwtAlgorithm TokenAlgorithm { get; set; } + } +} diff --git a/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenResultErrorEventArgs.cs b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenResultErrorEventArgs.cs new file mode 100644 index 0000000000..c8dd6e7000 --- /dev/null +++ b/src/dev/impl/DevToys/Models/JwtDecoderEncoder/TokenResultErrorEventArgs.cs @@ -0,0 +1,14 @@ +#nullable enable + +using System; +using Microsoft.UI.Xaml.Controls; + +namespace DevToys.Models.JwtDecoderEncoder +{ + public class TokenResultErrorEventArgs : EventArgs + { + public string Message { get; set; } = string.Empty; + + public InfoBarSeverity Severity { get; set; } + } +} diff --git a/src/dev/impl/DevToys/Strings/en-US/JwtDecoderEncoder.resw b/src/dev/impl/DevToys/Strings/en-US/JwtDecoderEncoder.resw index 5cd60dc18f..8ac98bf455 100644 --- a/src/dev/impl/DevToys/Strings/en-US/JwtDecoderEncoder.resw +++ b/src/dev/impl/DevToys/Strings/en-US/JwtDecoderEncoder.resw @@ -120,8 +120,8 @@ JWT Encoder / Decoder tool - - JWT Encoder / Decoder + + JWT Header @@ -252,4 +252,10 @@ Public Key + + JWT Encoder / Decoder + + + Invalid Signature + \ No newline at end of file diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/Base64TextEncoderDecoder/Base64EncoderDecoderToolProvider.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/Base64TextEncoderDecoder/Base64EncoderDecoderToolProvider.cs index 8f8d7bc8f4..8fc18cab3a 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/Base64TextEncoderDecoder/Base64EncoderDecoderToolProvider.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/Base64TextEncoderDecoder/Base64EncoderDecoderToolProvider.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using DevToys.Api.Tools; using DevToys.Core.Threading; +using DevToys.Helpers; using DevToys.Shared.Api.Core; using Windows.UI.Xaml.Controls; @@ -46,7 +47,7 @@ public bool CanBeTreatedByTool(string data) } string? trimmedData = data.Trim(); - bool isBase64 = IsBase64DataStrict(trimmedData); + bool isBase64 = Base64Helper.IsBase64DataStrict(trimmedData); return isBase64; } @@ -55,68 +56,5 @@ public IToolViewModel CreateTool() { return _mefProvider.Import(); } - - private bool IsBase64DataStrict(string data) - { - if (string.IsNullOrWhiteSpace(data)) - { - return false; - } - - if (data.Length % 4 != 0) - { - return false; - } - - if (new Regex(@"[^A-Z0-9+/=]", RegexOptions.IgnoreCase).IsMatch(data)) - { - return false; - } - - int equalIndex = data.IndexOf('='); - int length = data.Length; - - if (!(equalIndex == -1 || equalIndex == length - 1 || (equalIndex == length - 2 && data[length - 1] == '='))) - { - return false; - } - - string? decoded; - - try - { - byte[]? decodedData = Convert.FromBase64String(data); - decoded = Encoding.UTF8.GetString(decodedData); - } - catch (Exception) - { - return false; - } - - //check for special chars that you know should not be there - char current; - for (int i = 0; i < decoded.Length; i++) - { - current = decoded[i]; - if (current == 65533) - { - return false; - } - -#pragma warning disable IDE0078 // Use pattern matching - if (!(current == 0x9 - || current == 0xA - || current == 0xD - || (current >= 0x20 && current <= 0xD7FF) - || (current >= 0xE000 && current <= 0xFFFD) - || (current >= 0x10000 && current <= 0x10FFFF))) -#pragma warning restore IDE0078 // Use pattern matching - { - return false; - } - } - - return true; - } } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/Base64TextEncoderDecoder/Base64EncoderDecoderToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/Base64TextEncoderDecoder/Base64EncoderDecoderToolViewModel.cs index a871c5db16..8e0ad94857 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/Base64TextEncoderDecoder/Base64EncoderDecoderToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/Base64TextEncoderDecoder/Base64EncoderDecoderToolViewModel.cs @@ -17,7 +17,7 @@ namespace DevToys.ViewModels.Tools.Base64EncoderDecoder { [Export(typeof(Base64EncoderDecoderToolViewModel))] - public class Base64EncoderDecoderToolViewModel : QueueWorkerViewModelBase, IToolViewModel + public class Base64EncoderDecoderToolViewModel : QueueWorkerViewModelBaseAsync, IToolViewModel { /// /// Whether the tool should encode or decode Base64. diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoder.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoder.cs new file mode 100644 index 0000000000..6b97a1fb14 --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoder.cs @@ -0,0 +1,293 @@ +#nullable enable + +using System; +using System.Composition; +using System.IdentityModel.Tokens.Jwt; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using DevToys.Helpers; +using DevToys.Helpers.JsonYaml; +using DevToys.Models; +using DevToys.Models.JwtDecoderEncoder; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using Microsoft.UI.Xaml.Controls; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; + +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + [Export(typeof(JwtDecoder))] + [Shared] + public class JwtDecoder + { + private const string PublicKeyStart = "-----BEGIN PUBLIC KEY-----"; + private const string PublicKeyEnd = "-----END PUBLIC KEY-----"; + private Action? _decodingErrorCallBack; + private JwtDecoderEncoderStrings _localizedStrings => LanguageManager.Instance.JwtDecoderEncoder; + + public TokenResult? DecodeToken( + DecoderParameters decodeParameters, + TokenParameters tokenParameters, + Action decodingErrorCallBack) + { + if (decodeParameters is null) + { + throw new ArgumentNullException(nameof(decodeParameters)); + } + + if (tokenParameters is null) + { + throw new ArgumentNullException(nameof(tokenParameters)); + } + + if (decodingErrorCallBack is null) + { + throw new ArgumentNullException(nameof(decodingErrorCallBack)); + } + + if (string.IsNullOrWhiteSpace(tokenParameters.Token)) + { + throw new ArgumentNullException(nameof(tokenParameters.Token)); + } + + _decodingErrorCallBack = decodingErrorCallBack; + + var tokenResult = new TokenResult(); + + try + { + IdentityModelEventSource.ShowPII = true; + var handler = new JwtSecurityTokenHandler(); + JwtSecurityToken jwtSecurityToken = handler.ReadJwtToken(tokenParameters.Token); + tokenResult.Header = JsonHelper.Format(jwtSecurityToken.Header.SerializeToJson(), Indentation.TwoSpaces, false); + tokenResult.Payload = JsonHelper.Format(jwtSecurityToken.Payload.SerializeToJson(), Indentation.TwoSpaces, false); + tokenResult.TokenAlgorithm = tokenParameters.TokenAlgorithm; + + if (decodeParameters.ValidateSignature) + { + var signatureValid = ValidateTokenSignature(handler, decodeParameters, tokenParameters, tokenResult); + if (!signatureValid) + { + return null; + } + } + } + catch (Exception exception) + { + RaiseError(exception.Message); + return null; + } + + return tokenResult; + } + + private bool ValidateTokenSignature( + JwtSecurityTokenHandler handler, + DecoderParameters decodeParameters, + TokenParameters tokenParameters, + TokenResult tokenResult) + { + SigningCredentials? signingCredentials = GetValidationCredentials(tokenParameters); + var validationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = signingCredentials.Key, + TryAllIssuerSigningKeys = true, + ValidateActor = decodeParameters.ValidateActor, + ValidateLifetime = decodeParameters.ValidateLifetime, + ValidateIssuer = decodeParameters.ValidateIssuer, + ValidateAudience = decodeParameters.ValidateAudience + }; + + if (decodeParameters.ValidateIssuer) + { + if (!tokenParameters.ValidIssuers.Any()) + { + RaiseError(_localizedStrings.ValidIssuersError); + return false; + } + validationParameters.ValidIssuers = tokenParameters.ValidIssuers; + } + + if (decodeParameters.ValidateAudience) + { + if (!tokenParameters.ValidAudiences.Any()) + { + RaiseError(_localizedStrings.ValidAudiencesError); + return false; + } + validationParameters.ValidAudiences = tokenParameters.ValidAudiences; + } + + try + { + handler.ValidateToken(tokenParameters.Token, validationParameters, out _); + tokenResult.Signature = tokenParameters.Signature; + tokenResult.PublicKey = tokenParameters.PublicKey; + return true; + } + catch (Exception exception) + { + RaiseError(exception.Message); + } + return false; + } + + private SigningCredentials GetValidationCredentials( + TokenParameters tokenParameters) + { + SigningCredentials? signingCredentials = tokenParameters.TokenAlgorithm switch + { + JwtAlgorithm.ES512 => new SigningCredentials(GetECDsaValidationKey(tokenParameters), SecurityAlgorithms.EcdsaSha512Signature), + JwtAlgorithm.ES384 => new SigningCredentials(GetECDsaValidationKey(tokenParameters), SecurityAlgorithms.EcdsaSha384Signature), + JwtAlgorithm.ES256 => new SigningCredentials(GetECDsaValidationKey(tokenParameters), SecurityAlgorithms.EcdsaSha256Signature), + JwtAlgorithm.PS512 => new SigningCredentials(GetRsaShaValidationKey(tokenParameters), SecurityAlgorithms.RsaSsaPssSha512), + JwtAlgorithm.PS384 => new SigningCredentials(GetRsaShaValidationKey(tokenParameters), SecurityAlgorithms.RsaSsaPssSha384), + JwtAlgorithm.PS256 => new SigningCredentials(GetRsaShaValidationKey(tokenParameters), SecurityAlgorithms.RsaSsaPssSha256), + JwtAlgorithm.RS512 => new SigningCredentials(GetRsaShaValidationKey(tokenParameters), SecurityAlgorithms.RsaSha512Signature), + JwtAlgorithm.RS384 => new SigningCredentials(GetRsaShaValidationKey(tokenParameters), SecurityAlgorithms.RsaSha384Signature), + JwtAlgorithm.RS256 => new SigningCredentials(GetRsaShaValidationKey(tokenParameters), SecurityAlgorithms.RsaSha256Signature), + JwtAlgorithm.HS512 => new SigningCredentials(GetHmacShaValidationKey(tokenParameters), SecurityAlgorithms.HmacSha512Signature), + JwtAlgorithm.HS384 => new SigningCredentials(GetHmacShaValidationKey(tokenParameters), SecurityAlgorithms.HmacSha384Signature), + _ => new SigningCredentials(GetHmacShaValidationKey(tokenParameters), SecurityAlgorithms.HmacSha256Signature),// HS256 + }; + return signingCredentials; + } + + private SymmetricSecurityKey? GetHmacShaValidationKey(TokenParameters tokenParameters) + { + if (string.IsNullOrWhiteSpace(tokenParameters.Signature)) + { + return null; + } + + byte[]? signatureByte; + if (Base64Helper.IsBase64DataStrict(tokenParameters.Signature)) + { + signatureByte = Convert.FromBase64String(tokenParameters.Signature); + } + else + { + signatureByte = Encoding.UTF8.GetBytes(tokenParameters.Signature); + } + byte[] byteKey = tokenParameters.TokenAlgorithm switch + { + JwtAlgorithm.HS512 => new HMACSHA512(signatureByte).Key, + JwtAlgorithm.HS384 => new HMACSHA384(signatureByte).Key, + _ => new HMACSHA256(signatureByte).Key,// HS256 + }; + return new SymmetricSecurityKey(byteKey); + } + + private RsaSecurityKey? GetRsaShaValidationKey(TokenParameters tokenParameters) + { + try + { + AsymmetricKeyParameter? asymmetricKeyParameter = GetPublicAsymmetricKeyParameter(tokenParameters); + if (asymmetricKeyParameter is null) + { + RaiseError(_localizedStrings.InvalidPublicKeyError); + return null; + } + + var publicKey = (RsaKeyParameters)asymmetricKeyParameter; + if (publicKey.IsPrivate) + { + RaiseError(_localizedStrings.PublicKeyIsPrivateKeyError); + return null; + } + + RSAParameters rsaParameters = new() + { + Modulus = publicKey.Modulus.ToByteArrayUnsigned(), + Exponent = publicKey.Exponent.ToByteArrayUnsigned() + }; + return new RsaSecurityKey(rsaParameters); + } + catch (Exception exception) + { + RaiseError($"{_localizedStrings.InvalidPublicKeyError}: '{exception.Message}'"); + return null; + } + } + + private ECDsaSecurityKey? GetECDsaValidationKey(TokenParameters tokenParameters) + { + try + { + AsymmetricKeyParameter? asymmetricKeyParameter = GetPublicAsymmetricKeyParameter(tokenParameters); + if (asymmetricKeyParameter is null) + { + RaiseError(_localizedStrings.InvalidPublicKeyError); + return null; + } + + var publicKey = (ECPublicKeyParameters)asymmetricKeyParameter; + if (publicKey.IsPrivate) + { + RaiseError(_localizedStrings.PublicKeyIsPrivateKeyError); + return null; + } + + ECParameters ecParameters = new() + { + Curve = ECCurve.NamedCurves.nistP256, + Q = new() + { + X = publicKey.Q.AffineXCoord.GetEncoded(), + Y = publicKey.Q.AffineYCoord.GetEncoded() + } + }; + var ecdSa = ECDsa.Create(ecParameters); + return new ECDsaSecurityKey(ecdSa); + } + catch (Exception exception) + { + RaiseError($"{_localizedStrings.InvalidPublicKeyError}: '{exception.Message}'"); + return null; + } + } + + private AsymmetricKeyParameter? GetPublicAsymmetricKeyParameter(TokenParameters tokenParameters) + { + if (string.IsNullOrWhiteSpace(tokenParameters.PublicKey)) + { + RaiseError(_localizedStrings.InvalidPublicKeyError); + return null; + } + var publicKeyStringBuilder = new StringBuilder(tokenParameters.PublicKey!.Trim()); + if (!tokenParameters.PublicKey!.StartsWith(PublicKeyStart)) + { + publicKeyStringBuilder.Insert(0, PublicKeyStart); + } + if (!tokenParameters.PublicKey.EndsWith(PublicKeyEnd)) + { + publicKeyStringBuilder.Append(PublicKeyEnd); + } + + var pemReader = new PemReader(new StringReader(publicKeyStringBuilder.ToString())); + var asymetricPublicKey = (AsymmetricKeyParameter)pemReader.ReadObject(); + if (asymetricPublicKey is null) + { + RaiseError(_localizedStrings.InvalidPublicKeyError); + return null; + } + + return asymetricPublicKey; + } + + private void RaiseError(string message) + { + var eventArg = new TokenResultErrorEventArgs + { + Message = message, + Severity = InfoBarSeverity.Error + }; + _decodingErrorCallBack!.Invoke(eventArg); + } + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControlViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControlViewModel.cs index 40880ea157..97362407df 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControlViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderControlViewModel.cs @@ -2,18 +2,15 @@ using System; using System.Composition; -using System.IdentityModel.Tokens.Jwt; -using System.Threading.Tasks; +using System.Linq; using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Tools; using DevToys.Core.Threading; -using DevToys.Helpers.JsonYaml; using DevToys.Models; +using DevToys.Models.JwtDecoderEncoder; using DevToys.Shared.Core.Threading; using DevToys.Views.Tools.EncodersDecoders.JwtDecoderEncoder; -using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Tokens; using Microsoft.Toolkit.Mvvm.Messaging; namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder @@ -21,221 +18,90 @@ namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder [Export(typeof(JwtDecoderControlViewModel))] public sealed class JwtDecoderControlViewModel : JwtDecoderEncoderViewModel, IToolViewModel, IRecipient { - private bool _validateSignature; - private bool _validateIssuer; - private bool _validateActor; - private bool _validateAudience; - private bool _validateLifetime; - - internal bool ValidateSignature - { - get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateSignature); - set - { - if (_validateSignature != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateSignature, value); - SetProperty(ref _validateSignature, value); - ShowValidation = value; - QueueNewTokenJob(); - } - } - } - - internal bool ValidateIssuer - { - get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateIssuer); - set - { - if (_validateIssuer != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateIssuer, value); - SetProperty(ref _validateIssuer, value); - QueueNewTokenJob(); - } - } - } - - internal bool ValidateActor - { - get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateActor); - set - { - if (_validateActor != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateActor, value); - SetProperty(ref _validateActor, value); - QueueNewTokenJob(); - } - } - } - - internal bool ValidateAudience - { - get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateAudience); - set - { - if (_validateAudience != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateAudience, value); - SetProperty(ref _validateAudience, value); - QueueNewTokenJob(); - } - } - } - - internal bool ValidateLifetime - { - get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateLifetime); - set - { - if (_validateLifetime != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateLifetime, value); - SetProperty(ref _validateLifetime, value); - QueueNewTokenJob(); - } - } - } + private readonly JwtDecoder _decoder; public Type View { get; } = typeof(JwtDecoderControl); [ImportingConstructor] public JwtDecoderControlViewModel( ISettingsProvider settingsProvider, - IMarketingService marketingService) + IMarketingService marketingService, + JwtDecoder decoder) : base(settingsProvider, marketingService) { IsActive = true; + _decoder = decoder; ShowValidation = ValidateSignature; } public void Receive(JwtJobAddedMessage message) { - TreatQueueAsync().Forget(); - } - - private async Task TreatQueueAsync() - { - if (WorkInProgress) + if (string.IsNullOrWhiteSpace(Token)) { return; } - WorkInProgress = true; - - await TaskScheduler.Default; - - while (JobQueue.TryDequeue(out _)) + DecoderParameters decoderParamters = new(); + if (ValidateSignature) { - DecodeToken(out string? header, out string? payload, out object? tokenAlgorithm); - ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => - { - Header = header; - Payload = payload; - - if (tokenAlgorithm is not null) - { - RequireSignature = true; - if ((JwtAlgorithm)tokenAlgorithm is - not Models.JwtAlgorithm.HS256 and - not Models.JwtAlgorithm.HS384 and - not Models.JwtAlgorithm.HS512) - { - RequireSignature = false; - } - } + decoderParamters.ValidateSignature = ValidateSignature; + decoderParamters.ValidateAudience = ValidateAudience; + decoderParamters.ValidateLifetime = ValidateLifetime; + decoderParamters.ValidateIssuer = ValidateIssuer; + decoderParamters.ValidateActor = ValidateActor; + } - if (ValidateSignature && !string.IsNullOrWhiteSpace(Token)) - { - DisplayValidationInfoBar(); - } + TokenParameters tokenParameters = new() + { + Token = Token, + Signature = Signature, + PublicKey = PublicKey, + }; - if (!ToolSuccessfullyWorked) - { - ToolSuccessfullyWorked = true; - MarketingService.NotifyToolSuccessfullyWorked(); - } - }).ForgetSafely(); + if (!string.IsNullOrEmpty(ValidIssuers)) + { + tokenParameters.ValidIssuers = ValidIssuers!.Split(',').ToHashSet(); } - WorkInProgress = false; - } - - private void DecodeToken(out string header, out string payload, out object? tokenAlgorithm) - { - if (string.IsNullOrWhiteSpace(Token)) + if (!string.IsNullOrEmpty(ValidAudiences)) { - header = string.Empty; - payload = string.Empty; - tokenAlgorithm = null; - return; + tokenParameters.ValidAudiences = ValidAudiences!.Split(',').ToHashSet(); } - try + TokenResult? result = _decoder.DecodeToken(decoderParamters, tokenParameters, TokenErrorCallBack); + + ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => { - IdentityModelEventSource.ShowPII = true; - var handler = new JwtSecurityTokenHandler(); - JwtSecurityToken jwtSecurityToken = handler.ReadJwtToken(Token); - header = JsonHelper.Format(jwtSecurityToken.Header.SerializeToJson(), Indentation.TwoSpaces, false); - payload = JsonHelper.Format(jwtSecurityToken.Payload.SerializeToJson(), Indentation.TwoSpaces, false); - Enum.TryParse(typeof(JwtAlgorithm), jwtSecurityToken.SignatureAlgorithm, out tokenAlgorithm); + if (result is null) + { + return; + } + + Header = result.Header; + Payload = result.Payload; if (ValidateSignature) { - SigningCredentials? signingCredentials = GetValidationCredentials((JwtAlgorithm)tokenAlgorithm); - - var validationParameters = new TokenValidationParameters + RequireSignature = true; + if (result.TokenAlgorithm is + not JwtAlgorithm.HS256 and + not JwtAlgorithm.HS384 and + not JwtAlgorithm.HS512) { - ValidateIssuerSigningKey = true, - IssuerSigningKey = signingCredentials.Key, - TryAllIssuerSigningKeys = true, - ValidateActor = ValidateActor, - ValidateLifetime = ValidateLifetime, - ValidateIssuer = ValidateIssuer, - ValidateAudience = ValidateAudience - }; - - if (ValidateIssuer) - { - if (string.IsNullOrWhiteSpace(ValidIssuers)) - { - JwtValidation.IsValid = false; - JwtValidation.ErrorMessage = LocalizedStrings.ValidIssuersError; - return; - } - validationParameters.ValidIssuers = ValidIssuers!.Split(','); + RequireSignature = false; } - if (ValidateAudience) - { - if (string.IsNullOrWhiteSpace(ValidAudiences)) - { - JwtValidation.IsValid = false; - JwtValidation.ErrorMessage = LocalizedStrings.ValidAudiencesError; - return; - } - validationParameters.ValidAudiences = ValidAudiences!.Split(','); - } + } - try - { - handler.ValidateToken(Token, validationParameters, out _); - JwtValidation.IsValid = true; - } - catch (Exception exception) - { - JwtValidation.IsValid = false; - JwtValidation.ErrorMessage = exception.Message; - } + DisplayValidationInfoBar(); + + + if (ToolSuccessfullyWorked) + { + ToolSuccessfullyWorked = true; + MarketingService.NotifyToolSuccessfullyWorked(); } - } - catch (Exception ex) - { - header = ex.Message; - payload = ex.Message; - tokenAlgorithm = null; - } + }).ForgetSafely(); } } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderSettings.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderSettings.cs index a9790ad832..e89ad90fe9 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderSettings.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderSettings.cs @@ -1,4 +1,6 @@ -using DevToys.Api.Core.Settings; +#nullable enable + +using DevToys.Api.Core.Settings; using DevToys.Models; namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder @@ -95,6 +97,15 @@ internal static class JwtDecoderEncoderSettings isRoaming: true, defaultValue: false); + /// + /// Define if the token has a default time + /// + public static readonly SettingDefinition ValidAudiences + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ValidAudiences)}", + isRoaming: true, + defaultValue: string.Empty); + /// /// Define if the token has a default time /// @@ -104,6 +115,15 @@ internal static class JwtDecoderEncoderSettings isRoaming: true, defaultValue: false); + /// + /// Define if the token has a default time + /// + public static readonly SettingDefinition ValidIssuers + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(ValidIssuers)}", + isRoaming: true, + defaultValue: string.Empty); + /// /// Define if the token expiration year /// @@ -157,5 +177,32 @@ internal static class JwtDecoderEncoderSettings name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(JwtAlgorithm)}", isRoaming: true, defaultValue: Models.JwtAlgorithm.HS256); + + /// + /// Define if the token expiration minutes + /// + public static readonly SettingDefinition PublicKey + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(PublicKey)}", + isRoaming: true, + defaultValue: string.Empty); + + /// + /// Define if the token expiration minutes + /// + public static readonly SettingDefinition PrivateKey + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(PrivateKey)}", + isRoaming: true, + defaultValue: string.Empty); + + /// + /// Define if the token expiration minutes + /// + public static readonly SettingDefinition Signature + = new( + name: $"{nameof(JwtDecoderEncoderViewModel)}.{nameof(Signature)}", + isRoaming: true, + defaultValue: string.Empty); } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolProvider.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolProvider.cs index 00057a0772..24c7bfbee5 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolProvider.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolProvider.cs @@ -19,9 +19,9 @@ internal sealed class JwtDecoderEncoderToolProvider : IToolProvider { private readonly IMefProvider _mefProvider; - public string MenuDisplayName => LanguageManager.Instance.JwtDecoderEncoder.DisplayName; + public string MenuDisplayName => LanguageManager.Instance.JwtDecoderEncoder.MenuDisplayName; - public string? SearchDisplayName => LanguageManager.Instance.JwtDecoderEncoder.DisplayName; + public string? SearchDisplayName => LanguageManager.Instance.JwtDecoderEncoder.SearchDisplayName; public string? Description => LanguageManager.Instance.JwtDecoderEncoder.Description; diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs index ef88a79ef4..0d87d7aa8e 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderToolViewModel.cs @@ -15,18 +15,13 @@ public sealed class JwtDecoderEncoderToolViewModel : ObservableRecipient, IToolV { private readonly ISettingsProvider _settingsProvider; - private bool _jwtToolMode; - internal bool JwtToolMode { get => _settingsProvider.GetSetting(JwtDecoderEncoderSettings.JWtToolMode); set { - if (_jwtToolMode != value) - { - _settingsProvider.SetSetting(JwtDecoderEncoderSettings.JWtToolMode, value); - SetProperty(ref _jwtToolMode, value); - } + _settingsProvider.SetSetting(JwtDecoderEncoderSettings.JWtToolMode, value); + OnPropertyChanged(); } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderViewModel.cs index cb390633b2..c7128ea4df 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtDecoderEncoderViewModel.cs @@ -3,42 +3,38 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Composition; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using DevToys.Api.Core; using DevToys.Api.Core.Settings; +using DevToys.Core; using DevToys.Helpers; using DevToys.Helpers.JsonYaml; using DevToys.Models; +using DevToys.Models.JwtDecoderEncoder; using DevToys.UI.Controls; using Microsoft.IdentityModel.Tokens; -using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.Messaging; using Microsoft.UI.Xaml.Controls; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Math; using Windows.UI.Xaml; -using YamlDotNet.Core.Tokens; using PemReader = Org.BouncyCastle.OpenSsl.PemReader; namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder { - public class JwtDecoderEncoderViewModel : ObservableRecipient + [Export(typeof(JwtDecoderEncoderViewModel))] + public class JwtDecoderEncoderViewModel : QueueWorkerViewModelBase { private string? _token; private string? _header; private string? _payload; - private string? _signature; - private string? _publicKey; - private string? _privateKey; - private string? _validIssuers; - private string? _validAudiences; - private JwtAlgorithmDisplayPair _algorithmSelected; - private bool _showValidation; private bool _requireSignature; + private JwtAlgorithmDisplayPair _algorithmSelected; private InfoBarData? _validationResult; private const string PublicKeyStart = "-----BEGIN PUBLIC KEY-----"; private const string PublicKeyEnd = "-----END PUBLIC KEY-----"; @@ -47,12 +43,13 @@ public class JwtDecoderEncoderViewModel : ObservableRecipient protected bool WorkInProgress; protected bool ToolSuccessfullyWorked; - protected ValidationBase JwtValidation = new(); protected JwtToolJobItem CurrentJobItem = new(); protected readonly Queue JobQueue = new(); protected readonly ISettingsProvider SettingsProvider; protected readonly IMarketingService MarketingService; + internal ValidationBase JwtValidation = new(); + internal JwtDecoderEncoderStrings LocalizedStrings => LanguageManager.Instance.JwtDecoderEncoder; internal RoutedEventHandler InputFocusChanged { get; } @@ -96,68 +93,114 @@ public class JwtDecoderEncoderViewModel : ObservableRecipient } } + internal bool ValidateSignature + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateSignature); + set + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateSignature, value); + OnPropertyChanged(); + ShowValidation = value; + QueueNewTokenJob(); + } + } + + internal bool ValidateIssuer + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateIssuer); + set + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateIssuer, value); + OnPropertyChanged(); + QueueNewTokenJob(); + } + } + + internal bool ValidateActor + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateActor); + set + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateActor, value); + OnPropertyChanged(); + QueueNewTokenJob(); + } + } + + internal bool ValidateAudience + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateAudience); + set + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateAudience, value); + OnPropertyChanged(); + QueueNewTokenJob(); + } + } + + internal bool ValidateLifetime + { + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidateLifetime); + set + { + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidateLifetime, value); + OnPropertyChanged(); + QueueNewTokenJob(); + } + } + internal string? ValidIssuers { - get => _validIssuers; + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidIssuers); set { - if (_validIssuers != value) - { - SetProperty(ref _validIssuers, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidIssuers, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } internal string? ValidAudiences { - get => _validAudiences; + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ValidAudiences); set { - if (_validAudiences != value) - { - SetProperty(ref _validAudiences, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ValidAudiences, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } internal string? PublicKey { - get => _publicKey; + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.PublicKey); set { - if (_publicKey != value) - { - SetProperty(ref _publicKey, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.PublicKey, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } internal string? PrivateKey { - get => _privateKey; + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.PrivateKey); set { - if (_privateKey != value) - { - SetProperty(ref _privateKey, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.PrivateKey, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } internal string? Signature { - get => _signature; + get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.Signature); set { - if (_signature != value) - { - SetProperty(ref _signature, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.Signature, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -166,12 +209,9 @@ internal bool ShowValidation get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.ShowValidation); set { - if (_showValidation != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ShowValidation, value); - SetProperty(ref _showValidation, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ShowValidation, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -235,8 +275,12 @@ internal JwtAlgorithmDisplayPair AlgorithmMode internal void QueueNewTokenJob() { - JobQueue.Enqueue(true); - Messenger.Send(new JwtJobAddedMessage()); + JwtValidation = new ValidationBase + { + IsValid = true + }; + var newJob = new JwtJobAddedMessage(); + Messenger.Send(newJob); } protected void DisplayValidationInfoBar() @@ -593,5 +637,17 @@ private void IsSignatureRequired(JwtAlgorithmDisplayPair value) RequireSignature = false; } } + + protected override void TreatComputationQueue(bool value) + { + throw new NotImplementedException(); + } + + protected void TokenErrorCallBack(TokenResultErrorEventArgs e) + { + JwtValidation.IsValid = false; + JwtValidation.ErrorMessage = e.Message; + } + //=> _validationResult = new InfoBarData(e.Severity, e.Message); } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoder.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoder.cs new file mode 100644 index 0000000000..f5250095f5 --- /dev/null +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoder.cs @@ -0,0 +1,285 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Composition; +using System.IdentityModel.Tokens.Jwt; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using DevToys.Helpers; +using DevToys.Models; +using DevToys.Models.JwtDecoderEncoder; +using Microsoft.IdentityModel.Tokens; +using Microsoft.UI.Xaml.Controls; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.OpenSsl; + +namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder +{ + [Export(typeof(JwtEncoder))] + [Shared] + public class JwtEncoder + { + private const string PrivateKeyStart = "-----BEGIN PRIVATE KEY-----"; + private const string PrivateKeyEnd = "-----END PRIVATE KEY-----"; + private Action? _encodingErrorCallBack; + private JwtDecoderEncoderStrings _localizedStrings => LanguageManager.Instance.JwtDecoderEncoder; + + public TokenResult? GenerateToken( + EncoderParameters encodeParameters, + TokenParameters tokenParameters, + Action encodingErrorCallBack) + { + if (encodeParameters is null) + { + throw new ArgumentNullException(nameof(encodeParameters)); + } + + if (tokenParameters is null) + { + throw new ArgumentNullException(nameof(tokenParameters)); + } + + if (encodingErrorCallBack is null) + { + throw new ArgumentNullException(nameof(encodingErrorCallBack)); + } + + if (string.IsNullOrWhiteSpace(tokenParameters.Payload)) + { + throw new ArgumentNullException(nameof(tokenParameters.Payload)); + } + + _encodingErrorCallBack = encodingErrorCallBack; + + var tokenResult = new TokenResult(); + + try + { + var serializeOptions = new JsonSerializerOptions(); + serializeOptions.Converters.Add(new JwtPayloadConverter()); + Dictionary? payload = JsonSerializer.Deserialize>(tokenParameters.Payload!, serializeOptions); + SigningCredentials? signingCredentials = GetSigningCredentials(tokenParameters); + + if (signingCredentials is null) + { + return null; + } + + var tokenDescriptor = new SecurityTokenDescriptor + { + Claims = payload, + SigningCredentials = signingCredentials, + Expires = null + }; + + if (encodeParameters.HasExpiration) + { + DateTime expirationDate = DateTime.Now + .AddYears(tokenParameters.ExpirationYear) + .AddMonths(tokenParameters.ExpirationMonth) + .AddDays(tokenParameters.ExpirationDay) + .AddHours(tokenParameters.ExpirationHour) + .AddMinutes(tokenParameters.ExpirationMinute); + tokenDescriptor.Expires = expirationDate; + } + + if (encodeParameters.HasAudience) + { + tokenDescriptor.Audience = string.Join(',', tokenParameters.ValidAudiences); + } + + if (encodeParameters.HasIssuer) + { + tokenDescriptor.Issuer = string.Join(',', tokenParameters.ValidIssuers); + tokenDescriptor.IssuedAt = DateTime.Now; + } + + var handler = new JwtSecurityTokenHandler + { + SetDefaultTimesOnTokenCreation = false + }; + + if (encodeParameters.HasDefaultTime) + { + handler.SetDefaultTimesOnTokenCreation = true; + tokenDescriptor.Expires = DateTime.Now.AddHours(1); + } + + SecurityToken? token = handler.CreateToken(tokenDescriptor); + tokenResult.Token = handler.WriteToken(token); + tokenResult.Payload = tokenParameters.Payload; + } + catch (Exception exception) + { + RaiseError(exception.Message); + return null; + } + + return tokenResult; + } + + private SigningCredentials GetSigningCredentials(TokenParameters tokenParameters) + { + SigningCredentials? signingCredentials = tokenParameters.TokenAlgorithm switch + { + JwtAlgorithm.ES512 => new SigningCredentials(GetECDsaSigningKey(tokenParameters), SecurityAlgorithms.EcdsaSha512Signature), + JwtAlgorithm.ES384 => new SigningCredentials(GetECDsaSigningKey(tokenParameters), SecurityAlgorithms.EcdsaSha384Signature), + JwtAlgorithm.ES256 => new SigningCredentials(GetECDsaSigningKey(tokenParameters), SecurityAlgorithms.EcdsaSha256Signature), + JwtAlgorithm.PS512 => new SigningCredentials(GetRsaShaSigningKey(tokenParameters), SecurityAlgorithms.RsaSsaPssSha512), + JwtAlgorithm.PS384 => new SigningCredentials(GetRsaShaSigningKey(tokenParameters), SecurityAlgorithms.RsaSsaPssSha384), + JwtAlgorithm.PS256 => new SigningCredentials(GetRsaShaSigningKey(tokenParameters), SecurityAlgorithms.RsaSsaPssSha256), + JwtAlgorithm.RS512 => new SigningCredentials(GetRsaShaSigningKey(tokenParameters), SecurityAlgorithms.RsaSha512Signature), + JwtAlgorithm.RS384 => new SigningCredentials(GetRsaShaSigningKey(tokenParameters), SecurityAlgorithms.RsaSha384Signature), + JwtAlgorithm.RS256 => new SigningCredentials(GetRsaShaSigningKey(tokenParameters), SecurityAlgorithms.RsaSha256Signature), + JwtAlgorithm.HS512 => new SigningCredentials(GetHmacShaSigningKey(tokenParameters), SecurityAlgorithms.HmacSha512Signature), + JwtAlgorithm.HS384 => new SigningCredentials(GetHmacShaSigningKey(tokenParameters), SecurityAlgorithms.HmacSha384Signature), + _ => new SigningCredentials(GetHmacShaSigningKey(tokenParameters), SecurityAlgorithms.HmacSha256Signature),// HS256 + }; + + return signingCredentials; + } + + private SymmetricSecurityKey? GetHmacShaSigningKey(TokenParameters tokenParameters) + { + if (string.IsNullOrWhiteSpace(tokenParameters.Signature)) + { + throw new InvalidOperationException(_localizedStrings.InvalidSignatureError); + } + + byte[]? signatureByte; + if (Base64Helper.IsBase64DataStrict(tokenParameters.Signature)) + { + signatureByte = Convert.FromBase64String(tokenParameters.Signature); + } + else + { + signatureByte = Encoding.UTF8.GetBytes(tokenParameters.Signature); + } + byte[] byteKey = tokenParameters.TokenAlgorithm switch + { + JwtAlgorithm.HS512 => new HMACSHA512(signatureByte).Key, + JwtAlgorithm.HS384 => new HMACSHA384(signatureByte).Key, + _ => new HMACSHA256(signatureByte).Key,// HS256 + }; + return new SymmetricSecurityKey(byteKey); + } + + private RsaSecurityKey? GetRsaShaSigningKey(TokenParameters tokenParameters) + { + AsymmetricKeyParameter asymmetricKeyParameter = GetPrivateAsymmetricKeyParameter(tokenParameters); + + var rsaPrivateKeyParameters = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter; + if (!rsaPrivateKeyParameters.IsPrivate) + { + throw new InvalidOperationException(_localizedStrings.InvalidPrivateKeyError); + } + + RSAParameters rsaParameters = new(); + rsaParameters.Modulus = rsaPrivateKeyParameters.Modulus.ToByteArrayUnsigned(); + rsaParameters.Exponent = rsaPrivateKeyParameters.PublicExponent.ToByteArrayUnsigned(); + rsaParameters.P = rsaPrivateKeyParameters.P.ToByteArrayUnsigned(); + rsaParameters.Q = rsaPrivateKeyParameters.Q.ToByteArrayUnsigned(); + rsaParameters.D = ConvertRSAParametersField(rsaPrivateKeyParameters.Exponent, rsaParameters.Modulus.Length); + rsaParameters.DP = ConvertRSAParametersField(rsaPrivateKeyParameters.DP, rsaParameters.P.Length); + rsaParameters.DQ = ConvertRSAParametersField(rsaPrivateKeyParameters.DQ, rsaParameters.Q.Length); + rsaParameters.InverseQ = ConvertRSAParametersField(rsaPrivateKeyParameters.QInv, rsaParameters.Q.Length); + + return new RsaSecurityKey(rsaParameters); + } + + private ECDsaSecurityKey? GetECDsaSigningKey(TokenParameters tokenParameters) + { + AsymmetricKeyParameter? asymmetricKeyParameter = GetPrivateAsymmetricKeyParameter(tokenParameters); + var ecPrivateKeyParameters = (ECPrivateKeyParameters)asymmetricKeyParameter!; + if (!ecPrivateKeyParameters.IsPrivate) + { + throw new InvalidOperationException(_localizedStrings.InvalidPrivateKeyError); + } + + ECPoint ecPoint = new() + { + X = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded(), + Y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded() + }; + ECParameters ecParameters = new() + { + Curve = ECCurve.NamedCurves.nistP256, + Q = ecPoint, + D = ecPrivateKeyParameters.D.ToByteArrayUnsigned() + }; + + var ecdSa = ECDsa.Create(ecParameters); + return new ECDsaSecurityKey(ecdSa); + } + + private AsymmetricKeyParameter GetPrivateAsymmetricKeyParameter(TokenParameters tokenParameters) + { + if (string.IsNullOrWhiteSpace(tokenParameters.PrivateKey)) + { + throw new InvalidOperationException(_localizedStrings.InvalidPrivateKeyError); + } + var privateKeyStringBuilder = new StringBuilder(tokenParameters.PrivateKey!.Trim()); + if (!tokenParameters.PrivateKey!.StartsWith(PrivateKeyStart)) + { + privateKeyStringBuilder.Insert(0, PrivateKeyStart); + } + if (!tokenParameters.PrivateKey.EndsWith(PrivateKeyEnd)) + { + privateKeyStringBuilder.Append(PrivateKeyEnd); + } + + var pemReader = new PemReader(new StringReader(privateKeyStringBuilder.ToString())); + object? pemObject = pemReader.ReadObject(); + if (pemObject is null) + { + throw new InvalidOperationException(_localizedStrings.InvalidPrivateKeyError); + } + + if (pemObject is AsymmetricKeyParameter parameter) + { + return parameter; + } + else if (pemObject is AsymmetricCipherKeyPair) + { + var pair = pemObject as AsymmetricCipherKeyPair; + return pair!.Private; + } + + throw new InvalidOperationException(_localizedStrings.InvalidPrivateKeyError); + } + + /// + /// Source (https://stackoverflow.com/questions/28370414/import-rsa-key-from-bouncycastle-sometimes-throws-bad-data) + /// + private static byte[] ConvertRSAParametersField(BigInteger n, int size) + { + byte[] bs = n.ToByteArrayUnsigned(); + if (bs.Length == size) + { + return bs; + } + if (bs.Length > size) + { + throw new ArgumentException("Specified size too small", "size"); + } + byte[] padded = new byte[size]; + Array.Copy(bs, 0, padded, size - bs.Length, bs.Length); + return padded; + } + + private void RaiseError(string message) + { + var eventArg = new TokenResultErrorEventArgs + { + Message = message, + Severity = InfoBarSeverity.Error + }; + _encodingErrorCallBack!.Invoke(eventArg); + } + } +} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControlViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControlViewModel.cs index c2f589c75e..e23d66585d 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControlViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtEncoderControlViewModel.cs @@ -3,17 +3,15 @@ using System; using System.Collections.Generic; using System.Composition; -using System.IdentityModel.Tokens.Jwt; -using System.Text.Json; -using System.Threading.Tasks; +using System.Linq; using DevToys.Api.Core; using DevToys.Api.Core.Settings; using DevToys.Api.Tools; using DevToys.Core.Threading; using DevToys.Models; +using DevToys.Models.JwtDecoderEncoder; using DevToys.Shared.Core.Threading; using DevToys.Views.Tools.EncodersDecoders.JwtDecoderEncoder; -using Microsoft.IdentityModel.Tokens; using Microsoft.Toolkit.Mvvm.Messaging; namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder @@ -22,16 +20,9 @@ namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder public sealed class JwtEncoderControlViewModel : JwtDecoderEncoderViewModel, IToolViewModel, IRecipient { private string? _token; - private bool _hasExpiration; - private bool _hasDefaultTime; - private bool _hasAudience; - private bool _hasIssuer; private bool _hasError; - private int _expireYear = 0; - private int _expireMonth = 0; - private int _expireDay = 0; - private int _expireHour = 0; - private int _expireMinute = 0; + + private readonly JwtEncoder _encoder; internal override string? Token { @@ -50,12 +41,9 @@ internal bool HasExpiration get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.HasExpiration); set { - if (_hasExpiration != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasExpiration, value); - SetProperty(ref _hasExpiration, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasExpiration, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -64,12 +52,9 @@ internal bool HasAudience get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.HasAudience); set { - if (_hasAudience != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasAudience, value); - SetProperty(ref _hasAudience, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasAudience, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -78,12 +63,9 @@ internal bool HasIssuer get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.HasIssuer); set { - if (_hasIssuer != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasIssuer, value); - SetProperty(ref _hasIssuer, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasIssuer, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -92,12 +74,9 @@ internal bool HasDefaultTime get => SettingsProvider.GetSetting(JwtDecoderEncoderSettings.HasDefaultTime); set { - if (_hasDefaultTime != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasDefaultTime, value); - SetProperty(ref _hasDefaultTime, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.HasDefaultTime, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -119,12 +98,9 @@ internal int ExpireYear { return; } - if (_expireYear != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireYear, value); - SetProperty(ref _expireYear, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireYear, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -137,12 +113,9 @@ internal int ExpireMonth { return; } - if (_expireMonth != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireMonth, value); - SetProperty(ref _expireMonth, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireMonth, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -155,12 +128,9 @@ internal int ExpireDay { return; } - if (_expireDay != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireDay, value); - SetProperty(ref _expireDay, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireDay, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -174,12 +144,9 @@ internal int ExpireHour { return; } - if (_expireHour != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireHour, value); - SetProperty(ref _expireHour, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireHour, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -192,12 +159,9 @@ internal int ExpireMinute { return; } - if (_expireMinute != value) - { - SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireMinute, value); - SetProperty(ref _expireMinute, value); - QueueNewTokenJob(); - } + SettingsProvider.SetSetting(JwtDecoderEncoderSettings.ExpireMinute, value); + OnPropertyChanged(); + QueueNewTokenJob(); } } @@ -206,105 +170,76 @@ internal int ExpireMinute [ImportingConstructor] public JwtEncoderControlViewModel( ISettingsProvider settingsProvider, - IMarketingService marketingService) + IMarketingService marketingService, + JwtEncoder encoder) : base(settingsProvider, marketingService) { IsActive = true; + _encoder = encoder; } public void Receive(JwtJobAddedMessage message) { - TreatQueueAsync().Forget(); - } - - private async Task TreatQueueAsync() - { - if (WorkInProgress) + if (string.IsNullOrWhiteSpace(Payload)) { return; } - WorkInProgress = true; - - await TaskScheduler.Default; + EncoderParameters encoderParameters = new() + { + HasAudience = HasAudience, + HasExpiration = HasExpiration, + HasIssuer = HasIssuer, + HasDefaultTime = HasDefaultTime + }; - while (JobQueue.TryDequeue(out _)) + TokenParameters tokenParameters = new() { - GenerateToken(out string? tokenString, AlgorithmMode.Value); - ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => - { - Token = tokenString; + TokenAlgorithm = AlgorithmMode.Value, + Payload = Payload, + ExpirationYear = ExpireYear, + ExpirationMonth = ExpireMonth, + ExpirationDay = ExpireDay, + ExpirationHour = ExpireHour, + ExpirationMinute = ExpireMinute + }; - if (!ToolSuccessfullyWorked) - { - ToolSuccessfullyWorked = true; - MarketingService.NotifyToolSuccessfullyWorked(); - } - }).ForgetSafely(); + if (!string.IsNullOrEmpty(ValidIssuers)) + { + tokenParameters.ValidIssuers = ValidIssuers!.Split(',').ToHashSet(); } - WorkInProgress = false; - } - - private void GenerateToken(out string tokenString, JwtAlgorithm algorithmMode) - { - if (string.IsNullOrWhiteSpace(Header) || string.IsNullOrWhiteSpace(Payload)) + if (!string.IsNullOrEmpty(ValidAudiences)) { - tokenString = string.Empty; - return; + tokenParameters.ValidAudiences = ValidAudiences!.Split(',').ToHashSet(); } - try + if (AlgorithmMode.Value is JwtAlgorithm.HS256 || + AlgorithmMode.Value is JwtAlgorithm.HS384 || + AlgorithmMode.Value is JwtAlgorithm.HS512) { - var serializeOptions = new JsonSerializerOptions(); - serializeOptions.Converters.Add(new JwtPayloadConverter()); - Dictionary? payload = JsonSerializer.Deserialize>(Payload!, serializeOptions); - SigningCredentials? signingCredentials = GetSigningCredentials(algorithmMode); - - var tokenDescriptor = new SecurityTokenDescriptor - { - Claims = payload, - SigningCredentials = signingCredentials, - Expires = null - }; - - if (HasExpiration) - { - DateTime expirationDate = DateTime.UtcNow - .AddYears(ExpireYear) - .AddMonths(ExpireMonth) - .AddDays(ExpireDay) - .AddHours(ExpireHour) - .AddMinutes(ExpireMinute); - tokenDescriptor.Expires = expirationDate; - } + tokenParameters.Signature = Signature; + } + else + { + tokenParameters.PrivateKey = PrivateKey; + } - if (HasAudience) - { - tokenDescriptor.Audience = ValidAudiences; - } + TokenResult? result = _encoder.GenerateToken(encoderParameters, tokenParameters, TokenErrorCallBack); - if (HasIssuer) + ThreadHelper.RunOnUIThreadAsync(ThreadPriority.Low, () => + { + if (result is not null) { - tokenDescriptor.Issuer = ValidIssuers; - tokenDescriptor.IssuedAt = DateTime.UtcNow; + Token = result.Token; } - - var handler = new JwtSecurityTokenHandler(); - if (!HasDefaultTime) + HasError = JwtValidation.IsValid!; + if (ToolSuccessfullyWorked) { - handler.SetDefaultTimesOnTokenCreation = false; + ToolSuccessfullyWorked = true; + MarketingService.NotifyToolSuccessfullyWorked(); } - - SecurityToken? token = handler.CreateToken(tokenDescriptor); - tokenString = handler.WriteToken(token); - } - catch (Exception exception) - { - JwtValidation.IsValid = false; - JwtValidation.ErrorMessage = exception.Message; - tokenString = string.Empty; - } + }).ForgetSafely(); } } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtJobAddedMessage.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtJobAddedMessage.cs index 276430957f..9b593cef77 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtJobAddedMessage.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtJobAddedMessage.cs @@ -1,11 +1,8 @@ -using Windows.UI.Xaml.Documents; +#nullable enable namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder { public sealed class JwtJobAddedMessage { - public bool IsEncoding { get; set; } - - public bool IsDecoding { get; set; } } } diff --git a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolSwitchedMessage.cs b/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolSwitchedMessage.cs deleted file mode 100644 index f53b8d06a1..0000000000 --- a/src/dev/impl/DevToys/ViewModels/Tools/EncodersDecoders/JwtDecoderEncoder/JwtToolSwitchedMessage.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DevToys.ViewModels.Tools.EncodersDecoders.JwtDecoderEncoder -{ - public sealed class JwtToolSwitchedMessage - { - public JwtToolSwitchedMessage() - { } - } -} diff --git a/src/dev/impl/DevToys/ViewModels/Tools/Generators/HashGenerator/HashGeneratorToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/Generators/HashGenerator/HashGeneratorToolViewModel.cs index 178588d3a5..83af62fc78 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/Generators/HashGenerator/HashGeneratorToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/Generators/HashGenerator/HashGeneratorToolViewModel.cs @@ -18,7 +18,7 @@ namespace DevToys.ViewModels.Tools.HashGenerator { [Export(typeof(HashGeneratorToolViewModel))] - public sealed class HashGeneratorToolViewModel : QueueWorkerViewModelBase>, IToolViewModel + public sealed class HashGeneratorToolViewModel : QueueWorkerViewModelBaseAsync>, IToolViewModel { /// /// Whether the generated hash should be uppercase or lowercase. diff --git a/src/dev/impl/DevToys/ViewModels/Tools/Text/StringUtilities/StringUtilitiesToolViewModel.cs b/src/dev/impl/DevToys/ViewModels/Tools/Text/StringUtilities/StringUtilitiesToolViewModel.cs index 85af364248..f2bd087040 100644 --- a/src/dev/impl/DevToys/ViewModels/Tools/Text/StringUtilities/StringUtilitiesToolViewModel.cs +++ b/src/dev/impl/DevToys/ViewModels/Tools/Text/StringUtilities/StringUtilitiesToolViewModel.cs @@ -18,7 +18,7 @@ namespace DevToys.ViewModels.Tools.StringUtilities { [Export(typeof(StringUtilitiesToolViewModel))] - public sealed class StringUtilitiesToolViewModel : QueueWorkerViewModelBase, IToolViewModel + public sealed class StringUtilitiesToolViewModel : QueueWorkerViewModelBaseAsync, IToolViewModel { private static readonly object _lockObject = new(); diff --git a/src/tests/DevToys.Tests/DevToys.Tests.csproj b/src/tests/DevToys.Tests/DevToys.Tests.csproj index 6fe459e517..77deb8c53e 100644 --- a/src/tests/DevToys.Tests/DevToys.Tests.csproj +++ b/src/tests/DevToys.Tests/DevToys.Tests.csproj @@ -44,6 +44,8 @@ + + @@ -55,9 +57,12 @@ + + + @@ -85,6 +90,21 @@ + + + + + + + + + + + + + + + @@ -108,6 +128,15 @@ DevToys.Shared + + + + + + + + +