Skip to content
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

One-shot AES CBC and ECB #2406

Closed
vcsjones opened this issue Nov 4, 2019 · 44 comments · Fixed by #55480
Closed

One-shot AES CBC and ECB #2406

vcsjones opened this issue Nov 4, 2019 · 44 comments · Fixed by #55480
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Security
Milestone

Comments

@vcsjones
Copy link
Member

vcsjones commented Nov 4, 2019

.NET Core 3.0 has AesCcm and AesGcm type that are simple, one-shot APIs. I really like the API shape of these and would suggest that it might be worth adding AesCbc and AesEcb (sorry) types.

The one-shot (no streaming, no update / finalize) would give a few benefits.

  • I like that the mode is part of the type. The modes aren't really interchangeable, and having them as part of the type helps suggest that. That is, as far as using a primitive goes, I can't really "upgrade" from one mode to another like it's some kind of configuration. It requires some thought and consideration as to how each mode behaves.

  • Type-per-mode would allow the API shape to fit the mode better (no IV for ECB).

  • One-shot usage would be hard to "you're holding it wrong". The API surface would be simple Decrypt and Encrypt methods.

  • One-shot API would mean there is no state to carry between calls (like CNG updating the IV).
    That might afford a few more opportunities to use CryptoPool or stack buffers instead of new byte[].

  • Do not support some less common types of block padding (PKCS#7 and no padding only). This can help developers make better choices security-wise and push handling down to the underlying implementation.

/cc @GrabYourPitchforks @bartonjs


Updated API Proposal

Since the API review we added CFB support, so this adds the CFB members, and adds a mild reshaping to account for CFB being different than ECB and CBC:

Current approved additions, with no updates required:

namespace System.Security.Cryptography
{
    public abstract class SymmetricAlgorithm
    {
        // ECB (Block Aligned, no default padding, no IV)
        public byte[] DecryptEcb(byte[] ciphertext, PaddingMode paddingMode);
        public byte[] DecryptEcb(ReadOnlySpan<byte> ciphertext, PaddingMode paddingMode);
        public int DecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode);
        public byte[] EncryptEcb(byte[] plaintext, PaddingMode paddingMode);
        public byte[] EncryptEcb(ReadOnlySpan<byte> plaintext, PaddingMode paddingMode);
        public int EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode);
        public bool TryDecryptEcb(
            ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten);
        protected virtual bool TryDecryptEcbCore(
            ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten);
        public bool TryEncryptEcb(
            ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten);
        protected virtual bool TryEncryptEcbCore(
            ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten);

        // CBC (Block Aligned, default to PKCS#7 padding, IV required)
        public byte[] DecryptCbc(byte[] ciphertext, byte[] iv, PaddingMode paddingMode = PaddingMode.PKCS7);
        public byte[] DecryptCbc(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = PaddingMode.PKCS7);
        public int DecryptCbc(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination,
            PaddingMode paddingMode = PaddingMode.PKCS7);
        public byte[] EncryptCbc(byte[] plaintext, byte[] iv, PaddingMode paddingMode = PaddingMode.PKCS7);
        public byte[] EncryptCbc(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = PaddingMode.PKCS7);
        public int EncryptCbc(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination,
            PaddingMode paddingMode = PaddingMode.PKCS7);
        public bool TryDecryptCbc(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten,
            PaddingMode paddingMode = PaddingMode.PKCS7);
        protected virtual bool TryDecryptCbcCore(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode,
            out int bytesWritten);
        public bool TryEncryptCbc(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten,
            PaddingMode paddingMode = PaddingMode.PKCS7);
        protected virtual bool TryEncryptCbcCore(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode,
            out int bytesWritten);
    }
}

CFB additions

namespace System.Security.Cryptography
{
    public abstract class SymmetricAlgorithm
    {
        // CFB (Feedback-size aligned, default to No padding, IV required, adds feedback size (default 8))
        public byte[] DecryptCfb(
            byte[] ciphertext, byte[] iv, PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);
        public byte[] DecryptCfb(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = PaddingMode.None,
            int feedbackSizeBits = 8);
        public int DecryptCfb(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination,
            PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);

        public byte[] EncryptCfb(
            byte[] plaintext, byte[] iv, PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);
        public byte[] EncryptCfb(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = PaddingMode.None,
            int feedbackSizeBits = 8);
        public int EncryptCfb(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination,
            PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);

        public bool TryDecryptCfb(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten,
            PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);

        public bool TryEncryptCfb(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten,
            PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);

        protected virtual bool TryDecryptCfbCore(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode,
            int feedbackSizeBits, out int bytesWritten);

        protected virtual bool TryEncryptCfbCore(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode,
            int feedbackSizeBits, out int bytesWritten);
    }
}

Modifications of existing approved members:

