├── 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 |
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 | Date
20 | Temp. (C)
21 | Temp. (F)
22 | Summary
23 |
24 |
25 |
26 | @foreach (var forecast in forecasts)
27 | {
28 |
29 | @forecast.Date.ToShortDateString()
30 | @forecast.TemperatureC
31 | @forecast.TemperatureF
32 | @forecast.Summary
33 |
34 | }
35 |
36 |
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 | [](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 | Click me
11 | Click me 2
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 | }
--------------------------------------------------------------------------------