-
Notifications
You must be signed in to change notification settings - Fork 5k
ML-KEM: ImportFrom{Encrypted}Pem #114155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ML-KEM: ImportFrom{Encrypted}Pem #114155
Changes from all commits
3cf7d37
abd7cdb
dcc21e4
535afd2
97f0961
031379c
a97245a
cc79a5b
7e67dab
b5a801a
d45b3cb
f7a9f18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Diagnostics; | ||
using System.Security.Cryptography; | ||
|
||
namespace System.Security.Cryptography | ||
{ | ||
internal static partial class PemKeyHelpers | ||
{ | ||
internal delegate TAlg ImportFactoryKeyAction<TAlg>(ReadOnlySpan<byte> source); | ||
internal delegate ImportFactoryKeyAction<TAlg>? FindImportFactoryActionFunc<TAlg>(ReadOnlySpan<char> label); | ||
internal delegate TAlg ImportFactoryEncryptedKeyAction<TAlg, TPass>(ReadOnlySpan<TPass> password, ReadOnlySpan<byte> source); | ||
|
||
internal static TAlg ImportFactoryPem<TAlg>(ReadOnlySpan<char> source, FindImportFactoryActionFunc<TAlg> callback) where TAlg : class | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is largely based on the already existing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried de-duping the logic between the two but it got real messy because the delegates are different types and we need type parameters in this one. |
||
{ | ||
ImportFactoryKeyAction<TAlg>? importAction = null; | ||
PemFields foundFields = default; | ||
ReadOnlySpan<char> foundSlice = default; | ||
bool containsEncryptedPem = false; | ||
|
||
ReadOnlySpan<char> pem = source; | ||
while (PemEncoding.TryFind(pem, out PemFields fields)) | ||
{ | ||
ReadOnlySpan<char> label = pem[fields.Label]; | ||
ImportFactoryKeyAction<TAlg>? action = callback(label); | ||
|
||
if (action is not null) | ||
{ | ||
if (importAction is not null || containsEncryptedPem) | ||
{ | ||
throw new ArgumentException(SR.Argument_PemImport_AmbiguousPem, nameof(source)); | ||
} | ||
|
||
importAction = action; | ||
foundFields = fields; | ||
foundSlice = pem; | ||
} | ||
else if (label.SequenceEqual(PemLabels.EncryptedPkcs8PrivateKey)) | ||
{ | ||
if (importAction != null || containsEncryptedPem) | ||
{ | ||
throw new ArgumentException(SR.Argument_PemImport_AmbiguousPem, nameof(source)); | ||
} | ||
|
||
containsEncryptedPem = true; | ||
} | ||
|
||
Index offset = fields.Location.End; | ||
pem = pem[offset..]; | ||
} | ||
|
||
// The only PEM found that could potentially be used is encrypted PKCS8, | ||
// but we won't try to import it with a null or blank password, so | ||
// throw. | ||
if (containsEncryptedPem) | ||
{ | ||
throw new ArgumentException(SR.Argument_PemImport_EncryptedPem, nameof(source)); | ||
} | ||
|
||
// We went through the PEM and found nothing that could be handled. | ||
if (importAction is null) | ||
{ | ||
throw new ArgumentException(SR.Argument_PemImport_NoPemFound, nameof(source)); | ||
} | ||
|
||
ReadOnlySpan<char> base64Contents = foundSlice[foundFields.Base64Data]; | ||
#if NET | ||
int base64size = foundFields.DecodedDataLength; | ||
byte[] decodeBuffer = CryptoPool.Rent(base64size); | ||
int bytesWritten = 0; | ||
|
||
try | ||
{ | ||
if (!Convert.TryFromBase64Chars(base64Contents, decodeBuffer, out bytesWritten)) | ||
{ | ||
// Couldn't decode base64. We shouldn't get here since the | ||
// contents are pre-validated. | ||
Debug.Fail("Base64 decoding failed on already validated contents."); | ||
throw new ArgumentException(); | ||
} | ||
|
||
Debug.Assert(bytesWritten == base64size); | ||
Span<byte> decodedBase64 = decodeBuffer.AsSpan(0, bytesWritten); | ||
|
||
return importAction(decodedBase64); | ||
} | ||
finally | ||
{ | ||
CryptoPool.Return(decodeBuffer, clearSize: bytesWritten); | ||
} | ||
#else | ||
return importAction(Convert.FromBase64String(base64Contents.ToString())); | ||
#endif | ||
} | ||
|
||
internal static TAlg ImportEncryptedFactoryPem<TAlg, TPass>( | ||
ReadOnlySpan<char> source, | ||
ReadOnlySpan<TPass> password, | ||
ImportFactoryEncryptedKeyAction<TAlg, TPass> importAction) where TAlg : class | ||
{ | ||
bool foundEncryptedPem = false; | ||
PemFields foundFields = default; | ||
ReadOnlySpan<char> foundSlice = default; | ||
|
||
ReadOnlySpan<char> pem = source; | ||
while (PemEncoding.TryFind(pem, out PemFields fields)) | ||
{ | ||
ReadOnlySpan<char> label = pem[fields.Label]; | ||
|
||
if (label.SequenceEqual(PemLabels.EncryptedPkcs8PrivateKey)) | ||
{ | ||
if (foundEncryptedPem) | ||
{ | ||
throw new ArgumentException(SR.Argument_PemImport_AmbiguousPem, nameof(source)); | ||
} | ||
|
||
foundEncryptedPem = true; | ||
foundFields = fields; | ||
foundSlice = pem; | ||
} | ||
|
||
Index offset = fields.Location.End; | ||
pem = pem[offset..]; | ||
} | ||
|
||
if (!foundEncryptedPem) | ||
{ | ||
throw new ArgumentException(SR.Argument_PemImport_NoPemFound, nameof(source)); | ||
} | ||
|
||
ReadOnlySpan<char> base64Contents = foundSlice[foundFields.Base64Data]; | ||
#if NET | ||
int base64size = foundFields.DecodedDataLength; | ||
byte[] decodeBuffer = CryptoPool.Rent(base64size); | ||
int bytesWritten = 0; | ||
|
||
try | ||
{ | ||
if (!Convert.TryFromBase64Chars(base64Contents, decodeBuffer, out bytesWritten)) | ||
{ | ||
// Couldn't decode base64. We shouldn't get here since the | ||
// contents are pre-validated. | ||
Debug.Fail("Base64 decoding failed on already validated contents."); | ||
throw new ArgumentException(); | ||
} | ||
|
||
Debug.Assert(bytesWritten == base64size); | ||
Span<byte> decodedBase64 = decodeBuffer.AsSpan(0, bytesWritten); | ||
|
||
return importAction(password, decodedBase64); | ||
} | ||
finally | ||
{ | ||
CryptoPool.Return(decodeBuffer, clearSize: bytesWritten); | ||
} | ||
#else | ||
return importAction(password, Convert.FromBase64String(base64Contents.ToString())); | ||
#endif | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was not in the initial API design, and we don't have it on other types like
RSA
. However, I think it is useful for .NET Framework. In .NET Framework, there is no implicit conversion fromstring
toReadOnlySpan<char>
. So even C# developers will be forced to use theAsSpan
extension method, unless we give them a nice overload to use.