namespace System.Security.Cryptography
{
    public abstract class SymmetricAlgorithm
    {
-       public int GetCiphertextLength(PaddingMode paddingMode, int plaintextLength);
+       public int GetCiphertextLengthEcb(int plaintextLength, PaddingMode paddingMode);
+       public int GetCiphertextLengthCbc(int plaintextLength, PaddingMode paddingMode = PaddingMode.PKCS7);
+       public int GetCiphertextLengthCfb(
+           int plaintextLength, PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);
    }
}
Previously discussed, outdated, API Proposal
namespace System.Security.Cryptography {
    public abstract class SymmetricAlgorithm {

        public byte[] EncryptEcb(
            byte[] plaintext,
            PaddingMode paddingMode = default);
        public byte[] EncryptEcb(
            ReadOnlySpan<byte> plaintext,
            PaddingMode paddingMode = default);
        public int EncryptEcb(
            ReadOnlySpan<byte> plaintext,
            Span<byte> destination,
            PaddingMode paddingMode = default);
        public bool TryEncryptEcb(
            ReadOnlySpan<byte> plaintext,
            Span<byte> destination,
            out int bytesWritten,
            PaddingMode paddingMode = default);

        public byte[] EncryptCbc(
            byte[] plaintext,
            byte[] iv,
            PaddingMode paddingMode = default);
        public byte[] EncryptCbc(
            ReadOnlySpan<byte> plaintext,
            ReadOnlySpan<byte> iv,
            PaddingMode paddingMode = default);
        public int EncryptCbc(
            ReadOnlySpan<byte> plaintext,
            ReadOnlySpan<byte> iv,
            Span<byte> destination,
            PaddingMode paddingMode = default);
        public bool TryEncryptCbc(
            ReadOnlySpan<byte> plaintext,
            ReadOnlySpan<byte> iv,
            Span<byte> destination,
            out int bytesWritten,
            PaddingMode paddingMode = default);

        public byte[] DecryptEcb(
            byte[] ciphertext,
            PaddingMode paddingMode = default);
        public byte[] DecryptEcb(
            ReadOnlySpan<byte> ciphertext,
            PaddingMode paddingMode = default);
        public int DecryptEcb(
            ReadOnlySpan<byte> ciphertext,
            Span<byte> destination,
            PaddingMode paddingMode = default);
        public bool TryDecryptEcb(
            ReadOnlySpan<byte> ciphertext,
            Span<byte> destination,
            out int bytesWritten,
            PaddingMode paddingMode = default);

        public byte[] DecryptCbc(
            byte[] ciphertext,
            byte[] iv,
            PaddingMode paddingMode = default);
        public byte[] DecryptCbc(
            ReadOnlySpan<byte> ciphertext,
            ReadOnlySpan<byte> iv,
            PaddingMode paddingMode = default);
        public int DecryptCbc(
            ReadOnlySpan<byte> ciphertext,
            ReadOnlySpan<byte> iv,
            Span<byte> destination,
            PaddingMode paddingMode = default);
        public bool TryDecryptCbc(
            ReadOnlySpan<byte> ciphertext,
            ReadOnlySpan<byte> iv,
            Span<byte> destination,
            out int bytesWritten,
            PaddingMode paddingMode = default);


        protected virtual bool TryEncryptCbcCore(
            ReadOnlySpan<byte> plaintext,
            ReadOnlySpan<byte> iv,
            Span<byte> destination,
            PaddingMode paddingMode,
            out int bytesWritten);
        protected virtual bool TryDecryptCbcCore(
            ReadOnlySpan<byte> ciphertext,
            ReadOnlySpan<byte> iv,
            Span<byte> destination,
            PaddingMode paddingMode,
            out int bytesWritten);
        protected virtual bool TryEncryptEcbCore(
            ReadOnlySpan<byte> plaintext,
            Span<byte> destination,
            PaddingMode paddingMode,
            out int bytesWritten);
        protected virtual bool TryDecryptEcbCore(
            ReadOnlySpan<byte> ciphertext,
            Span<byte> destination,
            PaddingMode paddingMode,
            out int bytesWritten);

        public int GetCiphertextLength(PaddingMode paddingMode, int plaintextLength);
    }
}
@scalablecory
Copy link
Contributor

Do we want to make it easier to use ECB? :)

@vcsjones
Copy link
Member Author

vcsjones commented Nov 4, 2019

@scalablecory I even pre-apologized for the suggestion at the very beginning 😅.

If it were up to me ECB of any kind would be in an out of band package and removed from shared framework. That said, I know there is a very real need for it by some folks, so I threw it out there as an idea.

When / if this comes to an API proposal the thought can be put further to the test.

@bartonjs
Copy link
Member

bartonjs commented Nov 5, 2019

ECB is important as the building block for things that we don't have intrinsically, like AES-KW (IIRC that uses ECB)--if nothing else, pretty much every other mode can be written in terms of ECB.

I think the one shots are good, but I don't know that we want distinct types for classic modes. I had pictured these as just EncryptEcb, EncryptCbc, etc methods on SymmetricAlgorithm -- the drawback is the IV property, which was already weird.

And I don't think it makes sense to drop support for the alternate padding schemes.

