Skip to content

Commit 865c729

Browse files
feat: Allow injecting private key decryptor for jwt (box/box-codegen#754) (#528)
1 parent 0b47597 commit 865c729

File tree

8 files changed

+137
-81
lines changed

8 files changed

+137
-81
lines changed

.codegen.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{ "engineHash": "fc1155c", "specHash": "8402463", "version": "1.10.0" }
1+
{ "engineHash": "b8e7dbb", "specHash": "8402463", "version": "1.10.0" }

Box.Sdk.Gen/Box/JwtAuth/BoxJwtAuth.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public async System.Threading.Tasks.Task<AccessToken> RefreshTokenAsync(NetworkS
4747
}
4848
JwtAlgorithm alg = this.Config.Algorithm != null ? NullableUtils.Unwrap(this.Config.Algorithm) : JwtAlgorithm.Rs256;
4949
Dictionary<string, object> claims = new Dictionary<string, object>() { { "exp", Utils.GetEpochTimeInSeconds() + 30 }, { "box_sub_type", this.SubjectType } };
50-
JwtSignOptions jwtOptions = new JwtSignOptions(algorithm: alg, audience: "https://api.box.com/oauth2/token", subject: this.SubjectId, issuer: this.Config.ClientId, jwtid: Utils.GetUUID(), keyid: this.Config.JwtKeyId);
50+
JwtSignOptions jwtOptions = new JwtSignOptions(algorithm: alg, audience: "https://api.box.com/oauth2/token", subject: this.SubjectId, issuer: this.Config.ClientId, jwtid: Utils.GetUUID(), keyid: this.Config.JwtKeyId, privateKeyDecryptor: this.Config.PrivateKeyDecryptor);
5151
JwtKey jwtKey = new JwtKey(key: this.Config.PrivateKey, passphrase: this.Config.PrivateKeyPassphrase);
5252
string assertion = JwtUtils.CreateJwtAssertion(claims: claims, key: jwtKey, options: jwtOptions);
5353
AuthorizationManager authManager = new AuthorizationManager(networkSession: networkSession != null ? NullableUtils.Unwrap(networkSession) : new NetworkSession());

Box.Sdk.Gen/Box/JwtAuth/JwtConfig.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,16 @@ public class JwtConfig {
4646

4747
public ITokenStorage TokenStorage { get; }
4848

49-
public JwtConfig(string clientId, string clientSecret, string jwtKeyId, string privateKey, string privateKeyPassphrase, ITokenStorage? tokenStorage = default) {
49+
public IPrivateKeyDecryptor PrivateKeyDecryptor { get; }
50+
51+
public JwtConfig(string clientId, string clientSecret, string jwtKeyId, string privateKey, string privateKeyPassphrase, ITokenStorage? tokenStorage = default, IPrivateKeyDecryptor? privateKeyDecryptor = default) {
5052
ClientId = clientId;
5153
ClientSecret = clientSecret;
5254
JwtKeyId = jwtKeyId;
5355
PrivateKey = privateKey;
5456
PrivateKeyPassphrase = privateKeyPassphrase;
5557
TokenStorage = tokenStorage ?? new InMemoryTokenStorage();
58+
PrivateKeyDecryptor = privateKeyDecryptor ?? new DefaultPrivateKeyDecryptor();
5659
}
5760
/// <summary>
5861
/// Create an auth instance as defined by a string content of JSON file downloaded from the Box Developer Console.
@@ -62,11 +65,16 @@ public JwtConfig(string clientId, string clientSecret, string jwtKeyId, string p
6265
/// String content of JSON file containing the configuration.
6366
/// </param>
6467
/// <param name="tokenStorage">
65-
/// Object responsible for storing token. If no custom implementation provided, the token will be stored in memory.g
68+
/// Object responsible for storing token. If no custom implementation provided, the token will be stored in memory
69+
/// </param>
70+
/// <param name="privateKeyDecryptor">
71+
/// Object responsible for decrypting private key for jwt auth. If no custom implementation provided, the DefaultPrivateKeyDecryptor will be used.
6672
/// </param>
67-
public static JwtConfig FromConfigJsonString(string configJsonString, ITokenStorage? tokenStorage = null) {
73+
public static JwtConfig FromConfigJsonString(string configJsonString, ITokenStorage? tokenStorage = null, IPrivateKeyDecryptor? privateKeyDecryptor = null) {
6874
JwtConfigFile configJson = SimpleJsonSerializer.Deserialize<JwtConfigFile>(JsonUtils.JsonToSerializedData(text: configJsonString));
69-
JwtConfig newConfig = tokenStorage != null ? new JwtConfig(clientId: configJson.BoxAppSettings.ClientId, clientSecret: configJson.BoxAppSettings.ClientSecret, jwtKeyId: configJson.BoxAppSettings.AppAuth.PublicKeyId, privateKey: configJson.BoxAppSettings.AppAuth.PrivateKey, privateKeyPassphrase: configJson.BoxAppSettings.AppAuth.Passphrase, tokenStorage: tokenStorage) { EnterpriseId = configJson.EnterpriseId, UserId = configJson.UserId } : new JwtConfig(clientId: configJson.BoxAppSettings.ClientId, clientSecret: configJson.BoxAppSettings.ClientSecret, jwtKeyId: configJson.BoxAppSettings.AppAuth.PublicKeyId, privateKey: configJson.BoxAppSettings.AppAuth.PrivateKey, privateKeyPassphrase: configJson.BoxAppSettings.AppAuth.Passphrase) { EnterpriseId = configJson.EnterpriseId, UserId = configJson.UserId };
75+
ITokenStorage? tokenStorageToUse = tokenStorage == null ? new InMemoryTokenStorage() : tokenStorage;
76+
IPrivateKeyDecryptor? privateKeyDecryptorToUse = privateKeyDecryptor == null ? new DefaultPrivateKeyDecryptor() : privateKeyDecryptor;
77+
JwtConfig newConfig = new JwtConfig(clientId: configJson.BoxAppSettings.ClientId, clientSecret: configJson.BoxAppSettings.ClientSecret, jwtKeyId: configJson.BoxAppSettings.AppAuth.PublicKeyId, privateKey: configJson.BoxAppSettings.AppAuth.PrivateKey, privateKeyPassphrase: configJson.BoxAppSettings.AppAuth.Passphrase, tokenStorage: tokenStorageToUse, privateKeyDecryptor: privateKeyDecryptorToUse) { EnterpriseId = configJson.EnterpriseId, UserId = configJson.UserId };
7078
return newConfig;
7179
}
7280

@@ -80,9 +88,12 @@ public static JwtConfig FromConfigJsonString(string configJsonString, ITokenStor
8088
/// <param name="tokenStorage">
8189
/// Object responsible for storing token. If no custom implementation provided, the token will be stored in memory.
8290
/// </param>
83-
public static JwtConfig FromConfigFile(string configFilePath, ITokenStorage? tokenStorage = null) {
91+
/// <param name="privateKeyDecryptor">
92+
/// Object responsible for decrypting private key for jwt auth. If no custom implementation provided, the DefaultPrivateKeyDecryptor will be used.
93+
/// </param>
94+
public static JwtConfig FromConfigFile(string configFilePath, ITokenStorage? tokenStorage = null, IPrivateKeyDecryptor? privateKeyDecryptor = null) {
8495
string configJsonString = Utils.ReadTextFromFile(filepath: configFilePath);
85-
return JwtConfig.FromConfigJsonString(configJsonString: configJsonString, tokenStorage: tokenStorage);
96+
return JwtConfig.FromConfigJsonString(configJsonString: configJsonString, tokenStorage: tokenStorage, privateKeyDecryptor: privateKeyDecryptor);
8697
}
8798

8899
}

Box.Sdk.Gen/Box/JwtUtils.cs

Lines changed: 9 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
using Microsoft.IdentityModel.Tokens;
2-
using Org.BouncyCastle.Asn1.Pkcs;
3-
using Org.BouncyCastle.Crypto.Asymmetric;
4-
using Org.BouncyCastle.OpenSsl;
5-
using Org.BouncyCastle.Operators;
6-
using Org.BouncyCastle.Pkcs;
7-
using Org.BouncyCastle.Security;
82
using System;
93
using System.Collections.Generic;
104
using System.IdentityModel.Tokens.Jwt;
11-
using System.IO;
125
using System.Linq;
136
using System.Security.Claims;
147
using System.Security.Cryptography;
@@ -51,7 +44,7 @@ internal static string CreateJwtAssertion(Dictionary<string, object> claims, Jwt
5144
var jwtPayload = new JwtPayload(options.Issuer, options.Audience,
5245
jwtClaims, null, DateTimeOffset.FromUnixTimeSeconds(expTime).LocalDateTime);
5346

54-
var signingCredentials = GetSigningCredentials(key);
47+
var signingCredentials = GetSigningCredentials(key, options);
5548

5649
var header = new JwtHeader(signingCredentials);
5750

@@ -61,70 +54,15 @@ internal static string CreateJwtAssertion(Dictionary<string, object> claims, Jwt
6154
return assertion;
6255
}
6356

64-
private static SigningCredentials GetSigningCredentials(JwtKey key)
57+
private static SigningCredentials GetSigningCredentials(JwtKey key, JwtSignOptions options)
6558
{
66-
using (var keyReader = new StringReader(key.Key))
67-
{
68-
var reader = new OpenSslPemReader(keyReader);
69-
var privateCrtKeyParams = reader.ReadObject();
70-
71-
if (privateCrtKeyParams == null)
72-
{
73-
throw new ArgumentException("Invalid private JWT key!");
74-
}
75-
76-
if (privateCrtKeyParams is Pkcs8EncryptedPrivateKeyInfo)
77-
{
78-
var pkcs8 = (Pkcs8EncryptedPrivateKeyInfo)privateCrtKeyParams;
79-
PrivateKeyInfo privateKeyInfo = pkcs8.DecryptPrivateKeyInfo(
80-
new PkixPbeDecryptorProviderBuilder().Build(key.Passphrase.ToCharArray()));
81-
var bcKey = AsymmetricKeyFactory.CreatePrivateKey(privateKeyInfo.GetEncoded());
82-
83-
if (bcKey is AsymmetricRsaPrivateKey)
84-
{
85-
var bcRsaKey = (AsymmetricRsaPrivateKey)bcKey;
86-
var rsaParams = ToRSAParameters(bcRsaKey);
87-
var rsaKey = new RsaSecurityKey(rsaParams);
88-
89-
return new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256);
90-
}
91-
}
92-
93-
throw new ArgumentException("Provided JWT Key format is not supported");
94-
}
95-
}
59+
var rsa = options.PrivateKeyDecryptor.DecryptPrivateKey(key.Key, key.Passphrase);
9660

97-
private static RSAParameters ToRSAParameters(AsymmetricRsaPrivateKey privateKey)
98-
{
99-
RSAParameters rp = new RSAParameters();
100-
rp.Modulus = privateKey.Modulus.ToByteArrayUnsigned();
101-
rp.Exponent = privateKey.PublicExponent.ToByteArrayUnsigned();
102-
rp.P = privateKey.P.ToByteArrayUnsigned();
103-
rp.Q = privateKey.Q.ToByteArrayUnsigned();
104-
rp.D = ConvertRSAParametersField(privateKey.PrivateExponent, rp.Modulus.Length);
105-
rp.DP = ConvertRSAParametersField(privateKey.DP, rp.P.Length);
106-
rp.DQ = ConvertRSAParametersField(privateKey.DQ, rp.Q.Length);
107-
rp.InverseQ = ConvertRSAParametersField(privateKey.QInv, rp.Q.Length);
108-
return rp;
109-
}
61+
var rsaKey = new RsaSecurityKey(rsa);
11062

111-
private static byte[] ConvertRSAParametersField(Org.BouncyCastle.Math.BigInteger n, int size)
112-
{
113-
byte[] bs = n.ToByteArrayUnsigned();
114-
if (bs.Length == size)
115-
{
116-
return bs;
117-
}
118-
119-
if (bs.Length > size)
120-
{
121-
throw new ArgumentException("Specified size too small", "size");
122-
}
123-
124-
byte[] padded = new byte[size];
125-
Array.Copy(bs, 0, padded, size - bs.Length, bs.Length);
126-
return padded;
63+
return new SigningCredentials(rsaKey, SecurityAlgorithms.RsaSha256);
12764
}
65+
12866
}
12967

13068
enum JwtAlgorithm
@@ -152,15 +90,17 @@ class JwtSignOptions
15290
internal string Issuer { get; }
15391
internal string Jwtid { get; }
15492
internal string Keyid { get; }
93+
internal IPrivateKeyDecryptor PrivateKeyDecryptor { get; }
15594

156-
public JwtSignOptions(JwtAlgorithm algorithm, string audience, string subject, string issuer, string jwtid, string keyid)
95+
public JwtSignOptions(JwtAlgorithm algorithm, string audience, string subject, string issuer, string jwtid, string keyid, IPrivateKeyDecryptor privateKeyDecryptor)
15796
{
15897
Algorithm = algorithm;
15998
Audience = audience;
16099
Subject = subject;
161100
Issuer = issuer;
162101
Jwtid = jwtid;
163102
Keyid = keyid;
103+
PrivateKeyDecryptor = privateKeyDecryptor;
164104
}
165105
}
166106
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using Org.BouncyCastle.Asn1.Pkcs;
2+
using Org.BouncyCastle.Crypto.Asymmetric;
3+
using Org.BouncyCastle.OpenSsl;
4+
using Org.BouncyCastle.Operators;
5+
using Org.BouncyCastle.Pkcs;
6+
using Org.BouncyCastle.Security;
7+
using System;
8+
using System.IO;
9+
using System.Security.Cryptography;
10+
11+
namespace Box.Sdk.Gen
12+
{
13+
internal class DefaultPrivateKeyDecryptor : IPrivateKeyDecryptor
14+
{
15+
16+
internal DefaultPrivateKeyDecryptor()
17+
{
18+
}
19+
20+
/// <summary>
21+
/// Decrypts private key using a passphrase.
22+
/// </summary>
23+
public RSA DecryptPrivateKey(string encryptedPrivateKey, string passphrase)
24+
{
25+
using (var keyReader = new StringReader(encryptedPrivateKey))
26+
{
27+
var reader = new OpenSslPemReader(keyReader);
28+
var privateCrtKeyParams = reader.ReadObject();
29+
30+
if (privateCrtKeyParams == null)
31+
{
32+
throw new ArgumentException("Invalid private JWT key!");
33+
}
34+
35+
if (privateCrtKeyParams is Pkcs8EncryptedPrivateKeyInfo)
36+
{
37+
var pkcs8 = (Pkcs8EncryptedPrivateKeyInfo)privateCrtKeyParams;
38+
PrivateKeyInfo privateKeyInfo = pkcs8.DecryptPrivateKeyInfo(
39+
new PkixPbeDecryptorProviderBuilder().Build(passphrase.ToCharArray()));
40+
var bcKey = AsymmetricKeyFactory.CreatePrivateKey(privateKeyInfo.GetEncoded());
41+
42+
if (bcKey is AsymmetricRsaPrivateKey)
43+
{
44+
var bcRsaKey = (AsymmetricRsaPrivateKey)bcKey;
45+
var rsaParams = ToRSAParameters(bcRsaKey);
46+
47+
var rsa = RSA.Create();
48+
rsa.ImportParameters(rsaParams);
49+
50+
return rsa;
51+
}
52+
}
53+
54+
throw new ArgumentException("Provided JWT Key format is not supported");
55+
}
56+
}
57+
58+
private static RSAParameters ToRSAParameters(AsymmetricRsaPrivateKey privateKey)
59+
{
60+
RSAParameters rp = new RSAParameters();
61+
rp.Modulus = privateKey.Modulus.ToByteArrayUnsigned();
62+
rp.Exponent = privateKey.PublicExponent.ToByteArrayUnsigned();
63+
rp.P = privateKey.P.ToByteArrayUnsigned();
64+
rp.Q = privateKey.Q.ToByteArrayUnsigned();
65+
rp.D = ConvertRSAParametersField(privateKey.PrivateExponent, rp.Modulus.Length);
66+
rp.DP = ConvertRSAParametersField(privateKey.DP, rp.P.Length);
67+
rp.DQ = ConvertRSAParametersField(privateKey.DQ, rp.Q.Length);
68+
rp.InverseQ = ConvertRSAParametersField(privateKey.QInv, rp.Q.Length);
69+
return rp;
70+
}
71+
72+
private static byte[] ConvertRSAParametersField(Org.BouncyCastle.Math.BigInteger n, int size)
73+
{
74+
byte[] bs = n.ToByteArrayUnsigned();
75+
if (bs.Length == size)
76+
{
77+
return bs;
78+
}
79+
80+
if (bs.Length > size)
81+
{
82+
throw new ArgumentException("Specified size too small", "size");
83+
}
84+
85+
byte[] padded = new byte[size];
86+
Array.Copy(bs, 0, padded, size - bs.Length, bs.Length);
87+
return padded;
88+
}
89+
}
90+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Security.Cryptography;
2+
3+
namespace Box.Sdk.Gen
4+
{
5+
/// <summary>
6+
/// Interface used for private key decryption in JWT auth.
7+
/// </summary>
8+
public interface IPrivateKeyDecryptor
9+
{
10+
/// <summary>
11+
/// Decrypts private key using a passphrase.
12+
/// </summary>
13+
RSA DecryptPrivateKey(string encryptedPrivateKey, string passphrase);
14+
}
15+
}

Box.Sdk.Gen/Managers/Downloads/DownloadsManager.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ public async System.Threading.Tasks.Task<string> GetDownloadFileUrlAsync(string
4242
Dictionary<string, string> headersMap = Utils.PrepareParams(map: DictionaryUtils.MergeDictionaries(new Dictionary<string, string?>() { { "range", StringUtils.ToStringRepresentation(headers.Range) }, { "boxapi", StringUtils.ToStringRepresentation(headers.Boxapi) } }, headers.ExtraHeaders));
4343
FetchResponse response = await this.NetworkSession.NetworkClient.FetchAsync(options: new FetchOptions(url: string.Concat(this.NetworkSession.BaseUrls.BaseUrl, "/2.0/files/", StringUtils.ToStringRepresentation(fileId), "/content"), method: "GET", responseFormat: Box.Sdk.Gen.ResponseFormat.NoContent) { Parameters = queryParamsMap, Headers = headersMap, Auth = this.Auth, NetworkSession = this.NetworkSession, CancellationToken = cancellationToken, FollowRedirects = false }).ConfigureAwait(false);
4444
if (response.Headers.ContainsKey("location")) {
45-
return response.Headers["location"];
45+
return NullableUtils.Unwrap(response.Headers["location"]);
4646
}
4747
if (response.Headers.ContainsKey("Location")) {
48-
return response.Headers["Location"];
48+
return NullableUtils.Unwrap(response.Headers["Location"]);
4949
}
5050
throw new BoxSdkException(message: "No location header in response");
5151
}

Box.Sdk.Gen/Managers/Files/FilesManager.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,10 @@ public async System.Threading.Tasks.Task<string> GetFileThumbnailUrlAsync(string
187187
Dictionary<string, string> headersMap = Utils.PrepareParams(map: DictionaryUtils.MergeDictionaries(new Dictionary<string, string?>() { }, headers.ExtraHeaders));
188188
FetchResponse response = await this.NetworkSession.NetworkClient.FetchAsync(options: new FetchOptions(url: string.Concat(this.NetworkSession.BaseUrls.BaseUrl, "/2.0/files/", StringUtils.ToStringRepresentation(fileId), "/thumbnail.", StringUtils.ToStringRepresentation(extension)), method: "GET", responseFormat: Box.Sdk.Gen.ResponseFormat.NoContent) { Parameters = queryParamsMap, Headers = headersMap, Auth = this.Auth, NetworkSession = this.NetworkSession, CancellationToken = cancellationToken, FollowRedirects = false }).ConfigureAwait(false);
189189
if (response.Headers.ContainsKey("location")) {
190-
return response.Headers["location"];
190+
return NullableUtils.Unwrap(response.Headers["location"]);
191191
}
192192
if (response.Headers.ContainsKey("Location")) {
193-
return response.Headers["Location"];
193+
return NullableUtils.Unwrap(response.Headers["Location"]);
194194
}
195195
throw new BoxSdkException(message: "No location header in response");
196196
}

0 commit comments

Comments
 (0)