1- // Copyright (c) .NET Foundation. All rights reserved.
1+ // Copyright (c) .NET Foundation. All rights reserved.
22// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
44using System ;
@@ -10,6 +10,7 @@ namespace NuGetGallery.Infrastructure.Authentication
1010 public class ApiKeyV4
1111 {
1212 private const int IdPartLengthBytes = 10 ;
13+ private const int PasswordPartLengthBytes = 16 ;
1314 private static readonly byte [ ] IdPrefix = Encoding . ASCII . GetBytes ( "v4" ) ;
1415
1516 internal const int IdPartBase32Length = 20 ;
@@ -79,30 +80,35 @@ public bool Verify(string hashedApiKey)
7980 }
8081
8182 // The verification is not case sensitive. This is to maintain the existing behavior that ApiKey authentication is not case-sensitive.
82- return V3Hasher . VerifyHash ( hashedApiKeyPasswordPart . ToUpper ( ) . FromBase32String ( ) , PasswordPart ) ;
83+ return V3Hasher . VerifyHash ( hashedApiKeyPasswordPart . ToUpperInvariant ( ) . FromBase32String ( ) , PasswordPart ) ;
8384 }
8485
8586 private void CreateInternal ( )
8687 {
87- // Create Id
88- var randomBytes = new byte [ IdPartLengthBytes ] ;
88+ // Create ID. This will be incorporated into the prefix of the final API key.
89+ // After formatting, this will be stored as clear text in the DB for lookup.
90+ var idPartBytes = new byte [ IdPartLengthBytes ] ;
91+ var passwordPartBytes = new byte [ PasswordPartLengthBytes ] ;
8992 using ( var rng = new RNGCryptoServiceProvider ( ) )
9093 {
91- rng . GetNonZeroBytes ( randomBytes ) ;
94+ rng . GetNonZeroBytes ( idPartBytes ) ;
95+ rng . GetBytes ( passwordPartBytes ) ;
9296 }
9397
9498 byte [ ] idBytes = new byte [ IdPartLengthBytes + IdPrefix . Length ] ;
9599 Buffer . BlockCopy ( src : IdPrefix , srcOffset : 0 , dst : idBytes , dstOffset : 0 , count : IdPrefix . Length ) ;
96- Buffer . BlockCopy ( src : randomBytes , srcOffset : 0 , dst : idBytes , dstOffset : IdPrefix . Length , count : randomBytes . Length ) ;
100+ Buffer . BlockCopy ( src : idPartBytes , srcOffset : 0 , dst : idBytes , dstOffset : IdPrefix . Length , count : idPartBytes . Length ) ;
97101
98- // Convert to Base32 string. The length of the string is APIKeyV4_IdPartBase64Length
102+ // Convert to Base32 string. The length of the string is ApiKeyV4.IdPartBase32Length
99103 string idString = idBytes . ToBase32String ( ) . RemoveBase32Padding ( ) ;
100104
101- // Create password
102- var passwordString = Guid . NewGuid ( ) . ToByteArray ( ) . ToBase32String ( ) . RemoveBase32Padding ( ) ;
105+ // Create password. This will become the suffix of the API key and hashed before storing in the DB.
106+ var passwordString = passwordPartBytes . ToBase32String ( ) . RemoveBase32Padding ( ) ;
103107 passwordString = Normalize ( passwordString ) ;
104108
105109 // No need to remove padding or normalize here.. it's stored in the DB and doesn't need to be pretty
110+ // The hashed password bytes internally contains parameters for PBKDF2 key derivation, such as the salt,
111+ // iteration count, and algorithm used, in addition to the hash itself.
106112 var hashedPasswordString = V3Hasher . GenerateHashAsBytes ( passwordString ) . ToBase32String ( ) ;
107113
108114 IdPart = Normalize ( idString ) ;
@@ -123,7 +129,7 @@ private bool TryParseInternal(string plaintextApiKey)
123129 var id = plaintextApiKey . Substring ( 0 , IdPartBase32Length ) ;
124130 var validId = id
125131 . AppendBase32Padding ( )
126- . ToUpper ( )
132+ . ToUpperInvariant ( )
127133 . TryDecodeBase32String ( out var idBytes ) ;
128134
129135 if ( ! validId )
@@ -152,7 +158,9 @@ private bool TryParseInternal(string plaintextApiKey)
152158
153159 private string Normalize ( string input )
154160 {
161+ // This does not change the entropy of the input because the input is a base32 string, which is case
162+ // insensitive. The Base32 encoder produces an all uppercase string. The resulting API key is all lowercase.
155163 return input . ToLowerInvariant ( ) ;
156164 }
157165 }
158- }
166+ }
0 commit comments