AesCcm and AesGcm got new types because they're fundamentally different concepts than "just AES", and we wanted to not have any mode where the IV (nonce) got reused because of a property existing where it shouldn't have. And the notion of "what tag sizes are supported" is different per tag algorithm (could have been EncryptGcm, DecryptGcm, SupportedGcmTagSizes, etc, I guess...

Really the problem I want to avoid here is that when AES2 comes around we don't end up with type explosions from { AES, AES2, 3DES } x { CBC, ECB, CTR, ... }. The places that it makes sense to be a bit polymorphic we should be, and CBC/ECB/CTR/etc feel more like different methods to me (verbs/adverbs) than different classes (nouns).

@vcsjones
Copy link
Member Author

vcsjones commented Nov 6, 2019

I don't know that we want distinct types for classic modes. I had pictured these as just EncryptEcb, EncryptCbc, etc methods on SymmetricAlgorithm.

Yes, I'm not a fan of the IV property since that makes the developer think the IV has the same use / lifetime as the key. Padding with CTR is also somewhat weird, too. FeedbackSize is also a bit off for Aes. The current design feels very "do all the things" for different modes.

The places that it makes sense to be a bit polymorphic we should be

The usage of AES is rarely polymorphic in my own uses - the usage of AES is almost always specific to what mode it is in. AES ECB can only be used as a building block for key-wrap or rare occurrences of "I have only one block to encrypt with one time use keys". CBC needs a CSPRNG IV and does not tolerate predictable IVs. CTR tolerates predictable IVs and can be a counter, or it can be zero if the key is used once. Etc, etc.

So while the API shape may be polymorphic I don't really think it can ever be used as such. My thought was to encode that in the type.

And I don't think it makes sense to drop support for the alternate padding schemes.

Fair enough. I don't have strong arguments to remove it other than I will personally avoid them. I was hoping to push the padding handling down to the implementation.

@bartonjs
Copy link
Member

bartonjs commented Nov 8, 2019

The usage of AES is rarely polymorphic in my own uses

Well, I mean more like "RC2, DES, 3DES, and AES all support ECB, CBC, ...", so something like

partial class SymmetricAlgorithm
{
    // default(PaddingMode) => use the property?
    public int EncryptEcb(ReadOnlySpan<byte> source, Span<byte> destination, PaddingMode paddingMode = default);
    public int DecryptEcb(ReadOnlySpan<byte> source, Span<byte> destination, PaddingMode paddingMode = default);
    protected virtual bool TryTransformCbc(ReadOnlySpan<byte> source, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode, bool encrypt);

    // Maybe change iv to Span and add an optional parameter for "generate it for me",
    // but perhaps either "generate it for me" or "I already have one" is a named alternative,
    // such as EncryptCbcWithIv, EncryptCbcWithRandomIv
    public int EncryptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode = default);
    public int DecryptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode = default);

    // Split nonce and counter? Unify them?
    public int EncryptCtr(ReadOnlySpan<byte> plaintext, ..., Span<byte> destination);
    public int DecryptCtr(ReadOnlySpan<byte> ciphertext, ..., Span<byte> destination);

    // I suppose for consistency, feedbackSize == 0 means "use the property".
    // Same IV questions as CBC
    public int EncryptCfb(..., int feedbackSize = 0);
    public int DecryptCfb(..., int feedbackSize = 0);

// OFB, CTS?, etc.
}

All of the public methods (span-accepting, byte[] returning, Span-Try, etc) can do argument validation (and, when appropriate, random-value filling) then defer the single (per mode) protected virtual bool TryTransform[Mode](...) (which could be written in terms of CreateTransform for the base class). It's the Template Method Pattern, but doesn't need the Core suffix since it got a weird name with the addition of the encrypt (vs decrypt) Boolean.

No IV/Nonce value would ever be read from the IV property, but Padding and FeedbackSize seem like they can (as you rarely want to change them).

@vcsjones
Copy link
Member Author

vcsjones commented Nov 22, 2019

Well, I mean more like "RC2, DES, 3DES, and AES all support ECB, CBC,

Okay, if per-type modes is not looking like a good idea, then I would propose:

namespace System.Security.Cryptography {
    public abstract class SymmetricAlgorithm {
        public byte[] EncryptEcb(ReadOnlySpan<byte> plaintext, PaddingMode paddingMode = default);
        public bool TryEncyptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);
        public byte[] EncryptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = default);
        public bool TryEncyptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);

        public byte[] DecryptEcb(ReadOnlySpan<byte> ciphertext, PaddingMode paddingMode = default);
        public bool TryDecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);
        public byte[] DecryptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = default);
        public bool TryDecryptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);
    }
}

