Skip to content

Commit 69ddc33

Browse files
authored
Merge pull request #36 from PandaTechAM/development
Jose
2 parents dd49f0e + dceb3a7 commit 69ddc33

File tree

7 files changed

+399
-13
lines changed

7 files changed

+399
-13
lines changed

Readme.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ scattered code to handle everyday cryptographic tasks. The library provides an *
1414
- Secure random generation,
1515
- Password validation/strength checks,
1616
- Masking of sensitive data.
17+
- JOSE (**JWE only**): simple sealed-envelope encryption (RSA-OAEP-256 + AES-256-GCM) with JWK + `kid` thumbprint.
1718

1819
Whether you need to **encrypt data**, **hash passwords**, or **generate secure random tokens**, PandaTech.Crypto
1920
provides lightweight abstractions over popular cryptographic solutions, ensuring simplicity and usability without
@@ -204,6 +205,35 @@ byte[] newCipher = AesMigration.MigrateFromOldNonHashed(oldCiphertext);
204205

205206
The library provides nullable-friendly variants too (`MigrateFromOldHashedNullable`, etc.).
206207

208+
### JoseJwe — Simple JWE (RSA-OAEP-256 + A256GCM)
209+
210+
Confidentiality-only envelopes using JOSE **JWE** (no signatures). Keys are **RSA JWKs**, and `kid` is the **RFC 7638
211+
thumbprint** of the public JWK.
212+
213+
```csharp
214+
using Pandatech.Crypto.Helpers;
215+
using System.Text;
216+
217+
// 1) Issue RSA keys (2048+)
218+
// returns public/private JWKs and kid (thumbprint of public JWK)
219+
var (publicJwk, privateJwk, kid) = JoseJwe.IssueKeys();
220+
221+
// 2) Encrypt to recipient’s public key (header includes kid)
222+
var plaintext = Encoding.UTF8.GetBytes("hello");
223+
var jwe = JoseJwe.Encrypt(publicJwk, plaintext, kid);
224+
225+
// 3) Decrypt with recipient’s private key
226+
if (JoseJwe.TryDecrypt(privateJwk, jwe, out var bytes))
227+
{
228+
var text = Encoding.UTF8.GetString(bytes);
229+
}
230+
```
231+
232+
#### Security notes
233+
- RSA key **size ≥ 2048** bits (enforced).
234+
- `kid` must match the supplied `publicJwk` (enforced).
235+
- This is **encryption only** (no authenticity). If you need signing later, add JWS separately.
236+
207237
### Argon2id Class
208238

209239
**Default Configurations**

src/Pandatech.Crypto/Helpers/Aes256Gcm.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ public static void Encrypt(Stream input, Stream output, string? key)
4747

4848
using var aes = new AesGcm(k, TagSize);
4949

50-
var plain = new byte[DefaultChunkSize];
51-
var cipher = new byte[DefaultChunkSize]; // <— you removed this; we need it
52-
var tagBuf = new byte[TagSize];
53-
var nonceBuf = new byte[NonceSize];
50+
var plain = new byte[DefaultChunkSize];
51+
var cipher = new byte[DefaultChunkSize]; // <— you removed this; we need it
52+
var tagBuf = new byte[TagSize];
53+
var nonceBuf = new byte[NonceSize];
5454
var frameLen4 = new byte[4];
55-
var aadLen = 5 + NonceSize + 4;
55+
var aadLen = 5 + NonceSize + 4;
5656

5757
ulong counter = 0;
5858
int read;
@@ -62,16 +62,16 @@ public static void Encrypt(Stream input, Stream output, string? key)
6262
{
6363
DeriveNonce(baseNonce, counter, nonceBuf);
6464

65-
var p = plain.AsSpan(0, read);
66-
var c = cipher.AsSpan(0, read);
65+
var p = plain.AsSpan(0, read);
66+
var c = cipher.AsSpan(0, read);
6767
var tag = tagBuf.AsSpan();
6868

6969
aes.Encrypt(nonceBuf, p, c, tag, header[..aadLen]);
7070

7171
BinaryPrimitives.WriteUInt32LittleEndian(frameLen4, (uint)read);
72-
output.Write(frameLen4); // len
73-
output.Write(tagBuf); // tag
74-
output.Write(c); // ciphertext
72+
output.Write(frameLen4); // len
73+
output.Write(tagBuf); // tag
74+
output.Write(c); // ciphertext
7575

7676
counter++;
7777
}

src/Pandatech.Crypto/Helpers/Aes256SivLegacy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace Pandatech.Crypto.Helpers;
1212

