├── icon-128.png ├── SpawnDev.BlazorJS.Cryptography.Demo ├── SpawnDev.BlazorJS.Cryptography.Demo │ ├── appsettings.Development.json │ ├── wwwroot │ │ ├── favicon.png │ │ └── app.css │ ├── appsettings.json │ ├── Components │ │ ├── Routes.razor │ │ ├── _Imports.razor │ │ ├── Layout │ │ │ ├── MainLayout.razor │ │ │ ├── NavMenu.razor │ │ │ ├── MainLayout.razor.css │ │ │ └── NavMenu.razor.css │ │ ├── App.razor │ │ └── Pages │ │ │ ├── Error.razor │ │ │ └── Weather.razor │ ├── SpawnDev.BlazorJS.Cryptography.Demo.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ └── Controllers │ │ └── CryptographyTestController.cs └── SpawnDev.BlazorJS.Cryptography.Demo.Client │ ├── wwwroot │ ├── appsettings.json │ └── appsettings.Development.json │ ├── _Imports.razor │ ├── SpawnDev.BlazorJS.Cryptography.Demo.Client.csproj │ ├── Program.cs │ └── Pages │ └── TestPage.razor ├── SpawnDev.BlazorJS.Cryptography ├── DotNetCrypto.cs ├── Base │ ├── PortableECKey.cs │ ├── AESCBCPadding.cs │ ├── PortableECDSAKey.cs │ ├── PortableECDHKey.cs │ ├── PortableAESCBCKey.cs │ ├── PortableAESGCMKey.cs │ ├── PortableKey.cs │ ├── IPortableCrypto.cs │ └── PortableCrypto.cs ├── BrowserWASM │ ├── BrowserWASMCryptoSHA.cs │ ├── BrowserWASMCryptoRandom.cs │ ├── BrowserWASMAESCBCKey.cs │ ├── BrowserWASMAESGCMKey.cs │ ├── BrowserWASMECDHKey.cs │ ├── BrowserWASMECDSAKey.cs │ ├── BrowserWASMCryptoECDSA.cs │ ├── BrowserWASMCryptoECDH.cs │ ├── BrowserWASMCryptoAESGCM.cs │ └── BrowserWASMCryptoAESCBC.cs ├── Browser │ ├── BrowserCryptoSHA.cs │ ├── BrowserAESCBCKey.cs │ ├── BrowserECDHKey.cs │ ├── BrowserECDSAKey.cs │ ├── BrowserCryptoRandom.cs │ ├── BrowserAESGCMKey.cs │ ├── BrowserCryptoECDSA.cs │ ├── BrowserCryptoECDH.cs │ ├── BrowserCryptoAESGCM.cs │ └── BrowserCryptoAESCBC.cs ├── BrowserCrypto.cs ├── BrowserWASMCrypto.cs ├── DotNet │ ├── DotNetAESCBCKey.cs │ ├── DotNetECDSAKey.cs │ ├── DotNetCryptoRandom.cs │ ├── DotNetECDHKey.cs │ ├── DotNetAESGCMKey.cs │ ├── DotNetCryptoSHA.cs │ ├── DotNetCryptoECDSA.cs │ ├── DotNetCryptoECDH.cs │ ├── DotNetCryptoAESCBC.cs │ └── DotNetCryptoAESGCM.cs ├── Extensions.cs └── SpawnDev.BlazorJS.Cryptography.csproj ├── .dockerignore ├── LICENSE.txt ├── SpawnDev.BlazorJS.Cryptography.sln ├── .gitattributes ├── .gitignore └── README.md /icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.Cryptography/main/icon-128.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo.Client/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LostBeard/SpawnDev.BlazorJS.Cryptography/main/SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/wwwroot/favicon.png -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo.Client/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNetCrypto.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography 4 | { 5 | /// 6 | /// Cross platform cryptography tools.
7 | /// DotNetCrypto uses System.Security.Cryptography. Supports windows and linux platforms.
8 | ///
9 | public partial class DotNetCrypto : PortableCrypto 10 | { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/Routes.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/PortableECKey.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | /// 4 | /// Elliptic curve key abstract class 5 | /// 6 | public abstract class PortableECKey : PortableKey 7 | { 8 | /// 9 | /// The named curve 10 | /// 11 | public virtual string NamedCurve { get; protected set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/AESCBCPadding.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | /// 4 | /// AES-CBC padding 5 | /// 6 | public enum AESCBCPadding 7 | { 8 | /// 9 | /// No padding 10 | /// 11 | None, 12 | /// 13 | /// PKCS7 padding 14 | /// 15 | PKCS7, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using SpawnDev.BlazorJS.Cryptography.Demo.Client 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | !**/.gitignore 27 | !.git/HEAD 28 | !.git/config 29 | !.git/packed-refs 30 | !.git/refs/heads/** -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/PortableECDSAKey.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | /// 4 | /// ECDSA abstract class 5 | /// 6 | public abstract class PortableECDSAKey : PortableECKey 7 | { 8 | /// 9 | /// Key algorithm 10 | /// 11 | public override string AlgorithmName { get; } = "ECDSA"; 12 | /// 13 | /// Key usages 14 | /// 15 | public override string[] Usages { get; protected set; } = new string[] { "sign", "verify" }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/PortableECDHKey.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | /// 4 | /// ECDH key abstract class 5 | /// 6 | public abstract class PortableECDHKey : PortableECKey 7 | { 8 | /// 9 | /// Key algorithm 10 | /// 11 | public override string AlgorithmName { get; } = "ECDH"; 12 | /// 13 | /// Key usages 14 | /// 15 | public override string[] Usages { get; protected set; } = new string[] { "deriveBits", "deriveKey" }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using SpawnDev.BlazorJS.Cryptography.Demo 10 | @using SpawnDev.BlazorJS.Cryptography.Demo.Client 11 | @using SpawnDev.BlazorJS.Cryptography.Demo.Components 12 | @using SpawnDev.Blazor.UnitTesting 13 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
24 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMCryptoSHA.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | public partial class BrowserWASMCrypto 4 | { 5 | /// 6 | /// Hash the specified data using the specified hash algorithm 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | public override async Task Digest(string hashName, byte[] data) 13 | { 14 | using var arrayBuffer = await SubtleCrypto!.Digest(hashName, data); 15 | return arrayBuffer.ReadBytes(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/PortableAESCBCKey.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | /// 4 | /// AES-CBC key abstract class 5 | /// 6 | public abstract class PortableAESCBCKey : PortableKey 7 | { 8 | /// 9 | /// Key algorithm 10 | /// 11 | public override string AlgorithmName { get; } = "AES-CBC"; 12 | /// 13 | /// Key usages 14 | /// 15 | public override string[] Usages { get; protected set; } = new string[] { "encrypt", "decrypt" }; 16 | /// 17 | /// The key size 18 | /// 19 | public virtual int KeySize { get; protected set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserCryptoSHA.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | public partial class BrowserCrypto 4 | { 5 | /// 6 | /// Hash the specified data using the specified hash algorithm 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | public override async Task Digest(string hashName, byte[] data) 13 | { 14 | await using var arrayBuffer = await SubtleCrypto.Digest(hashName, data); 15 | var hash = await arrayBuffer.ReadBytes(); 16 | return hash; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo.Client/SpawnDev.BlazorJS.Cryptography.Demo.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | Default 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/PortableAESGCMKey.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | /// 4 | /// AES-GCM key abstract class 5 | /// 6 | public abstract class PortableAESGCMKey : PortableKey 7 | { 8 | /// 9 | /// Key algorithm 10 | /// 11 | public override string AlgorithmName { get; } = "AES-GCM"; 12 | /// 13 | /// Key usages 14 | /// 15 | public override string[] Usages { get; protected set; } = new string[] { "encrypt", "decrypt" }; 16 | /// 17 | /// Tag size used for encryption and decryption 18 | /// 19 | public virtual int TagSizeBytes { get; protected set; } 20 | /// 21 | /// Nonce size used for encryption and decryption 22 | /// 23 | public virtual int NonceSizeBytes { get; protected set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserCrypto.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using SpawnDev.BlazorJS.RemoteJSRuntime.AsyncObjects; 3 | 4 | namespace SpawnDev.BlazorJS.Cryptography 5 | { 6 | /// 7 | /// Cross platform cryptography tools.
8 | /// BrowserCrypto uses the web browser's SubtleCrypto API. Requires IJSRuntime and supports both server side rendering and webassembly rendering.
9 | ///
10 | public partial class BrowserCrypto : PortableCrypto 11 | { 12 | /// 13 | /// The JS runtime 14 | /// 15 | protected IJSRuntime JSA { get; set; } 16 | SubtleCryptoAsync SubtleCrypto { get; } 17 | /// 18 | /// Creates a new instance 19 | /// 20 | /// 21 | public BrowserCrypto(IJSRuntime jsa) 22 | { 23 | JSA = jsa; 24 | SubtleCrypto = new SubtleCryptoAsync(JSA); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 2 | using Microsoft.JSInterop; 3 | using SpawnDev.BlazorJS; 4 | using SpawnDev.BlazorJS.Cryptography; 5 | 6 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 7 | 8 | var temp = typeof(JSInProcessRuntime).GetMethod("Invoke", new Type[] { typeof(string), typeof(object[]) }); 9 | 10 | // Add BlazorJSRuntime service 11 | builder.Services.AddBlazorJSRuntime(); 12 | 13 | // Crypto for the browser 14 | builder.Services.AddScoped(); 15 | 16 | // Crypto for the browser (wasm only. on wasm blazor it can be used as an alternative to BrowserCrypto) 17 | builder.Services.AddScoped(); 18 | 19 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 20 | 21 | // build and start the app 22 | var host = await builder.Build().StartBackgroundServices(); 23 | 24 | await host.BlazorJSRunAsync(); 25 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserAESCBCKey.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.RemoteJSRuntime.AsyncObjects; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.Browser 4 | { 5 | /// 6 | /// Browser platform AES-CBC key 7 | /// 8 | public class BrowserAESCBCKey : PortableAESCBCKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public CryptoKeyAsync Key { get; protected set; } 14 | /// 15 | /// Create a new instance 16 | /// 17 | public BrowserAESCBCKey(CryptoKeyAsync key, int keySize, bool extractable) 18 | { 19 | Key = key; 20 | KeySize = keySize; 21 | Extractable = extractable; 22 | } 23 | /// 24 | /// Dispose instance resources 25 | /// 26 | public override async ValueTask DisposeAsync() 27 | { 28 | if (Key != null) await Key.DisposeAsync(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASMCrypto.cs: -------------------------------------------------------------------------------- 1 | #if NET6_0_OR_GREATER 2 | using SpawnDev.BlazorJS.JSObjects; 3 | using System.Runtime.Versioning; 4 | 5 | namespace SpawnDev.BlazorJS.Cryptography 6 | { 7 | /// 8 | /// Cross platform cryptography tools.
9 | /// BrowserWASMCrypto uses the web browser's SubtleCrypto API. Requires IJInProcessSRuntime and supports only webassembly rendering.
10 | ///
11 | public partial class BrowserWASMCrypto : PortableCrypto 12 | { 13 | BlazorJSRuntime JS { get; set; } 14 | Lazy _SubtleCrypto; 15 | SubtleCrypto SubtleCrypto => _SubtleCrypto.Value; 16 | /// 17 | /// Creates a new instance 18 | /// 19 | /// 20 | [SupportedOSPlatform("browser")] 21 | public BrowserWASMCrypto(BlazorJSRuntime js) 22 | { 23 | JS = js; 24 | _SubtleCrypto = new Lazy(() => JS.Get("crypto.subtle")); 25 | } 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserECDHKey.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.RemoteJSRuntime.AsyncObjects; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.Browser 4 | { 5 | /// 6 | /// Browser platform ECDH key 7 | /// 8 | public class BrowserECDHKey : PortableECDHKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public CryptoKeyPairAsync Key { get; protected set; } 14 | /// 15 | /// Create a new instance 16 | /// 17 | public BrowserECDHKey(CryptoKeyPairAsync key, string namedCurve, bool extractable, string[] usages) 18 | { 19 | Key = key; 20 | NamedCurve = namedCurve; 21 | Extractable = extractable; 22 | Usages = usages; 23 | } 24 | /// 25 | /// Dispose instance resources 26 | /// 27 | public override async ValueTask DisposeAsync() 28 | { 29 | if (Key != null) await Key.DisposeAsync(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserECDSAKey.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.RemoteJSRuntime.AsyncObjects; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.Browser 4 | { 5 | /// 6 | /// Browser platform ECDSA key 7 | /// 8 | public class BrowserECDSAKey : PortableECDSAKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public CryptoKeyPairAsync Key { get; protected set; } 14 | /// 15 | /// Create a new instance 16 | /// 17 | public BrowserECDSAKey(CryptoKeyPairAsync key, string namedCurve, bool extractable, string[] usages) 18 | { 19 | Key = key; 20 | NamedCurve = namedCurve; 21 | Extractable = extractable; 22 | Usages = usages; 23 | } 24 | /// 25 | /// Dispose instance resources 26 | /// 27 | public override async ValueTask DisposeAsync() 28 | { 29 | if (Key != null) await Key.DisposeAsync(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetAESCBCKey.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.DotNet 4 | { 5 | /// 6 | /// Windows, Linux platform AES-CBC key 7 | /// 8 | public class DotNetAESCBCKey : PortableAESCBCKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public Aes Key { get; protected set; } 14 | /// 15 | /// The key size 16 | /// 17 | public override int KeySize => Key?.KeySize ?? 0; 18 | /// 19 | /// Creates a new instance 20 | /// 21 | /// 22 | public DotNetAESCBCKey(Aes key) 23 | { 24 | Key = key; 25 | } 26 | /// 27 | /// Dispose instance resources 28 | /// 29 | protected override void Dispose(bool disposing) 30 | { 31 | if (disposing) 32 | { 33 | Key?.Dispose(); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetECDSAKey.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Security.Cryptography; 3 | 4 | namespace SpawnDev.BlazorJS.Cryptography.DotNet 5 | { 6 | /// 7 | /// Windows, Linux platform ECDSA key 8 | /// 9 | public class DotNetECDSAKey : PortableECDSAKey 10 | { 11 | /// 12 | /// The platform specific key 13 | /// 14 | public ECDsa Key { get; protected set; } 15 | /// 16 | /// The named curve 17 | /// 18 | public override string NamedCurve => $"P-{Key.KeySize}"; 19 | /// 20 | /// Create a new instance 21 | /// 22 | /// 23 | public DotNetECDSAKey(ECDsa key) 24 | { 25 | Key = key; 26 | } 27 | /// 28 | /// Dispose instance resources 29 | /// 30 | protected override void Dispose(bool disposing) 31 | { 32 | if (disposing) 33 | { 34 | Key?.Dispose(); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetCryptoRandom.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography 4 | { 5 | public partial class DotNetCrypto 6 | { 7 | /// 8 | /// Returns a bye array with the specified number of random bytes 9 | /// 10 | /// 11 | /// 12 | public override byte[] RandomBytes(int length) 13 | { 14 | return RandomNumberGenerator.GetBytes(length); 15 | } 16 | /// 17 | /// Fill the byte array with random data 18 | /// 19 | /// 20 | public override void RandomBytesFill(byte[] data) 21 | { 22 | RandomNumberGenerator.Fill(data); 23 | } 24 | /// 25 | /// Fill the byte span with random data 26 | /// 27 | /// 28 | public override void RandomBytesFill(Span data) 29 | { 30 | RandomNumberGenerator.Fill(data); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserCryptoRandom.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography 4 | { 5 | public partial class BrowserCrypto 6 | { 7 | /// 8 | /// Returns a bye array with the specified number of random bytes 9 | /// 10 | /// 11 | /// 12 | public override byte[] RandomBytes(int length) 13 | { 14 | return RandomNumberGenerator.GetBytes(length); 15 | } 16 | /// 17 | /// Fill the byte array with random data 18 | /// 19 | /// 20 | public override void RandomBytesFill(byte[] data) 21 | { 22 | RandomNumberGenerator.Fill(data); 23 | } 24 | /// 25 | /// Fill the byte span with random data 26 | /// 27 | /// 28 | public override void RandomBytesFill(Span data) 29 | { 30 | RandomNumberGenerator.Fill(data); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetECDHKey.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Security.Cryptography; 3 | 4 | namespace SpawnDev.BlazorJS.Cryptography.DotNet 5 | { 6 | /// 7 | /// Windows, Linux platform ECDH key 8 | /// 9 | public class DotNetECDHKey : PortableECDHKey 10 | { 11 | /// 12 | /// The platform specific key 13 | /// 14 | public ECDiffieHellman Key { get; protected set; } 15 | /// 16 | /// The named curve 17 | /// 18 | public override string NamedCurve => $"P-{Key.KeySize}"; 19 | /// 20 | /// Create a new instance 21 | /// 22 | /// 23 | public DotNetECDHKey(ECDiffieHellman key) 24 | { 25 | Key = key; 26 | } 27 | /// 28 | /// Dispose instance resources 29 | /// 30 | protected override void Dispose(bool disposing) 31 | { 32 | if (disposing) 33 | { 34 | Key?.Dispose(); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMCryptoRandom.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography 4 | { 5 | public partial class BrowserWASMCrypto 6 | { 7 | /// 8 | /// Returns a bye array with the specified number of random bytes 9 | /// 10 | /// 11 | /// 12 | public override byte[] RandomBytes(int length) 13 | { 14 | return RandomNumberGenerator.GetBytes(length); 15 | } 16 | /// 17 | /// Fill the byte array with random data 18 | /// 19 | /// 20 | public override void RandomBytesFill(byte[] data) 21 | { 22 | RandomNumberGenerator.Fill(data); 23 | } 24 | /// 25 | /// Fill the byte span with random data 26 | /// 27 | /// 28 | public override void RandomBytesFill(Span data) 29 | { 30 | RandomNumberGenerator.Fill(data); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserAESGCMKey.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.RemoteJSRuntime.AsyncObjects; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.Browser 4 | { 5 | /// 6 | /// Browser platform AES-GCM key 7 | /// 8 | public class BrowserAESGCMKey : PortableAESGCMKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public CryptoKeyAsync Key { get; protected set; } 14 | /// 15 | /// Create a new instance 16 | /// 17 | public BrowserAESGCMKey(CryptoKeyAsync key, int nonceSizeBytes, int tagSizeBytes, bool extractable, string[] usages) 18 | { 19 | Key = key; 20 | NonceSizeBytes = nonceSizeBytes; 21 | TagSizeBytes = tagSizeBytes; 22 | Extractable = extractable; 23 | Usages = usages; 24 | } 25 | /// 26 | /// Dispose instance resources 27 | /// 28 | public override async ValueTask DisposeAsync() 29 | { 30 | if (Key != null) await Key.DisposeAsync(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | 4 | namespace SpawnDev.BlazorJS.Cryptography 5 | { 6 | /// 7 | /// Extension methods 8 | /// 9 | public static class Extensions 10 | { 11 | /// 12 | /// Adds the IPortableCrypto service singleton based on the running platform.
13 | /// If running in the browser, BrowserWASMCrypto is used,
14 | /// otherwise DotNetCrypto is used.
15 | /// NOTE: Use BrowserCrypto directly if you need client side crypto with ServerSide rendering. 16 | ///
17 | /// 18 | /// 19 | public static IServiceCollection AddPlatformCrypto(this IServiceCollection services) 20 | { 21 | if (OperatingSystem.IsBrowser()) 22 | { 23 | services.TryAddSingleton(); 24 | } 25 | else 26 | { 27 | services.TryAddSingleton(); 28 | } 29 | return services; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetAESGCMKey.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.DotNet 4 | { 5 | /// 6 | /// Windows, Linux platform AES-GCM key 7 | /// 8 | public class DotNetAESGCMKey : PortableAESGCMKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public AesGcm Key { get; protected set; } 14 | /// 15 | /// The key tag size 16 | /// 17 | public override int TagSizeBytes => Key?.TagSizeInBytes ?? 0; 18 | /// 19 | /// Creates a new instance 20 | /// 21 | /// 22 | /// 23 | public DotNetAESGCMKey(AesGcm key, int nonceSizeBytes) 24 | { 25 | Key = key; 26 | NonceSizeBytes = nonceSizeBytes; 27 | } 28 | /// 29 | /// Dispose instance resources 30 | /// 31 | protected override void Dispose(bool disposing) 32 | { 33 | if (disposing) 34 | { 35 | Key?.Dispose(); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMAESCBCKey.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.BrowserWASM 4 | { 5 | /// 6 | /// Browser platform AES-CBC key 7 | /// 8 | public class BrowserWASMAESCBCKey : PortableAESCBCKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public CryptoKey Key { get; protected set; } 14 | /// 15 | /// Returns true if the private key can be extracted 16 | /// 17 | public override bool Extractable => Key?.Extractable ?? false; 18 | /// 19 | /// Key usages 20 | /// 21 | public override string[] Usages => Key?.Usages ?? System.Array.Empty(); 22 | /// 23 | /// Create a new instance 24 | /// 25 | public BrowserWASMAESCBCKey(CryptoKey key) 26 | { 27 | Key = key; 28 | var algorithmParams = key.AlgorithmAs(); 29 | KeySize = algorithmParams.Length; 30 | } 31 | /// 32 | /// Dispose instance resources 33 | /// 34 | protected override void Dispose(bool disposing) 35 | { 36 | if (disposing) 37 | { 38 | Key?.Dispose(); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMAESGCMKey.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.BrowserWASM 4 | { 5 | /// 6 | /// Browser platform AES-GCM key 7 | /// 8 | public class BrowserWASMAESGCMKey : PortableAESGCMKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public CryptoKey Key { get; protected set; } 14 | /// 15 | /// Returns true if the private key can be extracted 16 | /// 17 | public override bool Extractable => Key?.Extractable ?? false; 18 | /// 19 | /// Key usages 20 | /// 21 | public override string[] Usages => Key?.Usages ?? System.Array.Empty(); 22 | /// 23 | /// Create a new instance 24 | /// 25 | /// 26 | /// 27 | /// 28 | public BrowserWASMAESGCMKey(CryptoKey key, int nonceSizeBytes, int tagSizeBytes) 29 | { 30 | Key = key; 31 | NonceSizeBytes = nonceSizeBytes; 32 | TagSizeBytes = tagSizeBytes; 33 | } 34 | /// 35 | /// Dispose instance resources 36 | /// 37 | protected override void Dispose(bool disposing) 38 | { 39 | if (disposing) 40 | { 41 | Key?.Dispose(); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS; 2 | using SpawnDev.BlazorJS.Cryptography; 3 | using SpawnDev.BlazorJS.Cryptography.Demo.Components; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // Add BlazorJSRuntime service 8 | builder.Services.AddBlazorJSRuntime(); 9 | 10 | // Crypto for the server. Uses System.Security.Cryptography. 11 | builder.Services.AddSingleton(); 12 | 13 | // Crypto for the browser. Uses the browser's SubtleCrypto API. 14 | // Used on server for server side rendering 15 | builder.Services.AddScoped(); 16 | 17 | // Add services to the container. 18 | builder.Services.AddRazorComponents() 19 | .AddInteractiveServerComponents() 20 | .AddInteractiveWebAssemblyComponents(); 21 | 22 | builder.Services.AddControllers(); 23 | 24 | var app = builder.Build(); 25 | 26 | // Configure the HTTP request pipeline. 27 | if (app.Environment.IsDevelopment()) 28 | { 29 | app.UseWebAssemblyDebugging(); 30 | } 31 | else 32 | { 33 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 34 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 35 | app.UseHsts(); 36 | } 37 | 38 | app.UseHttpsRedirection(); 39 | 40 | app.MapControllers(); 41 | 42 | app.UseStaticFiles(); 43 | app.UseAntiforgery(); 44 | 45 | app.MapRazorComponents() 46 | .AddInteractiveServerRenderMode() 47 | .AddInteractiveWebAssemblyRenderMode() 48 | .AddAdditionalAssemblies(typeof(SpawnDev.BlazorJS.Cryptography.Demo.Client._Imports).Assembly); 49 | 50 | app.Run(); 51 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:7202", 8 | "sslPort": 44379 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5140", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7225;http://localhost:5140", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetCryptoSHA.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography 4 | { 5 | public partial class DotNetCrypto 6 | { 7 | /// 8 | /// Hash the specified data using the specified hash algorithm 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | public override Task Digest(string hashName, byte[] data) 15 | { 16 | switch (hashName) 17 | { 18 | case HashName.SHA256: 19 | { 20 | using var sha = SHA256.Create(); 21 | return Task.FromResult(sha.ComputeHash(data)); 22 | } 23 | case HashName.SHA384: 24 | { 25 | using var sha = SHA384.Create(); 26 | return Task.FromResult(sha.ComputeHash(data)); 27 | } 28 | case HashName.SHA512: 29 | { 30 | using var sha = SHA512.Create(); 31 | return Task.FromResult(sha.ComputeHash(data)); 32 | } 33 | case HashName.SHA1: 34 | { 35 | using var sha = SHA1.Create(); 36 | return Task.FromResult(sha.ComputeHash(data)); 37 | } 38 | } 39 | throw new NotImplementedException($"Digest failed: {hashName} hash algorithm not supported"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/SpawnDev.BlazorJS.Cryptography.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0;net10.0 5 | enable 6 | enable 7 | 2.4.0 8 | True 9 | true 10 | true 11 | Embedded 12 | SpawnDev.BlazorJS.Cryptography 13 | LostBeard 14 | A cross platform cryptography library that supports encryption with AES-GCM and AES-CBC, shared secret generation with ECDH, data signatures with ECDSA, and hashing with SHA on Windows, Linux, and Browser (Blazor) platforms. 15 | https://github.com/LostBeard/SpawnDev.BlazorJS.Cryptography 16 | README.md 17 | LICENSE.txt 18 | icon-128.png 19 | https://github.com/LostBeard/SpawnDev.BlazorJS.Cryptography.git 20 | git 21 | Blazor;BlazorWebAssembly;Cryptography;ECDSA;ECDH;SHA;AES-GCM;WebBrowser 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/PortableKey.cs: -------------------------------------------------------------------------------- 1 | namespace SpawnDev.BlazorJS.Cryptography 2 | { 3 | /// 4 | /// PortableKey abstract class 5 | /// 6 | public abstract class PortableKey : IDisposable, IAsyncDisposable 7 | { 8 | /// 9 | /// Key algorithm 10 | /// 11 | public abstract string AlgorithmName { get; } 12 | /// 13 | /// Returns true if the private key can be extracted 14 | /// 15 | public virtual bool Extractable { get; protected set; } = true; 16 | /// 17 | /// Key usages 18 | /// 19 | public virtual string[] Usages { get; protected set; } = Array.Empty(); 20 | /// 21 | /// Returns true if this instance has been disposed 22 | /// 23 | public bool IsDisposed { get; protected set; } 24 | /// 25 | /// Dispose instance resources 26 | /// 27 | /// 28 | protected virtual void Dispose(bool disposing) { } 29 | /// 30 | /// Dispose instance resources 31 | /// 32 | public void Dispose() 33 | { 34 | if (IsDisposed) return; 35 | IsDisposed = true; 36 | GC.SuppressFinalize(this); 37 | Dispose(false); 38 | } 39 | /// 40 | /// Dispose instance resources 41 | /// 42 | public virtual ValueTask DisposeAsync() 43 | { 44 | return ValueTask.CompletedTask; 45 | } 46 | /// 47 | /// Instance finalizer 48 | /// 49 | ~PortableKey() 50 | { 51 | Dispose(false); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/Pages/Weather.razor: -------------------------------------------------------------------------------- 1 | @page "/weather" 2 | @attribute [StreamRendering] 3 | 4 | Weather 5 | 6 |

Weather

7 | 8 |

This component demonstrates showing data.

9 | 10 | @if (forecasts == null) 11 | { 12 |

Loading...

13 | } 14 | else 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var forecast in forecasts) 27 | { 28 | 29 | 30 | 31 | 32 | 33 | 34 | } 35 | 36 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
37 | } 38 | 39 | @code { 40 | private WeatherForecast[]? forecasts; 41 | 42 | protected override async Task OnInitializedAsync() 43 | { 44 | // Simulate asynchronous loading to demonstrate streaming rendering 45 | await Task.Delay(500); 46 | 47 | var startDate = DateOnly.FromDateTime(DateTime.Now); 48 | var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 49 | forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast 50 | { 51 | Date = startDate.AddDays(index), 52 | TemperatureC = Random.Shared.Next(-20, 55), 53 | Summary = summaries[Random.Shared.Next(summaries.Length)] 54 | }).ToArray(); 55 | } 56 | 57 | private class WeatherForecast 58 | { 59 | public DateOnly Date { get; set; } 60 | public int TemperatureC { get; set; } 61 | public string? Summary { get; set; } 62 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.12.35424.110 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpawnDev.BlazorJS.Cryptography", "SpawnDev.BlazorJS.Cryptography\SpawnDev.BlazorJS.Cryptography.csproj", "{8EB351FD-4658-4859-934E-5557CC840454}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpawnDev.BlazorJS.Cryptography.Demo", "SpawnDev.BlazorJS.Cryptography.Demo\SpawnDev.BlazorJS.Cryptography.Demo\SpawnDev.BlazorJS.Cryptography.Demo.csproj", "{30B87D8A-B726-4AF6-B182-598FD802A52C}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpawnDev.BlazorJS.Cryptography.Demo.Client", "SpawnDev.BlazorJS.Cryptography.Demo\SpawnDev.BlazorJS.Cryptography.Demo.Client\SpawnDev.BlazorJS.Cryptography.Demo.Client.csproj", "{0280B64E-241A-42B0-B5CF-56D3768D2C86}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {8EB351FD-4658-4859-934E-5557CC840454}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {8EB351FD-4658-4859-934E-5557CC840454}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {8EB351FD-4658-4859-934E-5557CC840454}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {8EB351FD-4658-4859-934E-5557CC840454}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {30B87D8A-B726-4AF6-B182-598FD802A52C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {30B87D8A-B726-4AF6-B182-598FD802A52C}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {30B87D8A-B726-4AF6-B182-598FD802A52C}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {30B87D8A-B726-4AF6-B182-598FD802A52C}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {0280B64E-241A-42B0-B5CF-56D3768D2C86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {0280B64E-241A-42B0-B5CF-56D3768D2C86}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {0280B64E-241A-42B0-B5CF-56D3768D2C86}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {0280B64E-241A-42B0-B5CF-56D3768D2C86}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {233AC44B-36F2-4638-AEDF-5F3C2EC45FD1} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMECDHKey.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.BrowserWASM 4 | { 5 | /// 6 | /// Browser platform ECDH key 7 | /// 8 | public class BrowserWASMECDHKey : PortableECDHKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public CryptoKeyPair Key { get; protected set; } 14 | /// 15 | /// The named curve 16 | /// 17 | public override string NamedCurve 18 | { 19 | get 20 | { 21 | using var privateKey = Key.PrivateKey; 22 | if (privateKey != null) return privateKey.JSRef!.Get("algorithm.namedCurve"); 23 | using var publicKey = Key.PublicKey; 24 | if (publicKey != null) return publicKey.JSRef!.Get("algorithm.namedCurve"); 25 | return string.Empty; 26 | } 27 | } 28 | /// 29 | /// Returns true if the key can be extracted 30 | /// 31 | public override bool Extractable 32 | { 33 | get 34 | { 35 | using var privateKey = Key.PrivateKey; 36 | return privateKey?.Extractable ?? false; 37 | } 38 | } 39 | /// 40 | /// Key usages 41 | /// 42 | public override string[] Usages 43 | { 44 | get 45 | { 46 | var ret = new List(); 47 | using var privateKey = Key.PrivateKey; 48 | if (privateKey != null) ret.AddRange(privateKey.Usages); 49 | using var publicKey = Key.PublicKey; 50 | if (publicKey != null) ret.AddRange(publicKey.Usages); 51 | return ret.ToArray(); 52 | } 53 | } 54 | /// 55 | /// Create a new instance 56 | /// 57 | /// 58 | public BrowserWASMECDHKey(CryptoKeyPair key) 59 | { 60 | Key = key; 61 | } 62 | /// 63 | /// Dispose instance resources 64 | /// 65 | protected override void Dispose(bool disposing) 66 | { 67 | if (disposing) 68 | { 69 | Key?.Dispose(); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMECDSAKey.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.JSObjects; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography.BrowserWASM 4 | { 5 | /// 6 | /// Browser platform ECDSA key 7 | /// 8 | public class BrowserWASMECDSAKey : PortableECDSAKey 9 | { 10 | /// 11 | /// The platform specific key 12 | /// 13 | public CryptoKeyPair Key { get; protected set; } 14 | /// 15 | /// The named curve 16 | /// 17 | public override string NamedCurve 18 | { 19 | get 20 | { 21 | using var privateKey = Key.PrivateKey; 22 | if (privateKey != null) return privateKey.JSRef!.Get("algorithm.namedCurve"); 23 | using var publicKey = Key.PublicKey; 24 | if (publicKey != null) return publicKey.JSRef!.Get("algorithm.namedCurve"); 25 | return string.Empty; 26 | } 27 | } 28 | /// 29 | /// Returns true if the key can be extracted 30 | /// 31 | public override bool Extractable 32 | { 33 | get 34 | { 35 | using var privateKey = Key.PrivateKey; 36 | return privateKey?.Extractable ?? false; 37 | } 38 | } 39 | /// 40 | /// Key usages 41 | /// 42 | public override string[] Usages 43 | { 44 | get 45 | { 46 | var ret = new List(); 47 | using var privateKey = Key.PrivateKey; 48 | if (privateKey != null) ret.AddRange(privateKey.Usages); 49 | using var publicKey = Key.PublicKey; 50 | if (publicKey != null) ret.AddRange(publicKey.Usages); 51 | return ret.ToArray(); 52 | } 53 | } 54 | /// 55 | /// Create a new instance 56 | /// 57 | /// 58 | public BrowserWASMECDSAKey(CryptoKeyPair key) 59 | { 60 | Key = key; 61 | } 62 | /// 63 | /// Dispose instance resources 64 | /// 65 | protected override void Dispose(bool disposing) 66 | { 67 | if (disposing) 68 | { 69 | Key?.Dispose(); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Components/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | appearance: none; 3 | cursor: pointer; 4 | width: 3.5rem; 5 | height: 2.5rem; 6 | color: white; 7 | position: absolute; 8 | top: 0.5rem; 9 | right: 1rem; 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); 12 | } 13 | 14 | .navbar-toggler:checked { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | } 17 | 18 | .top-row { 19 | height: 3.5rem; 20 | background-color: rgba(0,0,0,0.4); 21 | } 22 | 23 | .navbar-brand { 24 | font-size: 1.1rem; 25 | } 26 | 27 | .bi { 28 | display: inline-block; 29 | position: relative; 30 | width: 1.25rem; 31 | height: 1.25rem; 32 | margin-right: 0.75rem; 33 | top: -1px; 34 | background-size: cover; 35 | } 36 | 37 | .bi-house-door-fill-nav-menu { 38 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | .bi-plus-square-fill-nav-menu { 42 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 43 | } 44 | 45 | .bi-list-nested-nav-menu { 46 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 47 | } 48 | 49 | .nav-item { 50 | font-size: 0.9rem; 51 | padding-bottom: 0.5rem; 52 | } 53 | 54 | .nav-item:first-of-type { 55 | padding-top: 1rem; 56 | } 57 | 58 | .nav-item:last-of-type { 59 | padding-bottom: 1rem; 60 | } 61 | 62 | .nav-item ::deep .nav-link { 63 | color: #d7d7d7; 64 | background: none; 65 | border: none; 66 | border-radius: 4px; 67 | height: 3rem; 68 | display: flex; 69 | align-items: center; 70 | line-height: 3rem; 71 | width: 100%; 72 | } 73 | 74 | .nav-item ::deep a.active { 75 | background-color: rgba(255,255,255,0.37); 76 | color: white; 77 | } 78 | 79 | .nav-item ::deep .nav-link:hover { 80 | background-color: rgba(255,255,255,0.1); 81 | color: white; 82 | } 83 | 84 | .nav-scrollable { 85 | display: none; 86 | } 87 | 88 | .navbar-toggler:checked ~ .nav-scrollable { 89 | display: block; 90 | } 91 | 92 | @media (min-width: 641px) { 93 | .navbar-toggler { 94 | display: none; 95 | } 96 | 97 | .nav-scrollable { 98 | /* Never collapse the sidebar for wide screens */ 99 | display: block; 100 | 101 | /* Allow sidebar to scroll for tall menus */ 102 | height: calc(100vh - 3.5rem); 103 | overflow-y: auto; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo/Controllers/CryptographyTestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using System.Text; 4 | using static SpawnDev.BlazorJS.Cryptography.Demo.Client.Pages.TestPage; 5 | 6 | namespace SpawnDev.BlazorJS.Cryptography.Demo.Controllers 7 | { 8 | [Route("api/[controller]")] 9 | [ApiController] 10 | public class CryptographyTestController : ControllerBase 11 | { 12 | 13 | DotNetCrypto DotNetCrypto; 14 | public CryptographyTestController(DotNetCrypto dotNetCrypto) 15 | { 16 | DotNetCrypto = dotNetCrypto; 17 | 18 | } 19 | static bool BeenInit = false; 20 | static PortableECDSAKey? ECDSAKey = null; 21 | static PortableECDHKey? ECDHKey = null; 22 | async Task InitAsync() 23 | { 24 | if (BeenInit) return; 25 | BeenInit = true; 26 | ECDSAKey = await DotNetCrypto.GenerateECDSAKey(); 27 | ECDHKey = await DotNetCrypto.GenerateECDHKey(); 28 | } 29 | 30 | [HttpGet("ecdsa")] 31 | public async Task GetECDSA() 32 | { 33 | await InitAsync(); 34 | var publicKeyBytes = await DotNetCrypto.ExportPublicKeySpki(ECDSAKey!); 35 | return publicKeyBytes; 36 | } 37 | 38 | [HttpGet("ecdh")] 39 | public async Task GetECDH() 40 | { 41 | await InitAsync(); 42 | var publicKeyBytes = await DotNetCrypto.ExportPublicKeySpki(ECDHKey!); 43 | return publicKeyBytes; 44 | } 45 | 46 | [HttpPost("identify")] 47 | public async Task> IdentifyPost(string[] data) 48 | { 49 | await InitAsync(); 50 | var ret = "Hello world!!"; 51 | return new string[] { ret }; 52 | } 53 | 54 | [HttpPost("GetSharedSecret")] 55 | public async Task GetSharedSecret(GetSharedSecretArgs args) 56 | { 57 | await InitAsync(); 58 | // import the browser's base64 encoded ECDH public key 59 | using var browsersECDHKey = await DotNetCrypto.ImportECDHKey(Convert.FromBase64String(args.SenderECDHPublicKeyB64)); 60 | // generate a shared secret 61 | // the browser will generate a secret that should be identical to the one the server creates 62 | var sharedSecret = await DotNetCrypto.DeriveBits(ECDHKey!, browsersECDHKey); 63 | // send it to the browser for comparison. you would never do this in production. 64 | return sharedSecret; 65 | } 66 | 67 | [HttpPost("EncryptionTest")] 68 | public async Task EncryptionTest(EncryptionTestArgs args) 69 | { 70 | await InitAsync(); 71 | // import the browser's base64 encoded ECDH public key 72 | using var browsersECDHKey = await DotNetCrypto.ImportECDHKey(Convert.FromBase64String(args.SenderECDHPublicKeyB64)); 73 | // generate a shared secret 74 | // the browser will generate a secret that should be identical to the one the server creates 75 | var sharedSecret = await DotNetCrypto.DeriveBits(ECDHKey!, browsersECDHKey); 76 | // create an encryption key based on the shared secret 77 | using var encKey = await DotNetCrypto.GenerateAESGCMKey(sharedSecret); 78 | // decrypt the message encrypted by the browser 79 | var origMsgBytes = await DotNetCrypto.Decrypt(encKey, args.EncryptedMessage); 80 | // convert the message to text 81 | var origMsg = Encoding.UTF8.GetString(origMsgBytes); 82 | // send an encryptd response 83 | var responseMsg = "Response message!"; 84 | // convert bytes to text 85 | var responseBytes = Encoding.UTF8.GetBytes(responseMsg); 86 | // encrypt using the shared key 87 | var encMsg = await DotNetCrypto.Encrypt(encKey, responseBytes); 88 | // send it to the browser for comparison. you would never do this in production. 89 | return encMsg; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetCryptoECDSA.cs: -------------------------------------------------------------------------------- 1 | 2 | using SpawnDev.BlazorJS.Cryptography.DotNet; 3 | using System.Security.Cryptography; 4 | 5 | namespace SpawnDev.BlazorJS.Cryptography 6 | { 7 | public partial class DotNetCrypto 8 | { 9 | /// 10 | /// Generate a new ECDSA key 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public override Task GenerateECDSAKey(string namedCurve = NamedCurve.P521, bool extractable = true) 17 | { 18 | var eccurve = NamedCurveToECCurve(namedCurve); 19 | var key = ECDsa.Create(eccurve); 20 | return Task.FromResult(new DotNetECDSAKey(key)); 21 | } 22 | /// 23 | /// Exports the public key in Spki format 24 | /// 25 | /// 26 | /// 27 | /// 28 | public override Task ExportPublicKeySpki(PortableECDSAKey key) 29 | { 30 | if (key is not DotNetECDSAKey keyNet) throw new NotImplementedException(); 31 | return Task.FromResult(keyNet.Key.ExportSubjectPublicKeyInfo()); 32 | } 33 | /// 34 | /// Exports the private key in Pkcs8 format 35 | /// 36 | /// 37 | /// 38 | /// 39 | public override Task ExportPrivateKeyPkcs8(PortableECDSAKey key) 40 | { 41 | if (key is not DotNetECDSAKey keyNet) throw new NotImplementedException(); 42 | return Task.FromResult(keyNet.Key.ExportPkcs8PrivateKey()); 43 | } 44 | /// 45 | /// Import an ECDSA public key 46 | /// 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// 52 | public override Task ImportECDSAKey(byte[] publicKeySpkiData, string namedCurve = NamedCurve.P521, bool extractable = true) 53 | { 54 | var key = ECDsa.Create(); 55 | key.ImportSubjectPublicKeyInfo(publicKeySpkiData, out _); 56 | return Task.FromResult(new DotNetECDSAKey(key)); 57 | } 58 | /// 59 | /// Import an ECDSA public and private key 60 | /// 61 | /// 62 | /// 63 | /// 64 | /// 65 | /// 66 | /// 67 | public override Task ImportECDSAKey(byte[] publicKeySpkiData, byte[] privateKeyPkcs8Data, string namedCurve = NamedCurve.P521, bool extractable = true) 68 | { 69 | var key = ECDsa.Create(); 70 | key.ImportSubjectPublicKeyInfo(publicKeySpkiData, out _); 71 | key.ImportPkcs8PrivateKey(privateKeyPkcs8Data, out _); 72 | return Task.FromResult(new DotNetECDSAKey(key)); 73 | } 74 | /// 75 | /// Verify a data signature 76 | /// 77 | /// 78 | /// 79 | /// 80 | /// 81 | /// 82 | /// 83 | public override Task Verify(PortableECDSAKey key, byte[] data, byte[] signature, string hashName = HashName.SHA512) 84 | { 85 | if (key is not DotNetECDSAKey keyNet) throw new NotImplementedException(); 86 | var hashAlgorithm = HashNameToHashAlgorithmName(hashName); 87 | var verified = keyNet!.Key.VerifyData(data, signature, hashAlgorithm); 88 | return Task.FromResult(verified); 89 | } 90 | /// 91 | /// Sign data using an ECDSA key 92 | /// 93 | /// 94 | /// 95 | /// 96 | /// 97 | /// 98 | public override Task Sign(PortableECDSAKey key, byte[] data, string hashName = HashName.SHA512) 99 | { 100 | if (key is not DotNetECDSAKey keyNet) throw new NotImplementedException(); 101 | var hashAlgorithm = HashNameToHashAlgorithmName(hashName); 102 | var signature = keyNet!.Key.SignData(data, hashAlgorithm); 103 | return Task.FromResult(signature); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMCryptoECDSA.cs: -------------------------------------------------------------------------------- 1 | 2 | using SpawnDev.BlazorJS.Cryptography.BrowserWASM; 3 | using SpawnDev.BlazorJS.JSObjects; 4 | 5 | namespace SpawnDev.BlazorJS.Cryptography 6 | { 7 | public partial class BrowserWASMCrypto 8 | { 9 | /// 10 | /// Generate a new ECDSA key 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public override async Task GenerateECDSAKey(string namedCurve = NamedCurve.P521, bool extractable = true) 17 | { 18 | var keyUsages = new string[] { "sign", "verify" }; 19 | var key = await SubtleCrypto!.GenerateKey(new EcKeyGenParams { Name = Algorithm.ECDSA, NamedCurve = namedCurve }, extractable, keyUsages); 20 | return new BrowserWASMECDSAKey(key); 21 | } 22 | /// 23 | /// Exports the public key in Spki format 24 | /// 25 | /// 26 | /// 27 | /// 28 | public override async Task ExportPublicKeySpki(PortableECDSAKey key) 29 | { 30 | if (key is not BrowserWASMECDSAKey keyJS) throw new NotImplementedException(); 31 | using var publicKey = keyJS.Key.PublicKey; 32 | using var arrayBuffer = await SubtleCrypto!.ExportKeySpki(publicKey!); 33 | return arrayBuffer.ReadBytes(); 34 | } 35 | /// 36 | /// Exports the private key in Pkcs8 format 37 | /// 38 | /// 39 | /// 40 | /// 41 | public override async Task ExportPrivateKeyPkcs8(PortableECDSAKey key) 42 | { 43 | if (key is not BrowserWASMECDSAKey keyJS) throw new NotImplementedException(); 44 | using var privateKey = keyJS.Key.PrivateKey; 45 | using var arrayBuffer = await SubtleCrypto!.ExportKeyPkcs8(privateKey!); 46 | return arrayBuffer.ReadBytes(); 47 | } 48 | /// 49 | /// Import an ECDSA public key 50 | /// 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// 56 | public override async Task ImportECDSAKey(byte[] publicKeySpkiData, string namedCurve = NamedCurve.P521, bool extractable = true) 57 | { 58 | var keyUsagesPublicKey = new string[] { "verify" }; 59 | var publicKey = await SubtleCrypto!.ImportKey("spki", publicKeySpkiData, new EcKeyImportParams { Name = Algorithm.ECDSA, NamedCurve = namedCurve }, extractable, keyUsagesPublicKey); 60 | var key = new CryptoKeyPair 61 | { 62 | PublicKey = publicKey, 63 | }; 64 | return new BrowserWASMECDSAKey(key); 65 | } 66 | /// 67 | /// Import an ECDSA public and private key 68 | /// 69 | /// 70 | /// 71 | /// 72 | /// 73 | /// 74 | /// 75 | public override async Task ImportECDSAKey(byte[] publicKeySpkiData, byte[] privateKeyPkcs8Data, string namedCurve = NamedCurve.P521, bool extractable = true) 76 | { 77 | var keyUsagesPrivateKey = new string[] { "sign" }; 78 | var keyUsagesPublicKey = new string[] { "verify" }; 79 | var privateKey = await SubtleCrypto!.ImportKey("pkcs8", privateKeyPkcs8Data, new EcKeyImportParams { Name = Algorithm.ECDSA, NamedCurve = namedCurve }, extractable, keyUsagesPrivateKey); 80 | var publicKey = await SubtleCrypto!.ImportKey("spki", publicKeySpkiData, new EcKeyImportParams { Name = Algorithm.ECDSA, NamedCurve = namedCurve }, extractable, keyUsagesPublicKey); 81 | var key = new CryptoKeyPair 82 | { 83 | PublicKey = publicKey, 84 | PrivateKey = privateKey, 85 | }; 86 | return new BrowserWASMECDSAKey(key); 87 | } 88 | /// 89 | /// Verify a data signature 90 | /// 91 | /// 92 | /// 93 | /// 94 | /// 95 | /// 96 | /// 97 | public override async Task Verify(PortableECDSAKey key, byte[] data, byte[] signature, string hashName = HashName.SHA512) 98 | { 99 | if (key is not BrowserWASMECDSAKey keyJS) throw new NotImplementedException(); 100 | using var publicKey = keyJS!.Key.PublicKey!; 101 | using var signatureUint8Array = new Uint8Array(signature); 102 | using var signatureArrayBuffer = signatureUint8Array.Buffer; 103 | var ret = await SubtleCrypto!.Verify(new EcdsaParams { Hash = hashName }, publicKey, signatureArrayBuffer, data); 104 | return ret; 105 | } 106 | /// 107 | /// Sign data using an ECDSA key 108 | /// 109 | /// 110 | /// 111 | /// 112 | /// 113 | /// 114 | public override async Task Sign(PortableECDSAKey key, byte[] data, string hashName = HashName.SHA512) 115 | { 116 | if (key is not BrowserWASMECDSAKey keyJS) throw new NotImplementedException(); 117 | using var privateKey = keyJS!.Key.PrivateKey!; 118 | using var signature = await SubtleCrypto!.Sign(new EcdsaParams { Hash = hashName }, privateKey, data); 119 | return signature.ReadBytes(); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserCryptoECDSA.cs: -------------------------------------------------------------------------------- 1 | 2 | using SpawnDev.BlazorJS.Cryptography.Browser; 3 | using SpawnDev.BlazorJS.JSObjects; 4 | using SpawnDev.BlazorJS.RemoteJSRuntime.AsyncObjects; 5 | 6 | namespace SpawnDev.BlazorJS.Cryptography 7 | { 8 | public partial class BrowserCrypto 9 | { 10 | /// 11 | /// Generate a new ECDSA key 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | public override async Task GenerateECDSAKey(string namedCurve = NamedCurve.P521, bool extractable = true) 18 | { 19 | var keyUsages = new string[] { "sign", "verify" }; 20 | var key = await SubtleCrypto!.GenerateKey(new EcKeyGenParams { Name = Algorithm.ECDSA, NamedCurve = namedCurve }, extractable, keyUsages); 21 | return new BrowserECDSAKey(key, namedCurve, extractable, keyUsages); 22 | } 23 | /// 24 | /// Exports the public key in Spki format 25 | /// 26 | /// 27 | /// 28 | /// 29 | public override async Task ExportPublicKeySpki(PortableECDSAKey key) 30 | { 31 | if (key is not BrowserECDSAKey keyJS) throw new NotImplementedException(); 32 | await using var publicKey = await keyJS.Key.Get_PublicKey(); 33 | await using var arrayBuffer = await SubtleCrypto!.ExportKeySpki(publicKey!); 34 | return await arrayBuffer.ReadBytes(); 35 | } 36 | /// 37 | /// Exports the private key in Pkcs8 format 38 | /// 39 | /// 40 | /// 41 | /// 42 | public override async Task ExportPrivateKeyPkcs8(PortableECDSAKey key) 43 | { 44 | if (key is not BrowserECDSAKey keyJS) throw new NotImplementedException(); 45 | await using var privateKey = await keyJS.Key.Get_PrivateKey(); 46 | await using var arrayBuffer = await SubtleCrypto!.ExportKeyPkcs8(privateKey!); 47 | return await arrayBuffer.ReadBytes(); 48 | } 49 | /// 50 | /// Import an ECDSA public key 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | public override async Task ImportECDSAKey(byte[] publicKeySpkiData, string namedCurve = NamedCurve.P521, bool extractable = true) 58 | { 59 | var keyUsagesPublicKey = new string[] { "verify" }; 60 | await using var publicKey = await SubtleCrypto!.ImportKey("spki", publicKeySpkiData, new EcKeyImportParams { Name = Algorithm.ECDSA, NamedCurve = namedCurve }, extractable, keyUsagesPublicKey); 61 | var key = await CryptoKeyPairAsync.New(JSA); 62 | await key.Set_PublicKey(publicKey); 63 | return new BrowserECDSAKey(key, namedCurve, extractable, keyUsagesPublicKey); 64 | } 65 | /// 66 | /// Import an ECDSA public and private key 67 | /// 68 | /// 69 | /// 70 | /// 71 | /// 72 | /// 73 | /// 74 | public override async Task ImportECDSAKey(byte[] publicKeySpkiData, byte[] privateKeyPkcs8Data, string namedCurve = NamedCurve.P521, bool extractable = true) 75 | { 76 | var keyUsagesPrivateKey = new string[] { "sign" }; 77 | var keyUsagesPublicKey = new string[] { "verify" }; 78 | await using var privateKey = await SubtleCrypto!.ImportKey("pkcs8", privateKeyPkcs8Data, new EcKeyImportParams { Name = Algorithm.ECDSA, NamedCurve = namedCurve }, extractable, keyUsagesPrivateKey); 79 | await using var publicKey = await SubtleCrypto!.ImportKey("spki", publicKeySpkiData, new EcKeyImportParams { Name = Algorithm.ECDSA, NamedCurve = namedCurve }, extractable, keyUsagesPublicKey); 80 | var key = await CryptoKeyPairAsync.New(JSA); 81 | await key.Set_PublicKey(publicKey); 82 | await key.Set_PrivateKey(privateKey); 83 | return new BrowserECDSAKey(key, namedCurve, extractable, keyUsagesPublicKey.Concat(keyUsagesPrivateKey).ToArray()); 84 | } 85 | /// 86 | /// Verify a data signature 87 | /// 88 | /// 89 | /// 90 | /// 91 | /// 92 | /// 93 | /// 94 | public override async Task Verify(PortableECDSAKey key, byte[] data, byte[] signature, string hashName = HashName.SHA512) 95 | { 96 | if (key is not BrowserECDSAKey keyJS) throw new NotImplementedException(); 97 | await using var publicKey = await keyJS!.Key.Get_PublicKey(); 98 | await using var signatureArrayBuffer = await ArrayBufferAsync.New(JSA, signature); 99 | var ret = await SubtleCrypto!.Verify(new EcdsaParams { Hash = hashName }, publicKey!, signatureArrayBuffer, data); 100 | return ret; 101 | } 102 | /// 103 | /// Sign data using an ECDSA key 104 | /// 105 | /// 106 | /// 107 | /// 108 | /// 109 | /// 110 | public override async Task Sign(PortableECDSAKey key, byte[] data, string hashName = HashName.SHA512) 111 | { 112 | if (key is not BrowserECDSAKey keyJS) throw new NotImplementedException(); 113 | await using var privateKey = await keyJS!.Key.Get_PrivateKey(); 114 | await using var signature = await SubtleCrypto!.Sign(new EcdsaParams { Hash = hashName }, privateKey, data); 115 | return await signature.ReadBytes(); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetCryptoECDH.cs: -------------------------------------------------------------------------------- 1 | 2 | using SpawnDev.BlazorJS.Cryptography.DotNet; 3 | using SpawnDev.BlazorJS.JSObjects; 4 | using System.Security.Cryptography; 5 | 6 | namespace SpawnDev.BlazorJS.Cryptography 7 | { 8 | public partial class DotNetCrypto 9 | { 10 | /// 11 | /// Generates a new ECDH crypto key 12 | /// 13 | /// 14 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
15 | /// P-256
16 | /// P-384
17 | /// P-521 18 | /// 19 | /// 20 | /// 21 | /// 22 | public override Task GenerateECDHKey(string namedCurve = NamedCurve.P521, bool extractable = true) 23 | { 24 | var eccurve = NamedCurveToECCurve(namedCurve); 25 | var key = ECDiffieHellman.Create(eccurve); 26 | return Task.FromResult(new DotNetECDHKey(key)); 27 | } 28 | /// 29 | /// Exports the public key in Spki format 30 | /// 31 | /// 32 | /// 33 | /// 34 | public override Task ExportPublicKeySpki(PortableECDHKey key) 35 | { 36 | if (key is not DotNetECDHKey keyNet) throw new NotImplementedException(); 37 | return Task.FromResult(keyNet.Key.ExportSubjectPublicKeyInfo()); 38 | } 39 | /// 40 | /// Exports the private key in Pkcs8 format 41 | /// 42 | /// 43 | /// 44 | /// 45 | public override Task ExportPrivateKeyPkcs8(PortableECDHKey key) 46 | { 47 | if (key is not DotNetECDHKey keyNet) throw new NotImplementedException(); 48 | return Task.FromResult(keyNet.Key.ExportPkcs8PrivateKey()); 49 | } 50 | /// 51 | /// Import an ECDH public key 52 | /// 53 | /// 54 | /// 55 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
56 | /// P-256
57 | /// P-384
58 | /// P-521 59 | /// 60 | /// 61 | /// 62 | /// 63 | public override Task ImportECDHKey(byte[] publicKeySpki, string namedCurve = NamedCurve.P521, bool extractable = true) 64 | { 65 | var key = ECDiffieHellman.Create(); 66 | key.ImportSubjectPublicKeyInfo(publicKeySpki, out _); 67 | return Task.FromResult(new DotNetECDHKey(key)); 68 | } 69 | /// 70 | /// Import an ECDH public and private key 71 | /// 72 | /// 73 | /// 74 | /// 75 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
76 | /// P-256
77 | /// P-384
78 | /// P-521 79 | /// 80 | /// 81 | /// 82 | /// 83 | public override Task ImportECDHKey(byte[] publicKeySpki, byte[] privateKeyPkcs8, string namedCurve = NamedCurve.P521, bool extractable = true) 84 | { 85 | var key = ECDiffieHellman.Create(); 86 | key.ImportPkcs8PrivateKey(privateKeyPkcs8, out _); 87 | return Task.FromResult(new DotNetECDHKey(key)); 88 | } 89 | /// 90 | /// Creates a shared secret that is cross-platform compatible 91 | /// 92 | /// 93 | /// 94 | /// Number of bits to derive.
For compatibility, this should be a multiple of 8 95 | /// 96 | /// 97 | /// 98 | /// 99 | /// 100 | public override Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey, int bitLength) 101 | { 102 | if (localPartyKey is not DotNetECDHKey localPartyKeyNet) throw new NotImplementedException(); 103 | if (otherPartyKey is not DotNetECDHKey otherPartyKeyNet) throw new NotImplementedException(); 104 | if (bitLength <= 0) throw new ArgumentOutOfRangeException(nameof(bitLength)); 105 | if (localPartyKeyNet?.Key == null) throw new ArgumentNullException($"localPartyKey.Key cannot be null"); 106 | if (otherPartyKeyNet?.Key?.PublicKey == null) throw new ArgumentNullException($"otherPartyKey.Key.PublicKey cannot be null"); 107 | var ret = localPartyKeyNet!.Key.DeriveRawSecretAgreement(otherPartyKeyNet!.Key.PublicKey); 108 | var retBitLength = ret.Length * 8; 109 | if (retBitLength < bitLength) throw new Exception($"Requested {bitLength} exceeds the max bitLength {retBitLength}"); 110 | var requestedByteLength = (int)Math.Ceiling(bitLength / 8d); 111 | if (requestedByteLength == retBitLength) return Task.FromResult(ret); 112 | return Task.FromResult(ret[..requestedByteLength]); 113 | } 114 | /// 115 | /// Creates a shared secret that is cross-platform compatible 116 | /// 117 | /// 118 | /// 119 | /// 120 | /// 121 | public override Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey) 122 | { 123 | // chooses the largest bit length divisible by 8 for best compatibility as recommended in MDN SubtleCrypto docs 124 | var bitLength = NamedCurveBitLength(localPartyKey.NamedCurve, true); 125 | return DeriveBits(localPartyKey, otherPartyKey, bitLength); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetCryptoAESCBC.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.Cryptography.DotNet; 2 | using System.Security.Cryptography; 3 | 4 | namespace SpawnDev.BlazorJS.Cryptography 5 | { 6 | public partial class DotNetCrypto 7 | { 8 | /// 9 | /// Generate an AES-CBC key 10 | /// 11 | /// 12 | /// 13 | /// 14 | public override Task GenerateAESCBCKey(int keySize, bool extractable = true) 15 | { 16 | var aes = Aes.Create(); 17 | aes.Mode = CipherMode.CBC; 18 | aes.Padding = PaddingMode.PKCS7; 19 | aes.KeySize = keySize; 20 | aes.GenerateKey(); 21 | return Task.FromResult(new DotNetAESCBCKey(aes)); 22 | } 23 | /// 24 | /// Encrypt data using an AES-CBC key 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | public override Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, byte[] iv, bool prependIV = false, AESCBCPadding padding = AESCBCPadding.PKCS7) 34 | { 35 | if (key is not DotNetAESCBCKey nKey) throw new NotImplementedException(); 36 | if (padding == AESCBCPadding.None) 37 | { 38 | if (plainBytes.Length % AES_CBC_BLOCK_SIZE != 0) 39 | { 40 | throw new Exception($"{plainBytes} length must be a multiple of 16 when using no padding."); 41 | } 42 | nKey.Key.Padding = PaddingMode.None; 43 | } 44 | else 45 | { 46 | nKey.Key.Padding = PaddingMode.PKCS7; 47 | } 48 | using var encryptor = nKey.Key.CreateEncryptor(nKey.Key.Key, iv); 49 | using var msEncrypt = new MemoryStream(); 50 | using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write); 51 | using var swEncrypt = new BinaryWriter(csEncrypt); 52 | swEncrypt.Write(plainBytes); 53 | csEncrypt.FlushFinalBlock(); 54 | msEncrypt.Position = 0; 55 | var encryptedData = msEncrypt.ToArray(); 56 | if (!prependIV) return Task.FromResult(encryptedData); 57 | var encryptedDataLength = encryptedData.Length + iv.Length; 58 | var result = new byte[encryptedDataLength]; 59 | // + iv 60 | iv.CopyTo(result, 0); 61 | // + encrypted data 62 | encryptedData.CopyTo(result, iv.Length); 63 | return Task.FromResult(result); 64 | } 65 | /// 66 | /// Encrypt data using an AES-CBC key 67 | /// 68 | /// 69 | /// 70 | /// 71 | /// 72 | /// 73 | public override Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, bool prependIV = true, AESCBCPadding padding = AESCBCPadding.PKCS7) 74 | { 75 | var iv = RandomBytes(16); 76 | return Encrypt(key, plainBytes, iv, prependIV, padding); 77 | } 78 | /// 79 | /// Encrypt data using an AES-CBC key 80 | /// 81 | /// 82 | /// 83 | /// 84 | /// 85 | /// 86 | /// 87 | public override async Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, byte[] iv, AESCBCPadding padding = AESCBCPadding.PKCS7) 88 | { 89 | if (key is not DotNetAESCBCKey nKey) throw new NotImplementedException(); 90 | if (padding == AESCBCPadding.None) 91 | { 92 | nKey.Key.Padding = PaddingMode.None; 93 | if (encryptedData.Length % AES_CBC_BLOCK_SIZE != 0) 94 | { 95 | throw new Exception($"{encryptedData} length must be a multiple of 16 when using no padding."); 96 | } 97 | } 98 | else 99 | { 100 | nKey.Key.Padding = PaddingMode.PKCS7; 101 | } 102 | using var decryptor = nKey.Key.CreateDecryptor(nKey.Key.Key, iv); 103 | using var msDecrypt = new MemoryStream(); 104 | using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write); 105 | using var srDecrypt = new BinaryWriter(csDecrypt); 106 | srDecrypt.Write(encryptedData); 107 | csDecrypt.FlushFinalBlock(); 108 | msDecrypt.Position = 0; 109 | var data = msDecrypt.ToArray(); 110 | return data; 111 | } 112 | /// 113 | /// Decrypt data using an AES-CBC key 114 | /// 115 | /// 116 | /// 117 | /// 118 | /// 119 | public override Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, AESCBCPadding padding = AESCBCPadding.PKCS7) 120 | { 121 | var iv = new byte[16]; 122 | Buffer.BlockCopy(encryptedData, 0, iv, 0, 16); 123 | var encrypted = new byte[encryptedData.Length - 16]; 124 | Buffer.BlockCopy(encryptedData, 16, encrypted, 0, encrypted.Length); 125 | return Decrypt(key, encrypted, iv, padding); 126 | } 127 | /// 128 | /// Decrypt data using an AES-CBC key 129 | /// 130 | /// 131 | /// 132 | /// 133 | public override Task ImportAESCBCKey(byte[] rawKey, bool extractable = true) 134 | { 135 | var key = Aes.Create(); 136 | key.Key = rawKey; 137 | key.Padding = PaddingMode.PKCS7; 138 | key.Mode = CipherMode.CBC; 139 | return Task.FromResult(new DotNetAESCBCKey(key)); 140 | } 141 | /// 142 | /// Export an AES-CBC key 143 | /// 144 | /// 145 | /// 146 | /// 147 | public override Task ExportAESCBCKey(PortableAESCBCKey key) 148 | { 149 | if (key is not DotNetAESCBCKey nKey) throw new NotImplementedException(); 150 | return Task.FromResult(nKey.Key.Key); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMCryptoECDH.cs: -------------------------------------------------------------------------------- 1 | 2 | using SpawnDev.BlazorJS.Cryptography.BrowserWASM; 3 | using SpawnDev.BlazorJS.JSObjects; 4 | 5 | namespace SpawnDev.BlazorJS.Cryptography 6 | { 7 | public partial class BrowserWASMCrypto 8 | { 9 | /// 10 | /// Generates a new ECDH crypto key 11 | /// 12 | /// 13 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
14 | /// P-256
15 | /// P-384
16 | /// P-521 17 | /// 18 | /// 19 | /// 20 | /// 21 | public override async Task GenerateECDHKey(string namedCurve = NamedCurve.P521, bool extractable = true) 22 | { 23 | var keyUsages = new string[] { "deriveBits", "deriveKey" }; 24 | var key = await SubtleCrypto!.GenerateKey(new EcKeyGenParams { Name = Algorithm.ECDH, NamedCurve = namedCurve }, extractable, keyUsages); 25 | return new BrowserWASMECDHKey(key); 26 | } 27 | /// 28 | /// Exports the public key in Spki format 29 | /// 30 | /// 31 | /// 32 | /// 33 | public override async Task ExportPublicKeySpki(PortableECDHKey key) 34 | { 35 | if (key is not BrowserWASMECDHKey keyJS) throw new NotImplementedException(); 36 | using var publicKey = keyJS.Key.PublicKey; 37 | using var arrayBuffer = await SubtleCrypto!.ExportKeySpki(publicKey!); 38 | return arrayBuffer.ReadBytes(); 39 | } 40 | /// 41 | /// Exports the private key in Pkcs8 format 42 | /// 43 | /// 44 | /// 45 | /// 46 | public override async Task ExportPrivateKeyPkcs8(PortableECDHKey key) 47 | { 48 | if (key is not BrowserWASMECDHKey keyJS) throw new NotImplementedException(); 49 | using var privateKey = keyJS.Key.PrivateKey; 50 | using var arrayBuffer = await SubtleCrypto!.ExportKeyPkcs8(privateKey!); 51 | return arrayBuffer.ReadBytes(); 52 | } 53 | /// 54 | /// Import an ECDH public key 55 | /// 56 | /// 57 | /// 58 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
59 | /// P-256
60 | /// P-384
61 | /// P-521 62 | /// 63 | /// 64 | /// 65 | /// 66 | public override async Task ImportECDHKey(byte[] publicKeySpki, string namedCurve = NamedCurve.P521, bool extractable = true) 67 | { 68 | var keyUsages = new string[] { }; 69 | var publicKey = await SubtleCrypto!.ImportKey("spki", publicKeySpki, new EcKeyImportParams { Name = Algorithm.ECDH, NamedCurve = namedCurve }, extractable, keyUsages); 70 | var key = new CryptoKeyPair 71 | { 72 | PublicKey = publicKey, 73 | }; 74 | return new BrowserWASMECDHKey(key); 75 | } 76 | /// 77 | /// Import an ECDH public and private key 78 | /// 79 | /// 80 | /// 81 | /// 82 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
83 | /// P-256
84 | /// P-384
85 | /// P-521 86 | /// 87 | /// 88 | /// 89 | /// 90 | public override async Task ImportECDHKey(byte[] publicKeySpki, byte[] privateKeyPkcs8, string namedCurve = NamedCurve.P521, bool extractable = true) 91 | { 92 | var keyUsages = new string[] { "deriveBits", "deriveKey" }; 93 | var privateKey = await SubtleCrypto!.ImportKey("pkcs8", privateKeyPkcs8, new EcKeyImportParams { Name = Algorithm.ECDH, NamedCurve = namedCurve }, extractable, keyUsages); 94 | var publicKey = await SubtleCrypto!.ImportKey("spki", publicKeySpki, new EcKeyImportParams { Name = Algorithm.ECDH, NamedCurve = namedCurve }, extractable, new string[] { }); 95 | var key = new CryptoKeyPair 96 | { 97 | PublicKey = publicKey, 98 | PrivateKey = privateKey, 99 | }; 100 | return new BrowserWASMECDHKey(key); 101 | } 102 | /// 103 | /// Creates a shared secret that is cross-platform compatible 104 | /// 105 | /// 106 | /// 107 | /// Number of bits to derive.
For compatibility, this should be a multiple of 8 108 | /// 109 | /// 110 | /// 111 | /// 112 | /// 113 | public override async Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey, int bitLength) 114 | { 115 | if (localPartyKey is not BrowserWASMECDHKey localPartyKeyJS) throw new NotImplementedException(); 116 | if (otherPartyKey is not BrowserWASMECDHKey otherPartyKeyJS) throw new NotImplementedException(); 117 | if (bitLength <= 0) throw new ArgumentOutOfRangeException(nameof(bitLength)); 118 | using var localPartyPrivateKey = localPartyKeyJS!.Key.PrivateKey; 119 | if (localPartyPrivateKey == null) throw new ArgumentNullException($"localPartyKey.Key.PrivateKey cannot be null"); 120 | using var otherPartyPublicKey = otherPartyKeyJS!.Key.PublicKey; 121 | if (otherPartyPublicKey == null) throw new ArgumentNullException($"otherPartyKey.Key.PublicKey cannot be null"); 122 | using var sharedSecret = await SubtleCrypto!.DeriveBits(new EcdhKeyDeriveParams { Public = otherPartyPublicKey }, localPartyPrivateKey, bitLength); 123 | var sharedSecretBytes = sharedSecret.ReadBytes(); 124 | return sharedSecretBytes; 125 | } 126 | /// 127 | /// Creates a shared secret that is cross-platform compatible 128 | /// 129 | /// 130 | /// 131 | /// 132 | /// 133 | public override Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey) 134 | { 135 | // chooses the largest bit length divisible by 8 for best compatibility as recommended in MDN SubtleCrypto docs 136 | var bitLength = NamedCurveBitLength(localPartyKey.NamedCurve, true); 137 | return DeriveBits(localPartyKey, otherPartyKey, bitLength); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | 365 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserCryptoECDH.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.Cryptography.Browser; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | using SpawnDev.BlazorJS.RemoteJSRuntime; 4 | using SpawnDev.BlazorJS.RemoteJSRuntime.AsyncObjects; 5 | 6 | namespace SpawnDev.BlazorJS.Cryptography 7 | { 8 | public partial class BrowserCrypto 9 | { 10 | /// 11 | /// Generates a new ECDH crypto key 12 | /// 13 | /// 14 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
15 | /// P-256
16 | /// P-384
17 | /// P-521 18 | /// 19 | /// 20 | /// 21 | /// 22 | public override async Task GenerateECDHKey(string namedCurve = NamedCurve.P521, bool extractable = true) 23 | { 24 | var keyUsages = new string[] { "deriveBits", "deriveKey" }; 25 | var key = await SubtleCrypto!.GenerateKey(new EcKeyGenParams { Name = Algorithm.ECDH, NamedCurve = namedCurve }, extractable, keyUsages); 26 | return new BrowserECDHKey(key, namedCurve, extractable, keyUsages); 27 | } 28 | /// 29 | /// Exports the public key in Spki format 30 | /// 31 | /// 32 | /// 33 | /// 34 | public override async Task ExportPublicKeySpki(PortableECDHKey key) 35 | { 36 | if (key is not BrowserECDHKey keyJS) throw new NotImplementedException(); 37 | var publicKey = await keyJS.Key.Get_PublicKey(); 38 | await using var arrayBuffer = await SubtleCrypto.ExportKeySpki(publicKey!); 39 | return await arrayBuffer.ReadBytes(); 40 | } 41 | /// 42 | /// Exports the private key in Pkcs8 format 43 | /// 44 | /// 45 | /// 46 | /// 47 | public override async Task ExportPrivateKeyPkcs8(PortableECDHKey key) 48 | { 49 | if (key is not BrowserECDHKey keyJS) throw new NotImplementedException(); 50 | await using var privateKey = await keyJS.Key.Get_PrivateKey(); 51 | await using var arrayBuffer = await SubtleCrypto!.ExportKeyPkcs8(privateKey!); 52 | return await arrayBuffer.ReadBytes(); 53 | } 54 | /// 55 | /// Import an ECDH public key 56 | /// 57 | /// 58 | /// 59 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
60 | /// P-256
61 | /// P-384
62 | /// P-521 63 | /// 64 | /// 65 | /// 66 | /// 67 | public override async Task ImportECDHKey(byte[] publicKeySpki, string namedCurve = NamedCurve.P521, bool extractable = true) 68 | { 69 | var keyUsages = new string[] { }; 70 | var publicKey = await SubtleCrypto!.ImportKey("spki", publicKeySpki, new EcKeyImportParams { Name = Algorithm.ECDH, NamedCurve = namedCurve }, extractable, keyUsages); 71 | var key = await CryptoKeyPairAsync.New(JSA); 72 | await key.Set_PublicKey(publicKey); 73 | return new BrowserECDHKey(key, namedCurve, extractable, keyUsages); 74 | } 75 | /// 76 | /// Import an ECDH public and private key 77 | /// 78 | /// 79 | /// 80 | /// 81 | /// A string representing the name of the elliptic curve to use. This may be any of the following names for NIST-approved curves:
82 | /// P-256
83 | /// P-384
84 | /// P-521 85 | /// 86 | /// 87 | /// 88 | /// 89 | public override async Task ImportECDHKey(byte[] publicKeySpki, byte[] privateKeyPkcs8, string namedCurve = NamedCurve.P521, bool extractable = true) 90 | { 91 | var keyUsages = new string[] { "deriveBits", "deriveKey" }; 92 | var privateKey = await SubtleCrypto!.ImportKey("pkcs8", privateKeyPkcs8, new EcKeyImportParams { Name = Algorithm.ECDH, NamedCurve = namedCurve }, extractable, keyUsages); 93 | var publicKey = await SubtleCrypto!.ImportKey("spki", publicKeySpki, new EcKeyImportParams { Name = Algorithm.ECDH, NamedCurve = namedCurve }, extractable, new string[] { }); 94 | var key = await CryptoKeyPairAsync.New(JSA); 95 | await key.Set_PublicKey(publicKey); 96 | await key.Set_PrivateKey(privateKey); 97 | return new BrowserECDHKey(key, namedCurve, extractable, keyUsages); 98 | } 99 | /// 100 | /// Creates a shared secret that is cross-platform compatible 101 | /// 102 | /// 103 | /// 104 | /// Number of bits to derive.
For compatibility, this should be a multiple of 8 105 | /// 106 | /// 107 | /// 108 | /// 109 | /// 110 | public override async Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey, int bitLength) 111 | { 112 | if (localPartyKey is not BrowserECDHKey localPartyKeyJS) throw new NotImplementedException(); 113 | if (otherPartyKey is not BrowserECDHKey otherPartyKeyJS) throw new NotImplementedException(); 114 | if (bitLength <= 0) throw new ArgumentOutOfRangeException(nameof(bitLength)); 115 | await using var localPartyPrivateKey = await localPartyKeyJS!.Key.Get_PrivateKey(); 116 | if (localPartyPrivateKey == null) throw new ArgumentNullException($"localPartyKey.Key.PrivateKey cannot be null"); 117 | await using var otherPartyPublicKey = await otherPartyKeyJS!.Key.Get_PublicKey(); 118 | if (otherPartyPublicKey == null) throw new ArgumentNullException($"otherPartyKey.Key.PublicKey cannot be null"); 119 | await using var sharedSecret = await SubtleCrypto!.DeriveBits(new EcdhKeyDeriveParamsAsync { Public = otherPartyPublicKey }, localPartyPrivateKey, bitLength); 120 | var sharedSecretBytes = await sharedSecret.ReadBytes(); 121 | return sharedSecretBytes; 122 | } 123 | /// 124 | /// Creates a shared secret that is cross-platform compatible 125 | /// 126 | /// 127 | /// 128 | /// 129 | /// 130 | public override Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey) 131 | { 132 | // chooses the largest bit length divisible by 8 for best compatibility as recommended in MDN SubtleCrypto docs 133 | var bitLength = NamedCurveBitLength(localPartyKey.NamedCurve, true); 134 | return DeriveBits(localPartyKey, otherPartyKey, bitLength); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/DotNet/DotNetCryptoAESGCM.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.Cryptography.DotNet; 2 | using System.Buffers.Binary; 3 | using System.Security.Cryptography; 4 | 5 | namespace SpawnDev.BlazorJS.Cryptography 6 | { 7 | public partial class DotNetCrypto 8 | { 9 | /// 10 | /// Generate an AES-GCM key using a secret byte array 11 | /// 12 | /// The secret that will be used to generate the key
The salt will be taken from the end of the secret 13 | /// 14 | /// A Number representing the number of times the hash function will be executed during key creation. This determines how computationally expensive (that is, slow) the key creation operation will be. In this context, slow is good, since it makes it more expensive for an attacker to run a dictionary attack against the keys. The general guidance here is to use as many iterations as possible, subject to keeping an acceptable level of performance for your application. 15 | /// 16 | /// 17 | /// A string representing the digest algorithm to use. This may be one of:
18 | /// SHA-256
19 | /// SHA-384
20 | /// SHA-512 21 | /// 22 | /// The length in bits of the key to generate. This must be one of: 16, 24, or 32 (128, 192, or 256 bits). 23 | /// The size of the tag, in bytes, that encryption and decryption must use. 24 | /// The size of the nonce, in bytes, that encryption and decryption must use. 25 | /// A boolean value indicating whether it will be possible to export the key (Browser environment only) 26 | /// PortableAESGCMKey 27 | /// 28 | public override Task GenerateAESGCMKey(byte[] secret, int iterations = 25000, string hashName = HashName.SHA256, int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true) 29 | { 30 | if (secret.Length < keySizeBytes * 2) throw new Exception($"{nameof(secret)}.Length must be at least {nameof(keySizeBytes)} * 2"); 31 | var salt = secret[^keySizeBytes..]; 32 | secret = secret[..keySizeBytes]; 33 | return GenerateAESGCMKey(secret, salt, iterations, hashName, keySizeBytes, tagSizeBytes, nonceSizeBytes, extractable); 34 | } 35 | /// 36 | /// Generate an AES-GCM key using a secret byte array and a salt 37 | /// 38 | /// The secret that will be used to generate the key 39 | /// This should be a random or pseudo-random value of at least 16 bytes 40 | /// 41 | /// A Number representing the number of times the hash function will be executed during key creation. This determines how computationally expensive (that is, slow) the key creation operation will be. In this context, slow is good, since it makes it more expensive for an attacker to run a dictionary attack against the keys. The general guidance here is to use as many iterations as possible, subject to keeping an acceptable level of performance for your application. 42 | /// 43 | /// 44 | /// A string representing the digest algorithm to use. This may be one of:
45 | /// SHA-256
46 | /// SHA-384
47 | /// SHA-512 48 | /// 49 | /// The length in bits of the key to generate. This must be one of: 16, 24, or 32 (128, 192, or 256 bits). 50 | /// The size of the tag, in bytes, that encryption and decryption must use. 51 | /// The size of the nonce, in bytes, that encryption and decryption must use. 52 | /// A boolean value indicating whether it will be possible to export the key (a value of false is supported in the Browser environment only) 53 | /// PortableAESGCMKey 54 | /// 55 | public override Task GenerateAESGCMKey(byte[] secret, byte[] salt, int iterations = 25000, string hashName = HashName.SHA256, int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true) 56 | { 57 | var hashAlgorithm = HashNameToHashAlgorithmName(hashName); 58 | // get encrypted message and decrypt using shared secret 59 | using var pbkdf2 = new Rfc2898DeriveBytes(secret, salt, iterations, hashAlgorithm); 60 | var key = new AesGcm(pbkdf2.GetBytes(keySizeBytes), tagSizeBytes); 61 | return Task.FromResult(new DotNetAESGCMKey(key, nonceSizeBytes)); 62 | } 63 | /// 64 | /// Encrypt data using an AES-GCM key 65 | /// 66 | /// 67 | /// 68 | /// 69 | /// 70 | public override Task Encrypt(PortableAESGCMKey key, byte[] plainBytes) 71 | { 72 | if (key is not DotNetAESGCMKey keyNet) throw new NotImplementedException(); 73 | var tagSize = key.TagSizeBytes; 74 | var nonceSize = key.NonceSizeBytes; 75 | int cipherDataSize = plainBytes.Length; 76 | int encryptedDataLength = 8 + nonceSize + cipherDataSize + tagSize; 77 | var nonce = RandomBytes(nonceSize); 78 | // 79 | var cipherData = new byte[cipherDataSize]; 80 | var tag = new byte[tagSize]; 81 | keyNet!.Key.Encrypt(nonce, plainBytes, cipherData, tag); 82 | // .Net, unlike SubtleCrypto, does not append the tag data to the cipherData 83 | // encryptedData = nonceSize + nonce + tagSize + cipherData + tag 84 | var encryptedData = new byte[encryptedDataLength]; 85 | // + nonceSize 86 | BinaryPrimitives.WriteInt32LittleEndian(new Span(encryptedData, 0, 4), nonceSize); 87 | // + nonce 88 | nonce.CopyTo(encryptedData, 4); 89 | // + tagSize 90 | BinaryPrimitives.WriteInt32LittleEndian(new Span(encryptedData, (4 + nonceSize), 4), tagSize); 91 | // + cipherData 92 | cipherData.CopyTo(encryptedData, 8 + nonceSize); 93 | // + tag 94 | tag.CopyTo(encryptedData, 4 + nonceSize + 4 + cipherDataSize); 95 | return Task.FromResult(encryptedData); 96 | } 97 | /// 98 | /// Decrypt data using an AES-GCM key 99 | /// 100 | /// 101 | /// 102 | /// 103 | /// 104 | public override Task Decrypt(PortableAESGCMKey key, byte[] encryptedData) 105 | { 106 | if (key is not DotNetAESGCMKey keyNet) throw new NotImplementedException(); 107 | // encryptedData = nonceSize + nonce + tagSize + cipherData + tag 108 | // get nonceSize 109 | var nonceSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData); 110 | // get nonce 111 | var nonce = new byte[nonceSize]; 112 | Buffer.BlockCopy(encryptedData, 4, nonce, 0, nonceSize); 113 | // get tagSize 114 | var tagSizeBytes = encryptedData[(4 + nonceSize)..(8 + nonceSize)]; 115 | var tagSize = BinaryPrimitives.ReadInt32LittleEndian(tagSizeBytes); 116 | // 117 | // get cipherData 118 | var cipherDataSize = encryptedData.Length - (8 + nonceSize + tagSize); 119 | var cipherData = new byte[cipherDataSize]; 120 | Buffer.BlockCopy(encryptedData, 8 + nonceSize, cipherData, 0, cipherDataSize); 121 | // get tag 122 | var tag = new byte[tagSize]; 123 | Buffer.BlockCopy(encryptedData, 8 + nonceSize + cipherDataSize, tag, 0, tagSize); 124 | // decrypt 125 | var plaintext = new byte[cipherDataSize]; 126 | keyNet!.Key.Decrypt(nonce, cipherData, tag, plaintext); 127 | return Task.FromResult(plaintext); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMCryptoAESGCM.cs: -------------------------------------------------------------------------------- 1 | 2 | using SpawnDev.BlazorJS.Cryptography.BrowserWASM; 3 | using SpawnDev.BlazorJS.JSObjects; 4 | using System.Buffers.Binary; 5 | 6 | namespace SpawnDev.BlazorJS.Cryptography 7 | { 8 | public partial class BrowserWASMCrypto 9 | { 10 | /// 11 | /// Generate an AES-GCM key using a secret byte array 12 | /// 13 | /// The secret that will be used to generate the key
The salt will be taken from the end of the secret 14 | /// 15 | /// A Number representing the number of times the hash function will be executed during key creation. This determines how computationally expensive (that is, slow) the key creation operation will be. In this context, slow is good, since it makes it more expensive for an attacker to run a dictionary attack against the keys. The general guidance here is to use as many iterations as possible, subject to keeping an acceptable level of performance for your application. 16 | /// 17 | /// 18 | /// A string representing the digest algorithm to use. This may be one of:
19 | /// SHA-256
20 | /// SHA-384
21 | /// SHA-512 22 | /// 23 | /// The length in bits of the key to generate. This must be one of: 16, 24, or 32 (128, 192, or 256 bits). 24 | /// The size of the tag, in bytes, that encryption and decryption must use. 25 | /// The size of the nonce, in bytes, that encryption and decryption must use. 26 | /// A boolean value indicating whether it will be possible to export the key (Browser environment only) 27 | /// PortableAESGCMKey 28 | /// 29 | public override Task GenerateAESGCMKey(byte[] secret, int iterations = 25000, string hashName = HashName.SHA256, int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true) 30 | { 31 | if (secret.Length < keySizeBytes * 2) throw new Exception($"{nameof(secret)}.Length must be at least {nameof(keySizeBytes)} * 2"); 32 | var salt = secret[^keySizeBytes..]; 33 | secret = secret[..keySizeBytes]; 34 | return GenerateAESGCMKey(secret, salt, iterations, hashName, keySizeBytes, tagSizeBytes, nonceSizeBytes, extractable); 35 | } 36 | /// 37 | /// Generate an AES-GCM key using a secret byte array and a salt 38 | /// 39 | /// The secret that will be used to generate the key 40 | /// This should be a random or pseudo-random value of at least 16 bytes 41 | /// 42 | /// A Number representing the number of times the hash function will be executed during key creation. This determines how computationally expensive (that is, slow) the key creation operation will be. In this context, slow is good, since it makes it more expensive for an attacker to run a dictionary attack against the keys. The general guidance here is to use as many iterations as possible, subject to keeping an acceptable level of performance for your application. 43 | /// 44 | /// 45 | /// A string representing the digest algorithm to use. This may be one of:
46 | /// SHA-256
47 | /// SHA-384
48 | /// SHA-512 49 | /// 50 | /// The length in bits of the key to generate. This must be one of: 16, 24, or 32 (128, 192, or 256 bits). 51 | /// The size of the tag, in bytes, that encryption and decryption must use. 52 | /// The size of the nonce, in bytes, that encryption and decryption must use. 53 | /// A boolean value indicating whether it will be possible to export the key (a value of false is supported in the Browser environment only) 54 | /// PortableAESGCMKey 55 | /// 56 | public override async Task GenerateAESGCMKey(byte[] secret, byte[] salt, int iterations = 25000, string hashName = HashName.SHA256, int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true) 57 | { 58 | var keyUsages = new string[] { "encrypt", "decrypt" }; 59 | var pbkKey = await SubtleCrypto!.ImportKey("raw", secret, "PBKDF2", false, new string[] { "deriveKey" }); 60 | var key = await SubtleCrypto!.DeriveKey( 61 | new Pbkdf2Params 62 | { 63 | Hash = hashName, 64 | Iterations = iterations, 65 | Salt = salt, 66 | }, 67 | pbkKey, 68 | new AesKeyGenParams 69 | { 70 | Name = Algorithm.AESGCM, 71 | Length = keySizeBytes * 8, 72 | }, 73 | extractable, 74 | keyUsages 75 | ); 76 | return new BrowserWASMAESGCMKey(key, nonceSizeBytes, tagSizeBytes); 77 | } 78 | /// 79 | /// Encrypt data using an AES-GCM key 80 | /// 81 | /// 82 | /// 83 | /// 84 | /// 85 | public override async Task Encrypt(PortableAESGCMKey key, byte[] plainBytes) 86 | { 87 | if (key is not BrowserWASMAESGCMKey jsKey) throw new NotImplementedException(); 88 | var tagSize = key.TagSizeBytes; 89 | var nonceSize = key.NonceSizeBytes; 90 | int cipherDataSize = plainBytes.Length; 91 | int encryptedDataLength = 8 + nonceSize + cipherDataSize + tagSize; 92 | var nonce = RandomBytes(nonceSize); 93 | // 94 | using var ret = await SubtleCrypto!.Encrypt(new AesGcmParams { Iv = nonce, TagLength = tagSize * 8 }, jsKey!.Key, plainBytes); 95 | var cipherDataAndTag = ret.ReadBytes(); 96 | // SubtleCrypto, unlike .Net, appends the tag data to the cipherData 97 | // encryptedData = nonceSize + nonce + tagSize + cipherDataAndTag 98 | var encryptedData = new byte[encryptedDataLength]; 99 | // + nonceSize 100 | BinaryPrimitives.WriteInt32LittleEndian(new Span(encryptedData, 0, 4), nonceSize); 101 | // + nonce 102 | nonce.CopyTo(encryptedData, 4); 103 | // + tagSize 104 | BinaryPrimitives.WriteInt32LittleEndian(new Span(encryptedData, (4 + nonceSize), 4), tagSize); 105 | // + cipherDataAndTag 106 | cipherDataAndTag.CopyTo(encryptedData, 8 + nonceSize); 107 | return encryptedData; 108 | } 109 | /// 110 | /// Decrypt data using an AES-GCM key 111 | /// 112 | /// 113 | /// 114 | /// 115 | /// 116 | public override async Task Decrypt(PortableAESGCMKey key, byte[] encryptedData) 117 | { 118 | if (key is not BrowserWASMAESGCMKey jsKey) throw new NotImplementedException(); 119 | // encryptedData = nonceSize + nonce + tagSize + cipherData + tag 120 | // get nonceSize 121 | var nonceSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData); 122 | // get nonce 123 | var nonce = new byte[nonceSize]; 124 | Buffer.BlockCopy(encryptedData, 4, nonce, 0, nonceSize); 125 | // get tagSize 126 | var tagSizeBytes = encryptedData[(4 + nonceSize)..(8 + nonceSize)]; 127 | var tagSize = BinaryPrimitives.ReadInt32LittleEndian(tagSizeBytes); 128 | // 129 | // get cipherDataAndTag 130 | var cipherDataAndTagSize = encryptedData.Length - (8 + nonceSize); 131 | var cipherDataAndTag = new byte[cipherDataAndTagSize]; 132 | Buffer.BlockCopy(encryptedData, 8 + nonceSize, cipherDataAndTag, 0, cipherDataAndTagSize); 133 | // decrypt 134 | var ret = await SubtleCrypto!.Decrypt(new AesGcmParams { Iv = nonce, TagLength = tagSize * 8 }, jsKey!.Key, cipherDataAndTag); 135 | return ret.ReadBytes(); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserCryptoAESGCM.cs: -------------------------------------------------------------------------------- 1 | 2 | using SpawnDev.BlazorJS.Cryptography.Browser; 3 | using SpawnDev.BlazorJS.JSObjects; 4 | using System.Buffers.Binary; 5 | 6 | namespace SpawnDev.BlazorJS.Cryptography 7 | { 8 | public partial class BrowserCrypto 9 | { 10 | /// 11 | /// Generate an AES-GCM key using a secret byte array 12 | /// 13 | /// The secret that will be used to generate the key
The salt will be taken from the end of the secret 14 | /// 15 | /// A Number representing the number of times the hash function will be executed during key creation. This determines how computationally expensive (that is, slow) the key creation operation will be. In this context, slow is good, since it makes it more expensive for an attacker to run a dictionary attack against the keys. The general guidance here is to use as many iterations as possible, subject to keeping an acceptable level of performance for your application. 16 | /// 17 | /// 18 | /// A string representing the digest algorithm to use. This may be one of:
19 | /// SHA-256
20 | /// SHA-384
21 | /// SHA-512 22 | /// 23 | /// The length in bits of the key to generate. This must be one of: 16, 24, or 32 (128, 192, or 256 bits). 24 | /// The size of the tag, in bytes, that encryption and decryption must use. 25 | /// The size of the nonce, in bytes, that encryption and decryption must use. 26 | /// A boolean value indicating whether it will be possible to export the key (Browser environment only) 27 | /// PortableAESGCMKey 28 | /// 29 | public override Task GenerateAESGCMKey(byte[] secret, int iterations = 25000, string hashName = HashName.SHA256, int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true) 30 | { 31 | if (secret.Length < keySizeBytes * 2) throw new Exception($"{nameof(secret)}.Length must be at least {nameof(keySizeBytes)} * 2"); 32 | var salt = secret[^keySizeBytes..]; 33 | secret = secret[..keySizeBytes]; 34 | return GenerateAESGCMKey(secret, salt, iterations, hashName, keySizeBytes, tagSizeBytes, nonceSizeBytes, extractable); 35 | } 36 | /// 37 | /// Generate an AES-GCM key using a secret byte array and a salt 38 | /// 39 | /// The secret that will be used to generate the key 40 | /// This should be a random or pseudo-random value of at least 16 bytes 41 | /// 42 | /// A Number representing the number of times the hash function will be executed during key creation. This determines how computationally expensive (that is, slow) the key creation operation will be. In this context, slow is good, since it makes it more expensive for an attacker to run a dictionary attack against the keys. The general guidance here is to use as many iterations as possible, subject to keeping an acceptable level of performance for your application. 43 | /// 44 | /// 45 | /// A string representing the digest algorithm to use. This may be one of:
46 | /// SHA-256
47 | /// SHA-384
48 | /// SHA-512 49 | /// 50 | /// The length in bits of the key to generate. This must be one of: 16, 24, or 32 (128, 192, or 256 bits). 51 | /// The size of the tag, in bytes, that encryption and decryption must use. 52 | /// The size of the nonce, in bytes, that encryption and decryption must use. 53 | /// A boolean value indicating whether it will be possible to export the key (a value of false is supported in the Browser environment only) 54 | /// PortableAESGCMKey 55 | /// 56 | public override async Task GenerateAESGCMKey(byte[] secret, byte[] salt, int iterations = 25000, string hashName = HashName.SHA256, int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true) 57 | { 58 | var keyUsages = new string[] { "encrypt", "decrypt" }; 59 | //var pbkKey = await SubtleCrypto!.ImportKey("raw", secret, "PBKDF2", false, new string[] { "deriveKey" }); 60 | await using var pbkKey = await SubtleCrypto.ImportKey("raw", secret, "PBKDF2", false, new string[] { "deriveKey" }); 61 | var key = await SubtleCrypto.DeriveKey( 62 | new Pbkdf2Params 63 | { 64 | Hash = hashName, 65 | Iterations = iterations, 66 | Salt = salt, 67 | }, 68 | pbkKey, 69 | new AesKeyGenParams 70 | { 71 | Name = Algorithm.AESGCM, 72 | Length = keySizeBytes * 8, 73 | }, 74 | extractable, 75 | keyUsages 76 | ); 77 | return new BrowserAESGCMKey(key, nonceSizeBytes, tagSizeBytes, extractable, keyUsages); 78 | } 79 | /// 80 | /// Encrypt data using an AES-GCM key 81 | /// 82 | /// 83 | /// 84 | /// 85 | /// 86 | public override async Task Encrypt(PortableAESGCMKey key, byte[] plainBytes) 87 | { 88 | if (key is not BrowserAESGCMKey jsKey) throw new NotImplementedException(); 89 | var tagSize = key.TagSizeBytes; 90 | var nonceSize = key.NonceSizeBytes; 91 | int cipherDataSize = plainBytes.Length; 92 | int encryptedDataLength = 8 + nonceSize + cipherDataSize + tagSize; 93 | var nonce = RandomBytes(nonceSize); 94 | // 95 | await using var ret = await SubtleCrypto.Encrypt(new AesGcmParams { Iv = nonce, TagLength = tagSize * 8 }, jsKey!.Key, plainBytes); 96 | var cipherDataAndTag = await ret.ReadBytes(); 97 | //using var ret = await SubtleCrypto!.Encrypt(new AesGcmParams { Iv = nonce, TagLength = tagSize * 8 }, jsKey!.Key, plainBytes); 98 | //var cipherDataAndTag = ret.ReadBytes(); 99 | // SubtleCrypto, unlike .Net, appends the tag data to the cipherData 100 | // encryptedData = nonceSize + nonce + tagSize + cipherDataAndTag 101 | var encryptedData = new byte[encryptedDataLength]; 102 | // + nonceSize 103 | BinaryPrimitives.WriteInt32LittleEndian(new Span(encryptedData, 0, 4), nonceSize); 104 | // + nonce 105 | nonce.CopyTo(encryptedData, 4); 106 | // + tagSize 107 | BinaryPrimitives.WriteInt32LittleEndian(new Span(encryptedData, (4 + nonceSize), 4), tagSize); 108 | // + cipherDataAndTag 109 | cipherDataAndTag.CopyTo(encryptedData, 8 + nonceSize); 110 | return encryptedData; 111 | } 112 | /// 113 | /// Decrypt data using an AES-GCM key 114 | /// 115 | /// 116 | /// 117 | /// 118 | /// 119 | public override async Task Decrypt(PortableAESGCMKey key, byte[] encryptedData) 120 | { 121 | if (key is not BrowserAESGCMKey jsKey) throw new NotImplementedException(); 122 | // encryptedData = nonceSize + nonce + tagSize + cipherData + tag 123 | // get nonceSize 124 | var nonceSize = BinaryPrimitives.ReadInt32LittleEndian(encryptedData); 125 | // get nonce 126 | var nonce = new byte[nonceSize]; 127 | Buffer.BlockCopy(encryptedData, 4, nonce, 0, nonceSize); 128 | // get tagSize 129 | var tagSizeBytes = encryptedData[(4 + nonceSize)..(8 + nonceSize)]; 130 | var tagSize = BinaryPrimitives.ReadInt32LittleEndian(tagSizeBytes); 131 | // 132 | // get cipherDataAndTag 133 | var cipherDataAndTagSize = encryptedData.Length - (8 + nonceSize); 134 | var cipherDataAndTag = new byte[cipherDataAndTagSize]; 135 | Buffer.BlockCopy(encryptedData, 8 + nonceSize, cipherDataAndTag, 0, cipherDataAndTagSize); 136 | // decrypt 137 | await using var ret = await SubtleCrypto.Decrypt(new AesGcmParams { Iv = nonce, TagLength = tagSize * 8 }, jsKey!.Key, cipherDataAndTag); 138 | var plainData = await ret.ReadBytes(); 139 | return plainData; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpawnDev.BlazorJS.Cryptography 2 | 3 | [![NuGet](https://badge.fury.io/nu/SpawnDev.BlazorJS.Cryptography.svg?delta=9&label=SpawnDev.BlazorJS.Cryptography)](https://www.nuget.org/packages/SpawnDev.BlazorJS.Cryptography) 4 | 5 | .Net cryptography library for Blazor, .Net Web APIs, and .Net apps. Supports browser and non-browser platforms. 6 | 7 | ### The problem this library solves 8 | Most of Microsoft's System.Security.Cryptography library is marked `[UnsupportedOSPlatform("browser")]`. To work around this limitation, the browser's built in [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) API is used when running in the browser and Microsoft's System.Security.Cryptography libraries are used when running on non-browser platforms. 9 | 10 | ### Features 11 | - AES-GCM - symmetric encryption and decryption 12 | - ECDH - shared secret generation (enables asymmetric encryption) 13 | - ECDSA - data signing and verification 14 | - SHA - data hashing 15 | 16 | ### PortableCrypto Classes 17 | The classes `DotNetCrypto`, `BrowserCrypto`, and `BrowserWASMCrypto` all inherit from [`PortableCrypto`](#portablecrypto-abstract-class) to provide a shared interface to common cryptography methods regardless of the platform the app is being executed on. 18 | 19 | ### IPortableCrypto Interface 20 | PortableCrypto implements the IPortableCrypto interface. Therefore all implementing classes, `DotNetCrypto`, `BrowserCrypto`, and `BrowserWASMCrypto`, implement it also. 21 | 22 | **DotNetCrypto** 23 | - Uses .Net System.Security.Cryptography on the executing platform 24 | - Browser platform not supported 25 | - Supports non-browser platforms (windows, linux, etc) 26 | - Targets Blazor server, .Net Web APIs, any non-browser platform .Net Apps 27 | 28 | **BrowserCrypto** 29 | - Uses IJSRuntime to access the browser's [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) API 30 | - Supports both server rendering and WebAssembly rendering modes 31 | - Targets the browser platform via Blazor server or Blazor WebAssembly 32 | 33 | **BrowserWASMCrypto** 34 | - Uses IJInProcessSRuntime to access the browser's [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) API 35 | - Supports only WebAssembly rendering 36 | - Targets the browser via Blazor WebAssembly 37 | 38 | ### Getting started 39 | 40 | Add the Nuget package 41 | ```nuget 42 | dotnet add package SpawnDev.BlazorJS.Cryptography 43 | ``` 44 | 45 | #### Web API Server Project 46 | Web API Server Program.cs 47 | ```cs 48 | // Crypto for the server. Uses System.Security.Cryptography. 49 | builder.Services.AddSingleton(); 50 | ``` 51 | 52 | #### Blazor Server Project 53 | Blazor Server Program.cs 54 | ```cs 55 | 56 | // Add BlazorJSRuntime service 57 | builder.Services.AddBlazorJSRuntime(); 58 | 59 | // Crypto for the server. Uses System.Security.Cryptography. 60 | // Runs on the server. 61 | builder.Services.AddSingleton(); 62 | 63 | // Crypto for the browser. Uses the browser's SubtleCrypto API via IJSRuntime. 64 | // Supports both server rendering and WebAssembly rendering modes. Runs in the browser. 65 | builder.Services.AddScoped(); 66 | ``` 67 | 68 | #### Blazor WebAssembly 69 | WebAssembly Program.cs 70 | ```cs 71 | // Add BlazorJSRuntime service 72 | builder.Services.AddBlazorJSRuntime(); 73 | 74 | // Crypto for the browser. Uses the browser's SubtleCrypto API via IJSRuntime. 75 | // Supports both server rendering and WebAssembly rendering modes 76 | builder.Services.AddScoped(); 77 | 78 | // OR 79 | 80 | // Crypto for the browser. Uses the browser's SubtleCrypto API via IJSInProcessRuntime. 81 | // Supports only WebAssembly rendering 82 | builder.Services.AddScoped(); 83 | ``` 84 | 85 | ### SHA Example 86 | - The below example, taken from the demo project, runs in Blazor server side rendering to test SHA hashing using the DotNetCrypto on the server and BrowserCrypto using IJSRuntime to run on the client browser. 87 | ```cs 88 | var data = new byte[] { 0, 1, 2 }; 89 | // - Server 90 | // DotNetCrypto indicated by the appended D, executes on the server using Microsoft.Security.Cryptography 91 | var hashD = await DotNetCrypto.Digest("SHA-512", data); 92 | 93 | // - Browser 94 | // BrowserCrypto indicated by the appended B, executes on the browser using Javascript's SubtleCrypto APIs 95 | var hashB = await BrowserCrypto.Digest("SHA-512", data); 96 | 97 | // verify the hashes match 98 | if (!hashB.SequenceEqual(hashD)) 99 | { 100 | throw new Exception("Hash mismatch"); 101 | } 102 | ``` 103 | 104 | ### ECDH Example 105 | - The below example, taken from the demo project, runs in Blazor server side rendering to test ECDH using the DotNetCrypto on the server and BrowserCrypto using IJSRuntime to run on the client browser. 106 | ```cs 107 | // - Server 108 | // generate server ECDH key 109 | var ecdhD = await DotNetCrypto.GenerateECDHKey(); 110 | // export ecdhD public key for browser to use 111 | var ecdhDPublicKeyBytes = await DotNetCrypto.ExportPublicKeySpki(ecdhD); 112 | 113 | // - Browser 114 | // generate browser ECDH key 115 | var ecdhB = await BrowserCrypto.GenerateECDHKey(); 116 | // export ecdhB public key for server to use 117 | var ecdhBPublicKeyBytes = await BrowserCrypto.ExportPublicKeySpki(ecdhB); 118 | 119 | // - Server 120 | // import the browser's ECDH public key using DotNetCrypto so DotNetCrypto can work with it 121 | var ecdhBPublicKeyD = await DotNetCrypto.ImportECDHKey(ecdhBPublicKeyBytes); 122 | // create shared secret 123 | var sharedSecretD = await DotNetCrypto.DeriveBits(ecdhD, ecdhBPublicKeyD); 124 | 125 | // - Browser 126 | // import the server's ECDH public key using BrowserCrypto so BrowserCrypto can work with it 127 | var ecdhDPublicKeyB = await BrowserCrypto.ImportECDHKey(ecdhDPublicKeyBytes); 128 | // create shared secret 129 | var sharedSecretB = await BrowserCrypto.DeriveBits(ecdhB, ecdhDPublicKeyB); 130 | 131 | // verify the shared secrets match 132 | if (!sharedSecretB.SequenceEqual(sharedSecretD)) 133 | { 134 | throw new Exception("Shared secret mismatch"); 135 | } 136 | ``` 137 | 138 | ## PortableCrypto Abstract Class 139 | 140 | ### SHA - Data Hashing 141 | 142 | #### `Task Digest(string hashName, byte[] data)` 143 | - Hash the specified data using the specified hash algorithm 144 | 145 | ### ECDH - Shared secret generation 146 | 147 | #### `Task GenerateECDHKey(string namedCurve = NamedCurve.P521, bool extractable = true)` 148 | - Generate a new ECDH crypto key 149 | 150 | #### `Task ExportPublicKeySpki(PortableECDHKey key)` 151 | - Export the ECDH public key in Spki format 152 | 153 | #### `Task ExportPrivateKeyPkcs8(PortableECDHKey key)` 154 | - Export the ECDH private key in Pkcs8 format 155 | 156 | #### `Task ImportECDHKey(byte[] publicKeySpki, string namedCurve = NamedCurve.P521, bool extractable = true)` 157 | - Import the ECDH public key 158 | 159 | #### `Task ImportECDHKey(byte[] publicKeySpki, byte[] privateKeyPkcs8, string namedCurve = NamedCurve.P521, bool extractable = true)` 160 | - Import the ECDH private key 161 | 162 | #### `Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey, int bitLength)` 163 | - Create a shared secret that is cross-platform compatible 164 | 165 | #### `Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey)` 166 | - Create a shared secret that is cross-platform compatible 167 | 168 | ### ECDSA - Data Signing 169 | 170 | #### `Task GenerateECDSAKey(string namedCurve = NamedCurve.P521, bool extractable = true)` 171 | - Generate a new ECDSA key 172 | 173 | #### `Task ExportPublicKeySpki(PortableECDSAKey key)` 174 | - Exports the ECDSA public key in Spki format 175 | 176 | #### `Task ExportPrivateKeyPkcs8(PortableECDSAKey key)` 177 | - Exports the ECDSA private key in Pkcs8 format 178 | 179 | #### `Task ImportECDSAKey(byte[] publicKeySpkiData, string namedCurve = NamedCurve.P521, bool extractable = true)` 180 | - Import an ECDSA public key 181 | 182 | #### `Task ImportECDSAKey(byte[] publicKeySpkiData, byte[] privateKeyPkcs8Data, string namedCurve = NamedCurve.P521, bool extractable = true)` 183 | - Import an ECDSA public and private key 184 | 185 | #### `Task Verify(PortableECDSAKey key, byte[] data, byte[] signature, string hashName = HashName.SHA512)` 186 | - Verify a data signature 187 | 188 | #### `Task Sign(PortableECDSAKey key, byte[] data, string hashName = HashName.SHA512)` 189 | - Sign data using an ECDSA key 190 | 191 | ### AES-GCM - Data Encryption 192 | 193 | #### `Task GenerateAESGCMKey(byte[] secret, int iterations = 25000, string hashName = HashName.SHA256, int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true)` 194 | - Generate an AES-GCM key using a secret byte array 195 | 196 | #### `Task GenerateAESGCMKey(byte[] secret, byte[] salt, int iterations = 25000, string hashName = HashName.SHA256, int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true)` 197 | - Generate an AES-GCM key using a secret byte array and a salt 198 | 199 | #### `Task Encrypt(PortableAESGCMKey key, byte[] plainBytes)` 200 | - Encrypt data using an AES-GCM key 201 | 202 | #### `Task Decrypt(PortableAESGCMKey key, byte[] encryptedData)` 203 | - Decrypt data using an AES-GCM key -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/BrowserWASM/BrowserWASMCryptoAESCBC.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.Cryptography.BrowserWASM; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | 4 | namespace SpawnDev.BlazorJS.Cryptography 5 | { 6 | public partial class BrowserWASMCrypto 7 | { 8 | /// 9 | /// Generate an AES-CBC key 10 | /// 11 | /// 12 | /// 13 | /// 14 | public override async Task GenerateAESCBCKey(int keySize, bool extractable = true) 15 | { 16 | var keyUsages = new string[] { "encrypt", "decrypt" }; 17 | var key = await SubtleCrypto.GenerateKey(new AesKeyGenParams { Name = Algorithm.AESCBC, Length = keySize }, extractable, keyUsages); 18 | return new BrowserWASMAESCBCKey(key); 19 | } 20 | /// 21 | /// Encrypt data using an AES-CBC key 22 | /// 23 | public async Task Encrypt(PortableAESCBCKey key, Uint8Array plainBytes, Uint8Array iv, bool prependIV = false, AESCBCPadding padding = AESCBCPadding.PKCS7) 24 | { 25 | if (key is not BrowserWASMAESCBCKey jsKey) throw new NotImplementedException(); 26 | if (padding == AESCBCPadding.None) 27 | { 28 | if (plainBytes.Length % AES_CBC_BLOCK_SIZE != 0) 29 | { 30 | throw new Exception($"{plainBytes} length must be a multiple of 16 when using no padding."); 31 | } 32 | } 33 | using var ret = await SubtleCrypto.Encrypt(new AesCbcParams { Iv = iv }, jsKey!.Key, plainBytes); 34 | var retUint8Array = new Uint8Array(ret); 35 | if (padding == AESCBCPadding.None) 36 | { 37 | // Trim PKCS#7 padding from the result. 38 | // The input must be a multiple of the block size when using no padding so the added padding size is equal to the block size 39 | var tmp = retUint8Array.SubArray(0, ret.ByteLength - AES_CBC_BLOCK_SIZE); 40 | retUint8Array.Dispose(); 41 | retUint8Array = tmp; 42 | } 43 | if (!prependIV) 44 | { 45 | return retUint8Array; 46 | } 47 | var encryptedDataLength = retUint8Array.Length + iv.Length; 48 | var result = new Uint8Array(encryptedDataLength); 49 | // + iv 50 | result.Set(iv, 0); 51 | // + encrypted data 52 | result.Set(retUint8Array, iv.Length); 53 | retUint8Array.Dispose(); 54 | return result; 55 | } 56 | /// 57 | /// Encrypt data using an AES-CBC key 58 | /// 59 | public override async Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, byte[] iv, bool prependIV = false, AESCBCPadding padding = AESCBCPadding.PKCS7) 60 | { 61 | using var ivUint8Array = new Uint8Array(iv); 62 | using var dataUint8Array = new Uint8Array(plainBytes); 63 | using var decrypted = await Encrypt(key, dataUint8Array, ivUint8Array, prependIV, padding); 64 | return decrypted.ReadBytes(); 65 | } 66 | /// 67 | /// Encrypt data using an AES-CBC key 68 | /// 69 | public override Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, bool prependIV = true, AESCBCPadding padding = AESCBCPadding.PKCS7) 70 | { 71 | if (key is not BrowserWASMAESCBCKey jsKey) throw new NotImplementedException(); 72 | var iv = RandomBytes(16); 73 | return Encrypt(key, plainBytes, iv, prependIV, padding); 74 | } 75 | /// 76 | /// Encrypt data using an AES-CBC key 77 | /// 78 | public async Task Encrypt(PortableAESCBCKey key, Uint8Array plainBytes, bool prependIV = true, AESCBCPadding padding = AESCBCPadding.PKCS7) 79 | { 80 | if (key is not BrowserWASMAESCBCKey jsKey) throw new NotImplementedException(); 81 | var iv = RandomBytes(16); 82 | using var ivUint8Array = new Uint8Array(iv); 83 | return await Encrypt(key, plainBytes, ivUint8Array, prependIV, padding); 84 | } 85 | /// 86 | /// Decrypt data using an AES-CBC key
87 | /// This method expects the IV to be supplied separately from the encrypted data 88 | ///
89 | public override async Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, byte[] iv, AESCBCPadding padding = AESCBCPadding.PKCS7) 90 | { 91 | using var ivUint8Array = new Uint8Array(iv); 92 | using var encryptedDataUint8Array = new Uint8Array(encryptedData); 93 | using var decrypted = await Decrypt(key, encryptedDataUint8Array, ivUint8Array, padding); 94 | return decrypted.ReadBytes(); 95 | } 96 | /// 97 | /// Decrypt data using an AES-CBC key
98 | /// This method expects the IV to be supplied separately from the encrypted data 99 | ///
100 | public async Task Decrypt(PortableAESCBCKey key, Uint8Array encryptedData, Uint8Array iv, AESCBCPadding padding = AESCBCPadding.PKCS7) 101 | { 102 | if (key is not BrowserWASMAESCBCKey jsKey) throw new NotImplementedException(); 103 | if (padding == AESCBCPadding.None) 104 | { 105 | var encryptedDataLength = encryptedData.ByteLength; 106 | // PKS7 padding must be added because SubtleCrypto's implementation of AES-CBC requires it 107 | if (encryptedDataLength % AES_CBC_BLOCK_SIZE != 0) 108 | { 109 | throw new Exception($"{encryptedData} length must be a multiple of 16 when using no padding."); 110 | } 111 | // create a Uint8Array to hold the padded data 112 | using var paddedData = new Uint8Array(AES_CBC_BLOCK_SIZE + encryptedDataLength); 113 | paddedData.Set(encryptedData, 0); 114 | // padding starts as a byte array of size paddingSize where each byte is the paddingSize 115 | using var paddingData = new Uint8Array(AES_CBC_BLOCK_SIZE); 116 | paddingData.FillVoid((byte)AES_CBC_BLOCK_SIZE); 117 | // use the last paddingSize bytes of data is the iv 118 | using var padBlockIv = encryptedData.Slice(-AES_CBC_BLOCK_SIZE); 119 | // encrypt the padding data 120 | using var padBlock = await Encrypt(jsKey, paddingData, padBlockIv, false, AESCBCPadding.None); 121 | paddedData.Set(padBlock, encryptedDataLength); 122 | // decrypt 123 | var decryptedArrayBuffer = await SubtleCrypto.Decrypt(new AesCbcParams { Iv = iv }, jsKey.Key, paddedData); 124 | // return the decrypted data as a Uint8Array 125 | return new Uint8Array(decryptedArrayBuffer); 126 | } 127 | else 128 | { 129 | using var arrayBuffer = await SubtleCrypto.Decrypt(new AesCbcParams { Iv = iv }, jsKey!.Key, encryptedData); 130 | return new Uint8Array(arrayBuffer); 131 | } 132 | } 133 | /// 134 | /// Decrypt data using an AES-CBC key
135 | /// This method expects the IV to be prepended to the encrypted data 136 | ///
137 | public override async Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, AESCBCPadding padding = AESCBCPadding.PKCS7) 138 | { 139 | using var encryptedDataUint8Array = new Uint8Array(encryptedData); 140 | using var decrypted = await Decrypt(key, encryptedDataUint8Array, padding); 141 | return decrypted.ReadBytes(); 142 | } 143 | /// 144 | /// Decrypt data using an AES-CBC key
145 | /// This method expects the IV to be prepended to the encrypted data 146 | ///
147 | public async Task Decrypt(PortableAESCBCKey key, Uint8Array encryptedData, AESCBCPadding padding = AESCBCPadding.PKCS7) 148 | { 149 | using var iv = encryptedData.Slice(0, 16); 150 | using var encrypted = encryptedData.Slice(16); 151 | return await Decrypt(key, encrypted, iv, padding); 152 | } 153 | /// 154 | /// Imports an AES-CBC key from a byte array 155 | /// 156 | /// 157 | /// 158 | /// 159 | public override async Task ImportAESCBCKey(byte[] rawKey, bool extractable = true) 160 | { 161 | var key = await SubtleCrypto.ImportKey("raw", rawKey, Algorithm.AESCBC, extractable, new string[] { "encrypt", "decrypt" }); 162 | return new BrowserWASMAESCBCKey(key); 163 | } 164 | /// 165 | /// Exports an AES-CBC key as a byte array 166 | /// 167 | /// 168 | /// 169 | /// 170 | public override async Task ExportAESCBCKey(PortableAESCBCKey key) 171 | { 172 | if (key is not BrowserWASMAESCBCKey jsKey) throw new NotImplementedException(); 173 | using var ret = await SubtleCrypto.ExportKeyRaw(jsKey.Key); 174 | return ret.ReadBytes(); 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography.Demo/SpawnDev.BlazorJS.Cryptography.Demo.Client/Pages/TestPage.razor: -------------------------------------------------------------------------------- 1 | @page "/test" 2 | @using SpawnDev.BlazorJS.RemoteJSRuntime 3 | @using System.Text 4 | @rendermode @(new InteractiveWebAssemblyRenderMode(prerender: false)) 5 | 6 | PortableCrypto Test 7 | 8 |

PortableCrypto Test

9 | 10 | 11 | 12 | 13 |
14 | ECDH public key:
15 |
@ECDHKeyB64
16 |
17 |
18 | ECDSA public key:
19 |
@ECDSAKeyB64
20 |
21 |
22 | Server ECDH public key:
23 |
@ServerECDHKeyB64
24 |
25 |
26 | Server ECDSA public key:
27 |
@ServerECDSAKeyB64
28 |
29 | 30 | 31 | @code { 32 | static string TestString = "Hello world!"; 33 | 34 | private int currentCount = 0; 35 | 36 | [Inject] 37 | HttpClient HttpClient { get; set; } 38 | 39 | [Inject] 40 | BrowserCrypto BrowserCrypto { get; set; } 41 | 42 | [Inject] 43 | BrowserWASMCrypto BrowserWASMCrypto { get; set; } 44 | 45 | [Inject] 46 | BlazorJSRuntimeAsync JSA { get; set; } 47 | 48 | // local ECDSA key for signing 49 | PortableECDSAKey? ECDSAKey = null; 50 | // local ECDH for shared secret creation 51 | PortableECDHKey? ECDHKey = null; 52 | // base 64 representation of local ECDSA public key 53 | string? ECDSAKeyB64 = null; 54 | // base 64 representation of local ECDH public key 55 | string? ECDHKeyB64 = null; 56 | 57 | // server's ECDSA key for verifying server signatures 58 | PortableECDSAKey? ServerECDSAKey = null; 59 | // server's ECDH key for creating a shared secret with the server 60 | PortableECDHKey? ServerECDHKey = null; 61 | // base 64 representation of the server's ECDSA public key 62 | string? ServerECDSAKeyB64 = null; 63 | // base 64 representation of the server's ECDH public key 64 | string? ServerECDHKeyB64 = null; 65 | 66 | int BusyCount = 0; 67 | bool Busy => BusyCount > 0; 68 | 69 | private async Task IncrementCount() 70 | { 71 | BusyCount++; 72 | StateHasChanged(); 73 | try 74 | { 75 | // create local ECDSA key for signing 76 | ECDSAKey = await BrowserCrypto.GenerateECDSAKey(); 77 | // create local ECDH for shared secret creation 78 | ECDHKey = await BrowserCrypto.GenerateECDHKey(); 79 | ECDSAKeyB64 = Convert.ToBase64String(await BrowserCrypto.ExportPublicKeySpki(ECDSAKey!)); 80 | ECDHKeyB64 = Convert.ToBase64String(await BrowserCrypto.ExportPublicKeySpki(ECDHKey!)); 81 | ServerECDSAKey = await BrowserCrypto.ImportECDSAKey(await Get("ecdsa")); 82 | ServerECDHKey = await BrowserCrypto.ImportECDHKey(await Get("ecdh")); 83 | ServerECDSAKeyB64 = Convert.ToBase64String(await BrowserCrypto.ExportPublicKeySpki(ServerECDSAKey!)); 84 | ServerECDHKeyB64 = Convert.ToBase64String(await BrowserCrypto.ExportPublicKeySpki(ServerECDHKey!)); 85 | // derive a shared secret using the other party's (server's) public ECDH key and this party's (browser's) private ECDH key 86 | // the shared secret should be identical on both sides 87 | var sharedSecret = await BrowserCrypto.DeriveBits(ECDHKey, ServerECDHKey); 88 | // get the shared secret from the server to verify it matches (you would never do this in production) 89 | var serversSharedSecret = await Post("GetSharedSecret", new GetSharedSecretArgs { SenderECDHPublicKeyB64 = ECDHKeyB64 }); 90 | if (serversSharedSecret == null || !sharedSecret.SequenceEqual(serversSharedSecret)) 91 | { 92 | throw new Exception("The server generated shared secret did not match the locally generated shared secret"); 93 | } 94 | // create an encryption key based on the shared secret 95 | using var encKey = await BrowserCrypto.GenerateAESGCMKey(sharedSecret); 96 | // encrypt test message to send to the server to verify it can decrypt it. it will reply with an encrypted message we must decode. 97 | var testMsgBytes = Encoding.UTF8.GetBytes(TestString); 98 | var encMsg = await BrowserCrypto.Encrypt(encKey, testMsgBytes); 99 | // send to the server 100 | var serversEncryptedResponse = await Post("EncryptionTest", new EncryptionTestArgs { EncryptedMessage = encMsg, SenderECDHPublicKeyB64 = ECDHKeyB64 }); 101 | // decrypt the server's response 102 | var serversDecryptedResponse = await BrowserCrypto.Decrypt(encKey, serversEncryptedResponse); 103 | // convert bytes to text 104 | var response = Encoding.UTF8.GetString(serversDecryptedResponse); 105 | if ("Response message!" != response) 106 | { 107 | throw new Exception("Failed to decrypt the message"); 108 | } 109 | } 110 | finally 111 | { 112 | BusyCount--; 113 | } 114 | StateHasChanged(); 115 | } 116 | private async Task IncrementCount2() 117 | { 118 | BusyCount++; 119 | StateHasChanged(); 120 | try 121 | { 122 | // create local ECDSA key for signing 123 | ECDSAKey = await BrowserWASMCrypto.GenerateECDSAKey(); 124 | // create local ECDH for shared secret creation 125 | ECDHKey = await BrowserWASMCrypto.GenerateECDHKey(); 126 | ECDSAKeyB64 = Convert.ToBase64String(await BrowserWASMCrypto.ExportPublicKeySpki(ECDSAKey!)); 127 | ECDHKeyB64 = Convert.ToBase64String(await BrowserWASMCrypto.ExportPublicKeySpki(ECDHKey!)); 128 | ServerECDSAKey = await BrowserWASMCrypto.ImportECDSAKey(await Get("ecdsa")); 129 | ServerECDHKey = await BrowserWASMCrypto.ImportECDHKey(await Get("ecdh")); 130 | ServerECDSAKeyB64 = Convert.ToBase64String(await BrowserWASMCrypto.ExportPublicKeySpki(ServerECDSAKey!)); 131 | ServerECDHKeyB64 = Convert.ToBase64String(await BrowserWASMCrypto.ExportPublicKeySpki(ServerECDHKey!)); 132 | // derive a shared secret using the other party's (server's) public ECDH key and this party's (browser's) private ECDH key 133 | // the shared secret should be identical on both sides 134 | var sharedSecret = await BrowserWASMCrypto.DeriveBits(ECDHKey, ServerECDHKey); 135 | // get the shared secret from the server to verify it matches (you would never do this in production) 136 | var serversSharedSecret = await Post("GetSharedSecret", new GetSharedSecretArgs { SenderECDHPublicKeyB64 = ECDHKeyB64 }); 137 | if (serversSharedSecret == null || !sharedSecret.SequenceEqual(serversSharedSecret)) 138 | { 139 | throw new Exception("The server generated shared secret did not match the locally generated shared secret"); 140 | } 141 | // create an encryption key based on the shared secret 142 | using var encKey = await BrowserWASMCrypto.GenerateAESGCMKey(sharedSecret); 143 | // encrypt test message to send to the server to verify it can decrypt it. it will reply with an encrypted message we must decode. 144 | var testMsgBytes = Encoding.UTF8.GetBytes(TestString); 145 | var encMsg = await BrowserWASMCrypto.Encrypt(encKey, testMsgBytes); 146 | // send to the server 147 | var serversEncryptedResponse = await Post("EncryptionTest", new EncryptionTestArgs { EncryptedMessage = encMsg, SenderECDHPublicKeyB64 = ECDHKeyB64 }); 148 | // decrypt the server's response 149 | var serversDecryptedResponse = await BrowserWASMCrypto.Decrypt(encKey, serversEncryptedResponse); 150 | // convert bytes to text 151 | var response = Encoding.UTF8.GetString(serversDecryptedResponse); 152 | if ("Response message!" != response) 153 | { 154 | throw new Exception("Failed to decrypt the message"); 155 | } 156 | } 157 | finally 158 | { 159 | BusyCount--; 160 | } 161 | StateHasChanged(); 162 | } 163 | public class GetSharedSecretArgs 164 | { 165 | public string SenderECDHPublicKeyB64 { get; set; } 166 | } 167 | public class EncryptionTestArgs 168 | { 169 | public string SenderECDHPublicKeyB64 { get; set; } 170 | public byte[] EncryptedMessage { get; set; } 171 | } 172 | private async Task Post(string endpoint, object data) where T : class 173 | { 174 | try 175 | { 176 | var resp = await HttpClient.PostAsJsonAsync($"api/CryptographyTest/{endpoint}", data); 177 | if (resp.IsSuccessStatusCode) 178 | { 179 | return await resp.Content.ReadFromJsonAsync(); 180 | } 181 | } 182 | catch { } 183 | return default; 184 | } 185 | private async Task Get(string endpoint) where T : class 186 | { 187 | try 188 | { 189 | return await HttpClient.GetFromJsonAsync($"api/CryptographyTest/{endpoint}"); 190 | } 191 | catch { } 192 | return default; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Browser/BrowserCryptoAESCBC.cs: -------------------------------------------------------------------------------- 1 | using SpawnDev.BlazorJS.Cryptography.Browser; 2 | using SpawnDev.BlazorJS.JSObjects; 3 | using SpawnDev.BlazorJS.RemoteJSRuntime.AsyncObjects; 4 | 5 | namespace SpawnDev.BlazorJS.Cryptography 6 | { 7 | public partial class BrowserCrypto 8 | { 9 | /// 10 | /// Generate an AES-CBC key 11 | /// 12 | public override async Task GenerateAESCBCKey(int keySize, bool extractable = true) 13 | { 14 | var key = await SubtleCrypto.GenerateKey(new AesKeyGenParams { Name = Algorithm.AESCBC, Length = keySize }, extractable, new string[] { "encrypt", "decrypt" }); 15 | return new BrowserAESCBCKey(key, keySize, extractable); 16 | } 17 | /// 18 | /// Encrypt data using an AES-CBC key 19 | /// 20 | public async Task Encrypt(PortableAESCBCKey key, Uint8ArrayAsync plainBytes, Uint8ArrayAsync iv, bool prependIV = false, AESCBCPadding padding = AESCBCPadding.PKCS7) 21 | { 22 | if (key is not BrowserAESCBCKey jsKey) throw new NotImplementedException(); 23 | Uint8ArrayAsync encryptedUint8Array; 24 | if (padding == AESCBCPadding.None) 25 | { 26 | var dataLength = await plainBytes.Get_Length(); 27 | if (dataLength % AES_CBC_BLOCK_SIZE != 0) 28 | { 29 | throw new Exception($"{plainBytes} length must be a multiple of 16 when using no padding."); 30 | } 31 | await using var encryptedArrayBuffer = await SubtleCrypto.Encrypt(new AesCbcParamsAsync { Iv = iv }, jsKey!.Key, plainBytes); 32 | encryptedUint8Array = await Uint8ArrayAsync.New(JSA, encryptedArrayBuffer); 33 | var encryptedLength = await encryptedArrayBuffer.Get_ByteLength(); 34 | var tmp = await encryptedUint8Array.SubArray(0, encryptedLength - AES_CBC_BLOCK_SIZE); 35 | await encryptedUint8Array.DisposeAsync(); 36 | encryptedUint8Array = tmp; 37 | } 38 | else 39 | { 40 | await using var encryptedArrayBuffer = await SubtleCrypto.Encrypt(new AesCbcParamsAsync { Iv = iv }, jsKey!.Key, plainBytes); 41 | encryptedUint8Array = await Uint8ArrayAsync.New(JSA, encryptedArrayBuffer); 42 | } 43 | if (!prependIV) 44 | { 45 | return encryptedUint8Array; 46 | } 47 | var ivLength = await iv.Get_Length(); 48 | var encryptedDataWithIvLength = await encryptedUint8Array.Get_Length() + ivLength; 49 | var result = await Uint8ArrayAsync.New(JSA, encryptedDataWithIvLength); 50 | // + iv 51 | await result.Set(iv, 0); 52 | // + encrypted data 53 | await result.Set(encryptedUint8Array, ivLength); 54 | await encryptedUint8Array.DisposeAsync(); 55 | return result; 56 | } 57 | /// 58 | /// Encrypt data using an AES-CBC key 59 | /// 60 | public override async Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, byte[] iv, bool prependIV = false, AESCBCPadding padding = AESCBCPadding.PKCS7) 61 | { 62 | await using var ivUint8Array = await Uint8ArrayAsync.New(JSA, iv); 63 | await using var dataUint8Array = await Uint8ArrayAsync.New(JSA, plainBytes); 64 | await using var decrypted = await Encrypt(key, dataUint8Array, ivUint8Array, prependIV, padding); 65 | return await decrypted.ReadBytes(); 66 | } 67 | /// 68 | /// Encrypt data using an AES-CBC key 69 | /// 70 | public override Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, bool prependIV = true, AESCBCPadding padding = AESCBCPadding.PKCS7) 71 | { 72 | if (key is not BrowserAESCBCKey jsKey) throw new NotImplementedException(); 73 | var iv = RandomBytes(16); 74 | return Encrypt(key, plainBytes, iv, prependIV, padding); 75 | } 76 | /// 77 | /// Encrypt data using an AES-CBC key 78 | /// 79 | public async Task Encrypt(PortableAESCBCKey key, Uint8ArrayAsync plainBytes, bool prependIV = true, AESCBCPadding padding = AESCBCPadding.PKCS7) 80 | { 81 | if (key is not BrowserAESCBCKey jsKey) throw new NotImplementedException(); 82 | var iv = RandomBytes(16); 83 | await using var ivUint8Array = await Uint8ArrayAsync.New(JSA, iv); 84 | return await Encrypt(key, plainBytes, ivUint8Array, prependIV, padding); 85 | } 86 | /// 87 | /// Decrypt data using an AES-CBC key
88 | /// This method expects the IV to be supplied separately from the encrypted data 89 | ///
90 | public async Task Decrypt(PortableAESCBCKey key, Uint8ArrayAsync encryptedData, Uint8ArrayAsync iv, AESCBCPadding padding = AESCBCPadding.PKCS7) 91 | { 92 | if (key is not BrowserAESCBCKey jsKey) throw new NotImplementedException(); 93 | if (padding == AESCBCPadding.None) 94 | { 95 | var dataLength = await encryptedData.Get_Length(); 96 | // PKS7 padding must be added because SubtleCrypto's implementation of AES-CBC requires it 97 | if (dataLength % AES_CBC_BLOCK_SIZE != 0) 98 | { 99 | throw new Exception($"{encryptedData} length must be a multiple of 16 when using no padding."); 100 | } 101 | // create a Uint8Array to hold the padded data 102 | await using var paddedData = await Uint8ArrayAsync.New(JSA, dataLength + AES_CBC_BLOCK_SIZE); 103 | await paddedData.Set(encryptedData, 0); 104 | // padding starts as a byte array of size paddingSize where each byte is the paddingSize 105 | await using var paddingData = await Uint8ArrayAsync.New(JSA, AES_CBC_BLOCK_SIZE); 106 | await paddingData.FillVoid((byte)AES_CBC_BLOCK_SIZE); 107 | // use the last paddingSize bytes of data is the iv 108 | await using var padBlockIv = await encryptedData.Slice(-AES_CBC_BLOCK_SIZE); 109 | // encrypt the padding data 110 | await using var padBlock = await Encrypt(jsKey, paddingData, padBlockIv, false, AESCBCPadding.None); 111 | await paddedData.Set(padBlock, dataLength); 112 | // decrypt 113 | var decryptedArrayBuffer = await SubtleCrypto.Decrypt(new AesCbcParamsAsync { Iv = iv }, jsKey.Key, paddedData); 114 | // return the decrypted data as a Uint8Array 115 | return await Uint8ArrayAsync.New(JSA, decryptedArrayBuffer); 116 | } 117 | else 118 | { 119 | await using var arrayBuffer = await SubtleCrypto.Decrypt(new AesCbcParamsAsync { Iv = iv }, jsKey!.Key, encryptedData); 120 | return await Uint8ArrayAsync.New(JSA, arrayBuffer); 121 | } 122 | } 123 | /// 124 | /// Decrypt data using an AES-CBC key
125 | /// This method expects the IV to be supplied separately from the encrypted data 126 | ///
127 | public override async Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, byte[] iv, AESCBCPadding padding = AESCBCPadding.PKCS7) 128 | { 129 | await using var ivUint8Array = await Uint8ArrayAsync.New(JSA, iv); 130 | await using var encryptedDataUint8Array = await Uint8ArrayAsync.New(JSA, encryptedData); 131 | await using var decrypted = await Decrypt(key, encryptedDataUint8Array, ivUint8Array, padding); 132 | return await decrypted.ReadBytes(); 133 | } 134 | /// 135 | /// Decrypt data using an AES-CBC key
136 | /// This method expects the IV to be prepended to the encrypted data 137 | ///
138 | public override async Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, AESCBCPadding padding = AESCBCPadding.PKCS7) 139 | { 140 | await using var encryptedDataUint8Array = await Uint8ArrayAsync.New(JSA, encryptedData); 141 | await using var decrypted = await Decrypt(key, encryptedDataUint8Array, padding); 142 | return await decrypted.ReadBytes(); 143 | } 144 | /// 145 | /// Decrypt data using an AES-CBC key
146 | /// This method expects the IV to be prepended to the encrypted data 147 | ///
148 | public async Task Decrypt(PortableAESCBCKey key, Uint8ArrayAsync encryptedData, AESCBCPadding padding = AESCBCPadding.PKCS7) 149 | { 150 | await using var iv = await encryptedData.Slice(0, 16); 151 | await using var encrypted = await encryptedData.Slice(16); 152 | return await Decrypt(key, encrypted, iv, padding); 153 | } 154 | /// 155 | /// Import an AES-CBC key 156 | /// 157 | /// 158 | /// 159 | /// 160 | public override async Task ImportAESCBCKey(byte[] rawKey, bool extractable = true) 161 | { 162 | var key = await SubtleCrypto.ImportKey("raw", rawKey, Algorithm.AESCBC, extractable, new string[] { "encrypt", "decrypt" }); 163 | return new BrowserAESCBCKey(key, rawKey.Length * 8, extractable); 164 | } 165 | /// 166 | /// Export an AES-CBC key 167 | /// 168 | public override async Task ExportAESCBCKey(PortableAESCBCKey key) 169 | { 170 | if (key is not BrowserAESCBCKey jsKey) throw new NotImplementedException(); 171 | await using var ret = await SubtleCrypto.ExportKeyRaw(jsKey.Key); 172 | return await ret.ReadBytes(); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/IPortableCrypto.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace SpawnDev.BlazorJS.Cryptography 3 | { 4 | /// 5 | /// PortableCrypto interface 6 | /// 7 | public interface IPortableCrypto 8 | { 9 | // Digest 10 | /// 11 | /// Hash data using the specified hash algorithm 12 | /// 13 | /// 14 | /// 15 | /// 16 | Task Digest(string hashName, byte[] data); 17 | // AES-GCM 18 | /// 19 | /// Decrypt data using the specified key 20 | /// 21 | /// 22 | /// 23 | /// 24 | Task Decrypt(PortableAESGCMKey key, byte[] encryptedData); 25 | /// 26 | /// Encrypt data using the specified key 27 | /// 28 | /// 29 | /// 30 | /// 31 | Task Encrypt(PortableAESGCMKey key, byte[] plainBytes); 32 | /// 33 | /// Generate an AES-GCM key 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// 43 | Task GenerateAESGCMKey(byte[] secret, int iterations = 25000, string hashName = "SHA-256", int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true); 44 | /// 45 | /// Generate an AES-GCM key 46 | /// 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// 56 | Task GenerateAESGCMKey(byte[] secret, byte[] salt, int iterations = 25000, string hashName = "SHA-256", int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true); 57 | // AES-CBC 58 | /// 59 | /// Decrypt data using an AES-CBC key 60 | /// 61 | /// 62 | /// 63 | /// 64 | /// 65 | Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, AESCBCPadding padding = AESCBCPadding.PKCS7); 66 | /// 67 | /// Decrypt data using an AES-CBC key 68 | /// 69 | /// 70 | /// 71 | /// 72 | /// 73 | /// 74 | Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, byte[] iv, AESCBCPadding padding = AESCBCPadding.PKCS7); 75 | /// 76 | /// Encrypt data using an AES-CBC key 77 | /// 78 | /// 79 | /// 80 | /// 81 | /// 82 | /// 83 | /// 84 | Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, byte[] iv, bool prependIV = false, AESCBCPadding padding = AESCBCPadding.PKCS7); 85 | /// 86 | /// Encrypt data using an AES-CBC key 87 | /// 88 | /// 89 | /// 90 | /// 91 | /// 92 | /// 93 | Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, bool prependIV = true, AESCBCPadding padding = AESCBCPadding.PKCS7); 94 | /// 95 | /// Generate an AES-CBC key 96 | /// 97 | /// 98 | /// 99 | /// 100 | Task GenerateAESCBCKey(int length, bool extractable = true); 101 | /// 102 | /// Import an AES-CBC key 103 | /// 104 | /// 105 | /// 106 | /// 107 | Task ImportAESCBCKey(byte[] rawKey, bool extractable = true); 108 | /// 109 | /// Export an AES-CBC key 110 | /// 111 | /// 112 | /// 113 | Task ExportAESCBCKey(PortableAESCBCKey key); 114 | // ECDH 115 | /// 116 | /// Derive bits from an ECDH key 117 | /// 118 | /// 119 | /// 120 | /// 121 | Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey); 122 | /// 123 | /// Derive bits from an ECDH key 124 | /// 125 | /// 126 | /// 127 | /// 128 | /// 129 | Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey, int bitLength); 130 | /// 131 | /// Export an ECDH private key 132 | /// 133 | /// 134 | /// 135 | Task ExportPrivateKeyPkcs8(PortableECDHKey key); 136 | /// 137 | /// Export an ECDH public key 138 | /// 139 | /// 140 | /// 141 | Task ExportPublicKeySpki(PortableECDHKey key); 142 | /// 143 | /// Generate an ECDH key 144 | /// 145 | /// 146 | /// 147 | /// 148 | Task GenerateECDHKey(string namedCurve = "P-521", bool extractable = true); 149 | /// 150 | /// Import an ECDH key 151 | /// 152 | /// 153 | /// 154 | /// 155 | /// 156 | Task ImportECDHKey(byte[] publicKeySpki, string namedCurve = "P-521", bool extractable = true); 157 | /// 158 | /// Import an ECDH key 159 | /// 160 | /// 161 | /// 162 | /// 163 | /// 164 | /// 165 | Task ImportECDHKey(byte[] publicKeySpki, byte[] privateKeyPkcs8, string namedCurve = "P-521", bool extractable = true); 166 | // ECDSA 167 | /// 168 | /// Export an ECDSA private key 169 | /// 170 | /// 171 | /// 172 | Task ExportPrivateKeyPkcs8(PortableECDSAKey key); 173 | /// 174 | /// Export an ECDSA public key 175 | /// 176 | /// 177 | /// 178 | Task ExportPublicKeySpki(PortableECDSAKey key); 179 | /// 180 | /// Generate an ECDSA key 181 | /// 182 | /// 183 | /// 184 | /// 185 | Task GenerateECDSAKey(string namedCurve = "P-521", bool extractable = true); 186 | /// 187 | /// Import an ECDSA key 188 | /// 189 | /// 190 | /// 191 | /// 192 | /// 193 | Task ImportECDSAKey(byte[] publicKeySpkiData, string namedCurve = "P-521", bool extractable = true); 194 | /// 195 | /// Import an ECDSA key 196 | /// 197 | /// 198 | /// 199 | /// 200 | /// 201 | /// 202 | Task ImportECDSAKey(byte[] publicKeySpkiData, byte[] privateKeyPkcs8Data, string namedCurve = "P-521", bool extractable = true); 203 | /// 204 | /// Sign data 205 | /// 206 | /// 207 | /// 208 | /// 209 | /// 210 | Task Sign(PortableECDSAKey key, byte[] data, string hashName = "SHA-512"); 211 | /// 212 | /// Verify a signature 213 | /// 214 | /// 215 | /// 216 | /// 217 | /// 218 | /// 219 | Task Verify(PortableECDSAKey key, byte[] data, byte[] signature, string hashName = "SHA-512"); 220 | // Random 221 | /// 222 | /// Generate random bytes 223 | /// 224 | /// 225 | /// 226 | byte[] RandomBytes(int length); 227 | /// 228 | /// Fill a byte array with random bytes 229 | /// 230 | /// 231 | void RandomBytesFill(byte[] data); 232 | /// 233 | /// Fill a Span with random bytes 234 | /// 235 | /// 236 | void RandomBytesFill(Span data); 237 | } 238 | } -------------------------------------------------------------------------------- /SpawnDev.BlazorJS.Cryptography/Base/PortableCrypto.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | 3 | namespace SpawnDev.BlazorJS.Cryptography 4 | { 5 | /// 6 | /// PortableCrypto base class shared by all implementations 7 | /// 8 | public abstract partial class PortableCrypto : IPortableCrypto 9 | { 10 | // Digest 11 | /// 12 | /// Hash data using the specified hash algorithm 13 | /// 14 | /// 15 | /// 16 | /// 17 | public abstract Task Digest(string hashName, byte[] data); 18 | // AES-GCM 19 | /// 20 | /// Decrypt data using an AES-GCM key 21 | /// 22 | /// 23 | /// 24 | /// 25 | public abstract Task Decrypt(PortableAESGCMKey key, byte[] encryptedData); 26 | /// 27 | /// Decrypt data using an AES-GCM key 28 | /// 29 | /// 30 | /// 31 | /// 32 | public abstract Task Encrypt(PortableAESGCMKey key, byte[] plainBytes); 33 | /// 34 | /// Generate an AES-GCM key 35 | /// 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// 44 | public abstract Task GenerateAESGCMKey(byte[] secret, int iterations = 25000, string hashName = "SHA-256", int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true); 45 | /// 46 | /// Generate an AES-GCM key 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | public abstract Task GenerateAESGCMKey(byte[] secret, byte[] salt, int iterations = 25000, string hashName = "SHA-256", int keySizeBytes = 32, int tagSizeBytes = 16, int nonceSizeBytes = 12, bool extractable = true); 58 | // AES-CBC 59 | /// 60 | /// Decrypt data using an AES-CBC key 61 | /// 62 | /// 63 | /// 64 | /// 65 | /// 66 | public abstract Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, AESCBCPadding padding = AESCBCPadding.PKCS7); 67 | /// 68 | /// Decrypt data using an AES-CBC key 69 | /// 70 | /// 71 | /// 72 | /// 73 | /// 74 | /// 75 | public abstract Task Decrypt(PortableAESCBCKey key, byte[] encryptedData, byte[] iv, AESCBCPadding padding = AESCBCPadding.PKCS7); 76 | /// 77 | /// Encrypt data using an AES-CBC key 78 | /// 79 | /// 80 | /// 81 | /// 82 | /// 83 | /// 84 | /// 85 | public abstract Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, byte[] iv, bool prependIV = false, AESCBCPadding padding = AESCBCPadding.PKCS7); 86 | /// 87 | /// Encrypt data using an AES-CBC key 88 | /// 89 | /// 90 | /// 91 | /// 92 | /// 93 | /// 94 | public abstract Task Encrypt(PortableAESCBCKey key, byte[] plainBytes, bool prependIV = true, AESCBCPadding padding = AESCBCPadding.PKCS7); 95 | /// 96 | /// Generate an AES-CBC key 97 | /// 98 | /// 99 | /// 100 | /// 101 | public abstract Task GenerateAESCBCKey(int keySize, bool extractable = true); 102 | /// 103 | /// Import an AES-CBC key 104 | /// 105 | /// 106 | /// 107 | /// 108 | public abstract Task ImportAESCBCKey(byte[] rawKey, bool extractable = true); 109 | /// 110 | /// Export an AES-CBC key 111 | /// 112 | /// 113 | /// 114 | public abstract Task ExportAESCBCKey(PortableAESCBCKey key); 115 | // ECDH 116 | /// 117 | /// Derive bits from an ECDH key 118 | /// 119 | /// 120 | /// 121 | /// 122 | public abstract Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey); 123 | /// 124 | /// Derive bits from an ECDH key 125 | /// 126 | /// 127 | /// 128 | /// 129 | /// 130 | public abstract Task DeriveBits(PortableECDHKey localPartyKey, PortableECDHKey otherPartyKey, int bitLength); 131 | /// 132 | /// Export an ECDH private key 133 | /// 134 | /// 135 | /// 136 | public abstract Task ExportPrivateKeyPkcs8(PortableECDHKey key); 137 | /// 138 | /// Export an ECDH public key 139 | /// 140 | /// 141 | /// 142 | public abstract Task ExportPublicKeySpki(PortableECDHKey key); 143 | /// 144 | /// Generate an ECDH key 145 | /// 146 | /// 147 | /// 148 | /// 149 | public abstract Task GenerateECDHKey(string namedCurve = "P-521", bool extractable = true); 150 | /// 151 | /// Import an ECDH key 152 | /// 153 | /// 154 | /// 155 | /// 156 | /// 157 | public abstract Task ImportECDHKey(byte[] publicKeySpki, string namedCurve = "P-521", bool extractable = true); 158 | /// 159 | /// Import an ECDH key 160 | /// 161 | /// 162 | /// 163 | /// 164 | /// 165 | /// 166 | public abstract Task ImportECDHKey(byte[] publicKeySpki, byte[] privateKeyPkcs8, string namedCurve = "P-521", bool extractable = true); 167 | // ECDSA 168 | /// 169 | /// Export an ECDSA private key 170 | /// 171 | /// 172 | /// 173 | public abstract Task ExportPrivateKeyPkcs8(PortableECDSAKey key); 174 | /// 175 | /// Export an ECDSA public key 176 | /// 177 | /// 178 | /// 179 | public abstract Task ExportPublicKeySpki(PortableECDSAKey key); 180 | /// 181 | /// Generate an ECDSA key 182 | /// 183 | /// 184 | /// 185 | /// 186 | public abstract Task GenerateECDSAKey(string namedCurve = "P-521", bool extractable = true); 187 | /// 188 | /// Import an ECDSA key 189 | /// 190 | /// 191 | /// 192 | /// 193 | /// 194 | public abstract Task ImportECDSAKey(byte[] publicKeySpkiData, string namedCurve = "P-521", bool extractable = true); 195 | /// 196 | /// Import an ECDSA key 197 | /// 198 | /// 199 | /// 200 | /// 201 | /// 202 | /// 203 | public abstract Task ImportECDSAKey(byte[] publicKeySpkiData, byte[] privateKeyPkcs8Data, string namedCurve = "P-521", bool extractable = true); 204 | /// 205 | /// Sign a message 206 | /// 207 | /// 208 | /// 209 | /// 210 | /// 211 | public abstract Task Sign(PortableECDSAKey key, byte[] data, string hashName = "SHA-512"); 212 | /// 213 | /// Verify a signature 214 | /// 215 | /// 216 | /// 217 | /// 218 | /// 219 | /// 220 | public abstract Task Verify(PortableECDSAKey key, byte[] data, byte[] signature, string hashName = "SHA-512"); 221 | // Random 222 | /// 223 | /// Generate a random byte array 224 | /// 225 | /// 226 | /// 227 | public abstract byte[] RandomBytes(int length); 228 | /// 229 | /// Fill a byte array with random bytes 230 | /// 231 | /// 232 | public abstract void RandomBytesFill(byte[] data); 233 | /// 234 | /// Fill a Span with random bytes 235 | /// 236 | /// 237 | public abstract void RandomBytesFill(Span data); 238 | 239 | /// 240 | /// AES-CBC block size 241 | /// 242 | public const int AES_CBC_BLOCK_SIZE = 16; 243 | 244 | /// 245 | /// EC named curves 246 | /// 247 | public static class Algorithm 248 | { 249 | /// 250 | /// ECDSA 251 | /// 252 | public const string ECDSA = "ECDSA"; 253 | /// 254 | /// ECDH 255 | /// 256 | public const string ECDH = "ECDH"; 257 | /// 258 | /// AES-GCM 259 | /// 260 | public const string AESGCM = "AES-GCM"; 261 | /// 262 | /// AES-CBC 263 | /// 264 | public const string AESCBC = "AES-CBC"; 265 | } 266 | /// 267 | /// EC named curves 268 | /// 269 | public static class NamedCurve 270 | { 271 | /// 272 | /// P-521 273 | /// 274 | public const string P521 = "P-521"; 275 | /// 276 | /// P-384 277 | /// 278 | public const string P384 = "P-384"; 279 | /// 280 | /// P-256 281 | /// 282 | public const string P256 = "P-256"; 283 | } 284 | /// 285 | /// Hash names 286 | /// 287 | public static class HashName 288 | { 289 | /// 290 | /// SHA-1 is deprecated. Do not use in cryptographic applications. 291 | /// 292 | public const string SHA1 = "SHA-1"; 293 | /// 294 | /// SHA-256 295 | /// 296 | public const string SHA256 = "SHA-256"; 297 | /// 298 | /// SHA-384 299 | /// 300 | public const string SHA384 = "SHA-384"; 301 | /// 302 | /// SHA-512 303 | /// 304 | public const string SHA512 = "SHA-512"; 305 | } 306 | /// 307 | /// Hash name to HashAlgorithmName 308 | /// 309 | /// 310 | /// 311 | /// 312 | protected static HashAlgorithmName HashNameToHashAlgorithmName(string hashName) 313 | { 314 | return hashName switch 315 | { 316 | HashName.SHA512 => HashAlgorithmName.SHA512, 317 | HashName.SHA384 => HashAlgorithmName.SHA384, 318 | HashName.SHA256 => HashAlgorithmName.SHA256, 319 | _ => throw new NotImplementedException($"HashName not implemented {hashName}") 320 | }; 321 | } 322 | /// 323 | /// Named curve to ECCurve 324 | /// 325 | /// 326 | /// 327 | /// 328 | protected static ECCurve NamedCurveToECCurve(string namedCurve) 329 | { 330 | return namedCurve switch 331 | { 332 | NamedCurve.P521 => ECCurve.NamedCurves.nistP521, 333 | NamedCurve.P384 => ECCurve.NamedCurves.nistP384, 334 | NamedCurve.P256 => ECCurve.NamedCurves.nistP256, 335 | _ => throw new NotImplementedException($"NamedCurve not implemented {namedCurve}") 336 | }; 337 | } 338 | /// 339 | /// Named curve bit length 340 | /// 341 | /// 342 | /// 343 | /// 344 | /// 345 | protected static int NamedCurveBitLength(string namedCurve, bool compatibilityMode = false) 346 | { 347 | return namedCurve switch 348 | { 349 | NamedCurve.P521 => compatibilityMode ? 512 : 521, 350 | NamedCurve.P384 => 384, 351 | NamedCurve.P256 => 256, 352 | _ => throw new NotImplementedException() 353 | }; 354 | } 355 | } 356 | } --------------------------------------------------------------------------------