Thoughts.

  1. It didn't make sense to me to do CFB/OFB since it's missing. (Something is causing the LocallocLarge test to run slowly #4647)
  2. CTS appears to be inconsistently supported? Is it CNG only? Doesn't work on my Linux box.
  3. I see AES CTR in your proposal. Was your intention to build CTR on top of ECB?

@bartonjs
Copy link
Member

public virtual

  • ✓ DO prefer protected accessibility over public accessibility for virtual members. Public members should provide extensibility (if required) by calling into a protected virtual member.

CFB/OFB/CTS/CTR/etc

Yeah, they wouldn't need to be done yet, but having a pattern worked out ahead of time seems virtuous. (CFB, at least, is on the 5.0 plan).

@vcsjones
Copy link
Member Author

DO prefer protected accessibility over public accessibility for virtual members. Public members should provide extensibility (if required) by calling into a protected virtual member.

Hm. That one is new to me. I struggle to find this documentation in the repository. There's this but doesn't mention that. Anyway, seems like a reasonable thing, updated.

@bartonjs
Copy link
Member

That one is at https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/virtual-members.

As part of updating the guidelines for a 3rd edition of Framework Design Guidelines we're also looking at how to better (more modernly?) share the guidelines. (Since there's a published book involved there's a bit of copyright/contract complication that slows things down a bit)

@GrabYourPitchforks
Copy link
Member

GrabYourPitchforks commented Nov 23, 2019

Just throwing this out there for discussion...

If we were to reinvent the SymmetricAlgorithm class today, would we still have properties like Key on it? It was already mentioned earlier that the IV property was problematic.

In my ideal world, the API would look something like below. (Ignore for now that it violates the FDG.)

public abstract class SymmetricAlgorithm2 : IDisposable
{
    public abstract int Decrypt(ROS<byte> src, Span<byte> dest);
    public abstract int Encrypt(ROS<byte> src, Span<byte> dest);

    public abstract void Dispose();
}

public sealed class AesAlgorithm2 : SymmetricAlgorithm2
{
    public AesAlgorithm(ReadOnlySpan<byte> key, ChainingMode chainingMode, PaddingMode paddingMode);
}

Basically, everything about how the algorithm behaves is fixed at construction time, and there's just Encrypt and Decrypt. Maybe I'm too influenced by the BCrypt* APIs? :)

Edit: I should clarify that this sketch API is only useful for one-shot. It's useless for streaming.

@vcsjones
Copy link
Member Author

would we still have properties like Key on it?

Probably? Keys are usually tied to some lifetime of "thing which can do encryption with the key" like a BCRYPT_KEY_HANDLE. Even in one-shot scenarios using the same key many times makes plenty of sense. The IV on the other hand has a lifetime associated with each encrypt / decrypt call.

If we were to reinvent the SymmetricAlgorithm class today..

I seem to have very different tastes from y'all on this. In my "ideal" world the mode is part of the type (hence my original suggestion) or at least something that is "compile time", not some attribute on a thing. The mode simply dictates too much of the API shape for me, and like CNG / BCrypt, you end up with some invariant structure of "stuff that only applies to the mode" like pPaddingInfo being void*.

I covered a lot of that above, so I won't reiterate it, but between feedback size, IV, nonces, etc, and some other modes of AES that are even more bizarre like IGE which has two IVs, trying to cram the mode into a single API surface doesn't make a lot of sense to me.

If the mode dictates what information is necessary for it to function, then a "base" encrypt / decrypt doesn't work.

The sketched API above doesn't have an IV in it anywhere, for example. Where does it go? It can't go on Encrypt / Decrypt since SymmetricAlgorithm doesn't have it, making it a property makes the inheritance work but I dislike that for reasons already stated. I think many people mistakenly believe the IV's usage lifetime is the same as the key's then when they "learn cryptography through intellisense".

You can have a method per-mode, or a type per-mode, and now I'm back to where I started.

@bartonjs
Copy link
Member

would we still have properties like Key on it?

No, I wouldn't bother with letting you get it back 😄. Maybe an export method, definitely not a property.

In my ideal world, the API would look something like below. [Candidate for SymmetricAlgorithm2]

I agree with @vcsjones, the mode inputs don't lend themselves to a general "encrypt/decrypt" virtual. Looking at the current model (ignoring CreateEncryptor(key,iv)), the strawman SymmetricAlgorithm2, and what we did for GCM/CCM, the instance is "the key", and then we need to model how things work.

Maybe I'm too influenced by the BCrypt* APIs? :)

Perhaps, but we can't (shouldn't/won't) take a PVOID for "uh, extra stuff needed for the mode...", like the IV/Nonce/TagSize/etc.

You can have a method per-mode, or a type per-mode, and now I'm back to where I started.

Yeah, and both are fine-ish. Type-per-mode makes things like PBES less pleasant since there's no unifying representation of (e.g.) CBC, which is why I'm favoring method(-pair)-per-mode. Type-per-algorithm-and-mode also makes for a lot more namespace-level noise for IntelliSense. (Type-per-mode DOES have the benefit of suggesting that you shouldn't mix modes with the same key (or, at least, not semi-suggesting mixing modes is OK), but I don't feel it does that "strongly" enough to offset the value lost to things like the PBES implementations for processing PKCS8/PKCS12).