13+
[Obsolete("This is a legacy implementation of AES256-SIV. Use Aes256Siv instead.", false)]
1314
public static class Aes256SivLegacy
1415
{
1516
private static string? GlobalKey { get; set; }
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
using System.Security.Cryptography;
2+
using System.Text;
3+
using System.Text.Json;
4+
using Jose;
5+
6+
namespace Pandatech.Crypto.Helpers;
7+
8+
public static class JoseJwe
9+
{
10+
public static (string PublicJwk, string PrivateJwk, string Kid) IssueKeys(int bits = 2048)
11+
{
12+
if (bits < 2048)
13+
{
14+
throw new ArgumentOutOfRangeException(nameof(bits), "RSA key must be >= 2048 bits.");
15+
}
16+
17+
using var rsa = RSA.Create(bits);
18+
var pubJwk = ExportPublicJwk(rsa);
19+
var prvJwk = ExportPrivateJwk(rsa);
20+
var kid = Thumbprint(pubJwk);
21+
return (pubJwk, prvJwk, kid);
22+
}
23+
24+
public static string Encrypt(string publicJwk, byte[] payload, string kid)
25+
{
26+
// Validate kid matches public key
27+
var computed = Thumbprint(publicJwk);
28+
if (!string.Equals(computed, kid, StringComparison.Ordinal))
29+
{
30+
throw new ArgumentException("kid does not match publicJwk (RFC7638).", nameof(kid));
31+
}
32+
33+
using var rsa = ImportPublic(publicJwk);
34+
35+
// JWE: RSA-OAEP-256 + A256GCM; compact serialization; header includes kid
36+
return JWT.EncodeBytes(
37+
payload,
38+
rsa,
39+
JweAlgorithm.RSA_OAEP_256,
40+
JweEncryption.A256GCM,
41+
extraHeaders: new Dictionary<string, object>
42+
{
43+
["kid"] = kid
44+
}
45+
);
46+
}
47+
48+
public static bool TryDecrypt(string privateJwk, string jwe, out byte[] payload)
49+
{
50+
try
51+
{
52+
using var rsa = ImportPrivate(privateJwk);
53+
payload = JWT.DecodeBytes(jwe, rsa, JweAlgorithm.RSA_OAEP_256, JweEncryption.A256GCM);
54+
return true;
55+
}
56+
catch
57+
{
58+
payload = [];
59+
return false;
60+
}
61+
}
62+
63+
public static string ComputeKid(string publicJwk) => Thumbprint(publicJwk);
64+
65+
private static RSA ImportPublic(string jwkJson)
66+
{
67+
using var doc = JsonDocument.Parse(jwkJson);
68+
var r = doc.RootElement;
69+
if (r.GetProperty("kty")
70+
.GetString() != "RSA") throw new ArgumentException("kty must be RSA.");
71+
var n = Base64Url.Decode(r.GetProperty("n")
72+
.GetString()!);
73+
74+
if (n.Length * 8 < 2048)
75+
{
76+
throw new CryptographicException("RSA public key must be >= 2048 bits.");
77+
}
78+
79+
var e = Base64Url.Decode(r.GetProperty("e")
80+
.GetString()!);
81+
var p = new RSAParameters
82+
{
83+
Modulus = n,
84+
Exponent = e
85+
};
86+
var rsa = RSA.Create();
87+
rsa.ImportParameters(p);
88+
return rsa;
89+
}
90+
91+
private static RSA ImportPrivate(string jwkJson)
92+
{
93+
using var doc = JsonDocument.Parse(jwkJson);
94+
var r = doc.RootElement;
95+
if (r.GetProperty("kty")
96+
.GetString() != "RSA")
97+
{
98+
throw new ArgumentException("kty must be RSA.");
99+
}
100+
101+
var n = Base64Url.Decode(r.GetProperty("n")
102+
.GetString()!);
103+
if (n.Length * 8 < 2048)
104+
{
105+
throw new CryptographicException("RSA private key must be >= 2048 bits.");
106+
}
107+
108+
var pars = new RSAParameters
109+
{
110+
Modulus = Base64Url.Decode(r.GetProperty("n")
111+
.GetString()!),
112+
113+
Exponent = Base64Url.Decode(r.GetProperty("e")
114+
.GetString()!),
115+
D = Base64Url.Decode(r.GetProperty("d")
116+
.GetString()!)
117+
};
118+
119+
// optional CRT params if present
120+
Try(r, "p", out pars.P);
121+
Try(r, "q", out pars.Q);
122+
Try(r, "dp", out pars.DP);
123+
Try(r, "dq", out pars.DQ);
124+
Try(r, "qi", out pars.InverseQ);
125+
126+
var rsa = RSA.Create();
127+
rsa.ImportParameters(pars);
128+
return rsa;
129+
130+
static void Try(JsonElement root, string name, out byte[]? val)
131+
{
132+
val = root.TryGetProperty(name, out var v) ? Base64Url.Decode(v.GetString()!) : null;
133+
}
134+
}
135+
136+
private static string ExportPublicJwk(RSA rsa)
137+
{
138+
var p = rsa.ExportParameters(false);
139+
var o = new
140+
{
141+
kty = "RSA",
142+
n = Base64Url.Encode(p.Modulus!),
143+
e = Base64Url.Encode(p.Exponent!)
144+
};
145+
return JsonSerializer.Serialize(o);
146+
}
147+
148+
private static string ExportPrivateJwk(RSA rsa)
149+
{
150+
var p = rsa.ExportParameters(true);
151+
var o = new
152+
{
153+
kty = "RSA",
154+
n = Base64Url.Encode(p.Modulus!),
155+
e = Base64Url.Encode(p.Exponent!),
156+
d = Base64Url.Encode(p.D!),
157+
p = p.P is null ? null : Base64Url.Encode(p.P),
158+
q = p.Q is null ? null : Base64Url.Encode(p.Q),
159+
dp = p.DP is null ? null : Base64Url.Encode(p.DP),
160+
dq = p.DQ is null ? null : Base64Url.Encode(p.DQ),
161+
qi = p.InverseQ is null ? null : Base64Url.Encode(p.InverseQ)
162+
};
163+
var json = JsonSerializer.Serialize(o);
164+
// remove nulls (compact)
165+
using var doc = JsonDocument.Parse(json);
166+
using var ms = new MemoryStream();
167+
using var w = new Utf8JsonWriter(ms);
168+
w.WriteStartObject();
169+
foreach (var prop in doc.RootElement
170+
.EnumerateObject()
171+
.Where(prop => prop.Value.ValueKind != JsonValueKind.Null))
172+
{
173+
prop.WriteTo(w);
174+
}
175+
176+
w.WriteEndObject();
177+
w.Flush();
178+
return Encoding.UTF8.GetString(ms.ToArray());
179+
}
180+
181+
// RFC 7638 thumbprint over {"e","kty","n"} with lexicographic keys
182+
private static string Thumbprint(string publicRsaJwk)
183+
{
184+
using var doc = JsonDocument.Parse(publicRsaJwk);
185+
var r = doc.RootElement;
186+
var canonical =
187+
$$"""{"e":"{{r.GetProperty("e").GetString()}}" ,"kty":"RSA","n":"{{r.GetProperty("n").GetString()}}"}"""
188+
.Replace(" ", "");
189+
var hash = SHA256.HashData(Encoding.ASCII.GetBytes(canonical));
190+
return Base64Url.Encode(hash);
191+
}
192+
}

src/Pandatech.Crypto/Pandatech.Crypto.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
<Copyright>MIT</Copyright>
99
<PackageIcon>pandatech.png</PackageIcon>
1010
<PackageReadmeFile>Readme.md</PackageReadmeFile>
11-
<Version>6.0.0</Version>
11+
<Version>6.1.0</Version>
1212
<Title>Pandatech.Crypto</Title>
1313
<PackageTags>Pandatech, library, encryption, hash, algorythms, security</PackageTags>
1414
<Description>PandaTech.Crypto is a .NET library simplifying common cryptograhic functions.</Description>
1515
<RepositoryUrl>https://github.com/PandaTechAM/be-lib-pandatech-crypto</RepositoryUrl>
16-
<PackageReleaseNotes>Aes256Siv breaking changes introduced and AES256GCM. Refer to readme.md</PackageReleaseNotes>
16+
<PackageReleaseNotes>Easy Jose interface added. Refer to readme.md for changes</PackageReleaseNotes>
1717
</PropertyGroup>
1818

1919
<ItemGroup>
@@ -23,6 +23,7 @@
2323

2424
<ItemGroup>
2525
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1"/>
26+
<PackageReference Include="jose-jwt" Version="5.2.0" />
2627
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1"/>
2728
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
2829
</ItemGroup>

test/Pandatech.Crypto.Tests/Aes256SivTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void Deterministic_SamePlaintextSameCiphertext()
4242
public void EmptyInput_Produces16ByteV_AndDecryptsToEmpty()
4343
{
4444
var key = Key();
45-
var c = Aes256Siv.Encrypt(Array.Empty<byte>(), key);
45+
var c = Aes256Siv.Encrypt([], key);
4646
Assert.Equal(16, c.Length); // V only
4747
var p = Aes256Siv.DecryptToBytes(c, key);
4848
Assert.Empty(p);

0 commit comments

Comments
 (0)