Skip to content

Commit 6a3f4c8

Browse files
authored
Improve ApiKeyV4 based on review feedback (#10234)
These changes are non-breaking and should produce API keys with similar characteristics. The ToUpper change is because this code should not be culture aware, and the character set is just base32 anyway. Added a comment about no entropy impact in Normalize. Use crypto random instead of Guid.NewGuid for 16 random bytes.
1 parent e003331 commit 6a3f4c8

1 file changed

Lines changed: 19 additions & 11 deletions

File tree

src/NuGetGallery.Services/Authentication/ApiKeyV4.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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

44
using 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

Comments
 (0)