@vcsjones
Copy link
Member Author

No, I wouldn't bother with letting you get it back

Sure I agree with that. I took the question to mean "is the key an attribute of the object instance or method call". I meant to say it should not be a method parameter.

@vcsjones
Copy link
Member Author

which is why I'm favoring method(-pair)-per-mode

Well, I suppose you can fix that with moar types...

public interface ICbcMode {
    bool TryEncyptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);
    bool TryDecyptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);
}

public class AesCbc : ICbcMode {
}

public class TripleDesCbc : ICbcMode {
}

I'm not sure if I am serious or not, but I see your point.

Type-per-algorithm-and-mode also makes for a lot more namespace-level noise for IntelliSense

Could stick it in a new Primitives namespace.

or, at least, not semi-suggesting mixing modes is OK

I think part of what I don't like about mode methods is that it forever implies that any new mode might apply to all future algorithms. Perhaps AES2 won't define any of these modes. Perhaps it'll only define GCM-SIV.

@bartonjs
Copy link
Member

I think part of what I don't like about mode methods is that it forever implies that any new mode might apply to all future algorithms.

There is a bit of a point-in-time "things that work everywhere" (not CCM/GCM) for deciding things can go on SymmetricAlgorithm, but that was really already done by the Mode property. The ultimate recourse is "NotSupportedException" if some future algorithm is not defined for ECB/CBC/etc modes.

But since we use SymmetricAlgorithm for block ciphers, and the block modes apply to all block ciphers, I don't really see a place where it would fall apart. (If we added a stream cipher, it'd fall apart, but so would the current model. If we ever added one it'd probably need a new base class already, or just be its own, standalone, thing.)

@vcsjones
Copy link
Member Author

vcsjones commented Nov 26, 2019

Okay. In that case then I will stick with my API suggestion here. I might also suggest an API for getting the ciphertext size based on padding?

// TODO: validation, no overflowing, etc.
public int GetCipherTextLength(PaddingMode paddingMode, int plaintextLength) {
    switch (paddingMode) {
        case PaddingMode.None:
            return plaintextLength; //TODO: throw if % BlockSize != 0?
        case PaddingMode.Zeros:
        case PaddingMode.PKCS7:
        case PaddingMode.ANSIX923:
        case PaddingMode.ISO10126:
            return (plaintextLength | BlockSize - 1) + 1;
        default:
            throw new CryptographicException();
    }
}

@bartonjs
Copy link
Member

GetCipherTextLength seems good to me. Looking at your proposal again I don't see anything virtual, where's the hook for the implementation types?

My suggestion was a per-mode method like protected virtual bool TryTransformCbc(ReadOnlySpan<byte> source, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode, bool encrypt);. I unified Encrypt and Decrypt into one Transform just to cut down on the number of required overrides. TryEncryptCbcCore/TryDecryptCbcCore is also reasonable (it's more straightforward, but every mode requires two overrides instead of one).

@vcsjones
Copy link
Member Author

Looking at your proposal again I don't see anything virtual, where's the hook for the implementation types?

Doh.

I unified Encrypt and Decrypt into one Transform just to cut down on the number of required overrides.

I separated them, only because I agree with the straight-forward remark and it is something I would expect very few developers to actually do.

namespace System.Security.Cryptography {
    public abstract class SymmetricAlgorithm {
        public byte[] EncryptEcb(ReadOnlySpan<byte> plaintext, PaddingMode paddingMode = default);
        public bool TryEncyptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);
        public byte[] EncryptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = default);
        public bool TryEncyptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);

        public byte[] DecryptEcb(ReadOnlySpan<byte> ciphertext, PaddingMode paddingMode = default);
        public bool TryDecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);
        public byte[] DecryptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = default);
        public bool TryDecryptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);

        protected virtual bool TryEncryptCbcCore(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);
        protected virtual bool TryDecryptCbcCore(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);
        protected virtual bool TryEncryptEcbCore(ReadOnlySpan<byte> plaintext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);
        protected virtual bool TryDecryptEcbCore(ReadOnlySpan<byte> ciphertext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);

        public int GetCiphertextLength(PaddingMode paddingMode, int plaintextLength);
    }
}

Both you and @GrabYourPitchforks's Span<T>-destination suggestions eschew the Try- pattern and return an int instead. Is that now a better practice? What if destination Span<T> is too small?

@bartonjs
Copy link
Member

Both you and @GrabYourPitchforks's Span-destination suggestions eschew the Try- pattern and return an int instead. Is that now a better practice?

It's something that can be done in addition to the Try. If I'm a caller with a presized destination (because I used GetCiphertextLength or it's decrypt and I provided a big enough buffer) I don't ever expect false. Rather than redundantly checking, or worse... ignoring the return, I can call the int-returning non-Try.

What if destination Span is too small?

throw new ArgumentException(somethingAboutBufferTooSmall, nameof(destination))

FWIW, here's a sample of almost every possible variation of a Span-writing thing. Which ones are appropriate depends a lot on context:

public partial class ByteOperations {
    public static byte[] FromHexadecimal(ReadOnlySpan<char> source) { ... }
    public static byte[] FromHexadecimal(char[] source) { ... }
    public static byte[] FromHexadecimal(char[] source, int sourceIndex, int count) { ... }
    public static byte[] FromHexadecimal(ArraySegment<char> source) { ... }
    public static byte[] FromHexadecimal(string source) { ... }
    
    public static int FromHexadecimal(ReadOnlySpan<char> source, Span<byte> destination) { ... }
    public static int FromHexadecimal(char[] source, byte[] destination) { ... }
    public static int FromHexadecimal(byte[] source, int sourceIndex, char[] destination, int destinationIndex, int count) { ... }
    public static int FromHexadecimal(ArraySegment<char> source, ArraySegment<byte> destination) { ... }
    
    public static bool TryFromHexadecimal(ReadOnlySpan<char> source, Span<byte> destination, out int bytesWritten) { ... }
    public static bool TryFromHexadecimal(char[] source, byte destination, out int bytesWritten) { ... }
    public static bool TryFromHexadecimal(char[] source, int sourceIndex, byte destination, int destinationIndex, int count, out int bytesWritten) { ... }
    public static bool TryFromHexadecimal(ArraySegment<char> source, ArraySegment<byte> destination, out int bytesWritten) { ... }
    
    public static OperationStatus FromHexadecimal(ReadOnlySpan<char> source, Span<byte> destination, out int charsConsumed, out int bytesWritten) { ... }
    public static OperationStatus FromHexadecimal(char[] source, byte[] destination, out int charsConsumed, out int bytesWritten) { ... }
    public static OperationStatus FromHexadecimal(char[] source, int sourceIndex, byte[] destination, int destinationIndex, int count, out int charsConsumed, out int bytesWritten) { ... }
    public static OperationStatus FromHexadecimal(ArraySegment<char> source, ArraySegment<byte> destination, out int charsConsumed, out int bytesWritten) { ... }
}

Since this one isn't char-based the string overloads drop out. OperationStatus isn't needed because there's no invalid input data to report. We don't really need the array-with-offset versions or ArraySegment.

So a full version of EncryptCbc might be

public byte[] EncryptCbc(byte[] plaintext, byte[] iv, PaddingMode paddingMode = default);
public byte[] EncryptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = default);
public int EncyptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode = default);

public bool TryEncyptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> 
destination, out int bytesWritten, PaddingMode paddingMode = default);

// All of the above can be written in terms of this.
protected virtual bool TryEncryptCbcCore(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);

@vcsjones
Copy link
Member Author

So a full version of EncryptCbc might be

What's the advantage of taking a ROS<byte> for plaintext and iv as well as byte[] if byte[] is implicitly convertible to ROS<byte>?

@bartonjs
Copy link
Member

CLR languages that don't support implicit conversions; or developers who don't understand that there's an implicit conversion.

When we first started making (ReadOnly)Span-only things we found out that none of us paid attention to the fact that F# won't use implicit conversions, and we made the calling syntax really ugly. F# now has a special rule to permit T[] => Span<T> and T[] => ReadOnlySpan<T> (and maybe ReadOnlySpan to Span), and we added the AsSpan() methods to make it nicer than new ReadOnlySpan<byte>(array), so it's not a universal thing. On highly-visible types that novice developers interact with, the byte[] versions are important for ease of use/getting started. Niche/low-level/power types can decide "familiarity with spans is a requirement".

@vcsjones
Copy link
Member Author

As always, thank you for the detailed explanations. Given that, the proposal now looks something like this:

namespace System.Security.Cryptography {
    public abstract class SymmetricAlgorithm {
        public byte[] EncryptEcb(byte[] plaintext, PaddingMode paddingMode = default);
        public byte[] EncryptEcb(ReadOnlySpan<byte> plaintext, PaddingMode paddingMode = default);
        public int EncyptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode = default);
        public bool TryEncyptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);

        public byte[] EncryptCbc(byte[] plaintext, byte[] iv, PaddingMode paddingMode = default);
        public byte[] EncryptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = default);
        public int EncyptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode = default);
        public bool TryEncyptCbc(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);

        public byte[] DecryptEcb(byte[] ciphertext, PaddingMode paddingMode = default);
        public byte[] DecryptEcb(ReadOnlySpan<byte> ciphertext, PaddingMode paddingMode = default);
        public int DecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode = default);
        public bool TryDecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);

        public byte[] DecryptCbc(byte[] ciphertext, byte[] iv, PaddingMode paddingMode = default);
        public byte[] DecryptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = default);
        public int DecryptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode = default);
        public bool TryDecryptCbc(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode = default);


        protected virtual bool TryEncryptCbcCore(ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);
        protected virtual bool TryDecryptCbcCore(ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);
        protected virtual bool TryEncryptEcbCore(ReadOnlySpan<byte> plaintext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);
        protected virtual bool TryDecryptEcbCore(ReadOnlySpan<byte> ciphertext, Span<byte> destination, out int bytesWritten, PaddingMode paddingMode);

        public int GetCiphertextLength(PaddingMode paddingMode, int plaintextLength);
    }
}

@GrabYourPitchforks
Copy link
Member

API review: CBC padding mode should default to PKCS7, ECB padding modes should be non-default (require explicit value at call site). For all methods, move non-out parameters before out parameters.

@bartonjs - please document whether the newly introduced virtual members would ever mutate the underlying instance (changing block mode or padding mode) or throw.

@vcsjones
Copy link
Member Author

Thanks. Will take this if someone wants to assign to me.

@bartonjs
Copy link
Member

@vcsjones Done. FWIW, during the meeting we landed on the Core methods NOT changing the mode, so they'll throw in the wrong mode.

It'll only matter for custom types that directly extend SymmetricAlgorithm, but @GrabYourPitchforks was concerned with people "knowing" that they can call CreateEncryptor/CreateDecryptor across threads without problems; so we'll stay defensive by default.

@vcsjones
Copy link
Member Author

@bartonjs Hm. It sounds like y'all had an idea on implementation that either I am misunderstanding or I am misunderstanding your comment / concerns.

we landed on the Core methods NOT changing the mode

Do you mean the instance property Mode? I envisioned these methods operating wholly independently from the instance, essentially ignoring the Mode, Padding, and IV properties.

they'll throw in the wrong mode.

What is a "wrong" mode? One that does not match the instance's value?

@vcsjones
Copy link
Member Author

@GrabYourPitchforks

ECB padding modes should be non-default (require explicit value at call site). For all methods, move non-out parameters before out parameters.

Optional parameters need to be last. Since ECB would no longer have an optional parameter, the byteWritten out could come after the padding mode, but just for ECB. Taken literally, it would look like this:

public bool TryEncyptEcb(
  ReadOnlySpan<byte> plaintext,
  Span<byte> destination,
  PaddingMode paddingMode,
  out int bytesWritten);

public bool TryEncyptCbc(
  ReadOnlySpan<byte> plaintext,
  ReadOnlySpan<byte> iv,
  Span<byte> destination,
  out int bytesWritten,
  PaddingMode paddingMode = PaddingMode.PKCS7);

Or would you expect the order to be consistent across modes:

public bool TryEncyptEcb(
  ReadOnlySpan<byte> plaintext,
  Span<byte> destination,
  out int bytesWritten,
  PaddingMode paddingMode);

public bool TryEncyptCbc(
  ReadOnlySpan<byte> plaintext,
  ReadOnlySpan<byte> iv,
  Span<byte> destination,
  out int bytesWritten,
  PaddingMode paddingMode = PaddingMode.PKCS7);

@bartonjs
Copy link
Member

public bool TryEncyptEcb(
  ReadOnlySpan<byte> plaintext,
  Span<byte> destination,
  PaddingMode paddingMode,
  out int bytesWritten);

public bool TryEncyptCbc(
  ReadOnlySpan<byte> plaintext,
  ReadOnlySpan<byte> iv,
  Span<byte> destination,
  out int bytesWritten,
  PaddingMode paddingMode = PaddingMode.PKCS7);

is correct for what the group decided. The two calls won't look the same. But that's fine, it further helps to make a brain engage when switching modes 😄.

As for the implementation, while all of the built-in types are going to override the virtual *Core methods, the base class has to do one of:

  1. throw NotSupportedException
  2. Mode = theModeINeed; ICryptoTransform xform = CreateEncryptor(Key, iv.ToArray()); Mode = savedMode; ...
  3. if (Mode != theModeINeed) throw something; ICryptoTransform xform = CreateEncryptor(Key, iv.ToArray()); ...

The suggestion is to do (3).

@bartonjs bartonjs added the api-needs-work API needs work before it is approved, it is NOT ready for implementation label Nov 4, 2020
@vcsjones
Copy link
Member Author

vcsjones commented Nov 4, 2020

@bartonjs Looks good to me; I made one small change to fix the "Encypt" -> "Encrypt" typo.

@bartonjs bartonjs added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-needs-work API needs work before it is approved, it is NOT ready for implementation labels Nov 4, 2020
@bartonjs bartonjs modified the milestones: Future, 6.0.0 Nov 5, 2020
@bartonjs
Copy link
Member

bartonjs commented Nov 10, 2020

Video

Looks good as proposed:

namespace System.Security.Cryptography
{
    public abstract class SymmetricAlgorithm
    {
        // ECB (Block Aligned, no default padding, no IV)
        public byte[] DecryptEcb(byte[] ciphertext, PaddingMode paddingMode);
        public byte[] DecryptEcb(ReadOnlySpan<byte> ciphertext, PaddingMode paddingMode);
        public int DecryptEcb(ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode);
        public byte[] EncryptEcb(byte[] plaintext, PaddingMode paddingMode);
        public byte[] EncryptEcb(ReadOnlySpan<byte> plaintext, PaddingMode paddingMode);
        public int EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode);
        public bool TryDecryptEcb(
            ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten);
        protected virtual bool TryDecryptEcbCore(
            ReadOnlySpan<byte> ciphertext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten);
        public bool TryEncryptEcb(
            ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten);
        protected virtual bool TryEncryptEcbCore(
            ReadOnlySpan<byte> plaintext, Span<byte> destination, PaddingMode paddingMode, out int bytesWritten);

        // CBC (Block Aligned, default to PKCS#7 padding, IV required)
        public byte[] DecryptCbc(byte[] ciphertext, byte[] iv, PaddingMode paddingMode = PaddingMode.PKCS7);
        public byte[] DecryptCbc(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = PaddingMode.PKCS7);
        public int DecryptCbc(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination,
            PaddingMode paddingMode = PaddingMode.PKCS7);
        public byte[] EncryptCbc(byte[] plaintext, byte[] iv, PaddingMode paddingMode = PaddingMode.PKCS7);
        public byte[] EncryptCbc(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = PaddingMode.PKCS7);
        public int EncryptCbc(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination,
            PaddingMode paddingMode = PaddingMode.PKCS7);
        public bool TryDecryptCbc(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten,
            PaddingMode paddingMode = PaddingMode.PKCS7);
        protected virtual bool TryDecryptCbcCore(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode,
            out int bytesWritten);
        public bool TryEncryptCbc(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten,
            PaddingMode paddingMode = PaddingMode.PKCS7);
        protected virtual bool TryEncryptCbcCore(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode,
            out int bytesWritten);
    }

    public abstract class SymmetricAlgorithm
    {
        // CFB (Feedback-size aligned, default to No padding, IV required, adds feedback size (default 8))
        public byte[] DecryptCfb(
            byte[] ciphertext, byte[] iv, PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);
        public byte[] DecryptCfb(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = PaddingMode.None,
            int feedbackSizeBits = 8);
        public int DecryptCfb(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination,
            PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);

        public byte[] EncryptCfb(
            byte[] plaintext, byte[] iv, PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);
        public byte[] EncryptCfb(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, PaddingMode paddingMode = PaddingMode.None,
            int feedbackSizeBits = 8);
        public int EncryptCfb(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination,
            PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);

        public bool TryDecryptCfb(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten,
            PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);

        public bool TryEncryptCfb(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, out int bytesWritten,
            PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);

        protected virtual bool TryDecryptCfbCore(
            ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode,
            int feedbackSizeBits, out int bytesWritten);

        protected virtual bool TryEncryptCfbCore(
            ReadOnlySpan<byte> plaintext, ReadOnlySpan<byte> iv, Span<byte> destination, PaddingMode paddingMode,
            int feedbackSizeBits, out int bytesWritten);
    }

    public abstract class SymmetricAlgorithm
    {
       public int GetCiphertextLengthEcb(int plaintextLength, PaddingMode paddingMode);
       public int GetCiphertextLengthCbc(int plaintextLength, PaddingMode paddingMode = PaddingMode.PKCS7);
       public int GetCiphertextLengthCfb(
           int plaintextLength, PaddingMode paddingMode = PaddingMode.None, int feedbackSizeBits = 8);
    }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Nov 10, 2020
@terrajobst
Copy link
Member

Looks good as proposed:

Of course you'd say that 😝

@vcsjones
Copy link
Member Author

@bartonjs what is the possibility that this could come in as smaller PRs? Perhaps something like:

  1. GetCiphertextLength*
  2. All of the Encrypt/Decrypt APIs, with the Core virtual on SymmetricAlgorithm throwing a "must override" exception. (All of the platform's impls will override it, anyway)
  3. Fill in the Core virtuals with Transform/TransformFinal.

I could understand being iffy on splitting 2 and 3. Maybe you have a better idea how to slice, or maybe you want it all in one go. That's cool too, just floating the idea.

@bartonjs
Copy link
Member

I'm cool with whatever staging makes sense, provided that tests and code comes together. By mode, Span-writing things first then coming back and adding the ease-of-use variants, Span-writing things first+by mode.

Doing the ease-of-use non-virtuals first wouldn't be great, since they'd be hard to test ;); other than that, I don't see any obviously bad plans.

@jeffhandley jeffhandley self-assigned this Jun 17, 2021
@bartonjs bartonjs mentioned this issue Jul 7, 2021
1 task
@vcsjones vcsjones mentioned this issue Jul 9, 2021
2 tasks
@ghost ghost added in-pr There is an active PR which will close this issue when it is merged and removed in-pr There is an active PR which will close this issue when it is merged labels Jul 9, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jul 11, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 13, 2021
@vcsjones
Copy link
Member Author

🥳

@bartonjs bartonjs removed their assignment Jul 26, 2021
@ghost ghost locked as resolved and limited conversation to collaborators Aug 25, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.Security
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants