mirror of
https://github.com/bitwarden/server.git
synced 2026-01-31 14:13:18 +08:00
This reverts commit 8cb8030534.
This commit is contained in:
committed by
GitHub
parent
8cb8030534
commit
029a5f6a2d
@@ -1,5 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
@@ -10,7 +9,6 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@@ -592,504 +590,6 @@ public class AccountsControllerTests : IDisposable
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostRegisterVerificationEmailClicked(requestModel));
|
||||
}
|
||||
|
||||
// PM-28143 - When removing the old properties, update this test to just test the new properties working
|
||||
// as expected.
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostRegisterFinish_EmailVerification_BothDataForms_ProduceEquivalentOutcomes(
|
||||
string email,
|
||||
string emailVerificationToken,
|
||||
string masterPasswordHash,
|
||||
string masterKeyWrappedUserKey,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey)
|
||||
{
|
||||
// Arrange: new-form model (MasterPasswordAuthenticationData + MasterPasswordUnlockData)
|
||||
|
||||
var kdfData = new KdfRequestModel
|
||||
{
|
||||
KdfType = KdfType.Argon2id,
|
||||
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
Memory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
|
||||
};
|
||||
|
||||
var newModel = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
EmailVerificationToken = emailVerificationToken,
|
||||
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
|
||||
{
|
||||
Kdf = kdfData,
|
||||
MasterPasswordAuthenticationHash = masterPasswordHash,
|
||||
Salt = email // salt choice is not validated here during registration
|
||||
},
|
||||
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
|
||||
{
|
||||
Kdf = kdfData,
|
||||
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
|
||||
Salt = email
|
||||
},
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
// Arrange: legacy-form model (MasterPasswordHash + legacy KDF + UserSymmetricKey)
|
||||
var legacyModel = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
EmailVerificationToken = emailVerificationToken,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Kdf = KdfType.Argon2id,
|
||||
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
|
||||
UserSymmetricKey = masterKeyWrappedUserKey,
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
var newUser = newModel.ToUser();
|
||||
var legacyUser = legacyModel.ToUser();
|
||||
|
||||
_registerUserCommand
|
||||
.RegisterUserViaEmailVerificationToken(Arg.Any<User>(), masterPasswordHash, emailVerificationToken)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act: call with new form
|
||||
var newResult = await _sut.PostRegisterFinish(newModel);
|
||||
// Act: call with legacy form
|
||||
var legacyResult = await _sut.PostRegisterFinish(legacyModel);
|
||||
|
||||
// Assert: outcomes are identical in effect (success response)
|
||||
Assert.NotNull(newResult);
|
||||
Assert.NotNull(legacyResult);
|
||||
|
||||
// Assert: effective users are equivalent
|
||||
Assert.Equal(legacyUser.Email, newUser.Email);
|
||||
Assert.Equal(legacyUser.MasterPasswordHint, newUser.MasterPasswordHint);
|
||||
Assert.Equal(legacyUser.Kdf, newUser.Kdf);
|
||||
Assert.Equal(legacyUser.KdfIterations, newUser.KdfIterations);
|
||||
Assert.Equal(legacyUser.KdfMemory, newUser.KdfMemory);
|
||||
Assert.Equal(legacyUser.KdfParallelism, newUser.KdfParallelism);
|
||||
Assert.Equal(legacyUser.Key, newUser.Key);
|
||||
Assert.Equal(legacyUser.PublicKey, newUser.PublicKey);
|
||||
Assert.Equal(legacyUser.PrivateKey, newUser.PrivateKey);
|
||||
|
||||
// Assert: hash forwarded identically from both inputs
|
||||
await _registerUserCommand.Received(2).RegisterUserViaEmailVerificationToken(
|
||||
Arg.Is<User>(u =>
|
||||
u.Email == newUser.Email &&
|
||||
u.Kdf == newUser.Kdf &&
|
||||
u.KdfIterations == newUser.KdfIterations &&
|
||||
u.KdfMemory == newUser.KdfMemory &&
|
||||
u.KdfParallelism == newUser.KdfParallelism &&
|
||||
u.Key == newUser.Key),
|
||||
masterPasswordHash,
|
||||
emailVerificationToken);
|
||||
|
||||
await _registerUserCommand.Received(2).RegisterUserViaEmailVerificationToken(
|
||||
Arg.Is<User>(u =>
|
||||
u.Email == legacyUser.Email &&
|
||||
u.Kdf == legacyUser.Kdf &&
|
||||
u.KdfIterations == legacyUser.KdfIterations &&
|
||||
u.KdfMemory == legacyUser.KdfMemory &&
|
||||
u.KdfParallelism == legacyUser.KdfParallelism &&
|
||||
u.Key == legacyUser.Key),
|
||||
masterPasswordHash,
|
||||
emailVerificationToken);
|
||||
}
|
||||
|
||||
// PM-28143 - When removing the old properties, update this test to just test the new properties working
|
||||
// as expected.
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostRegisterFinish_OrgInvite_BothDataForms_ProduceEquivalentOutcomes(
|
||||
string email,
|
||||
string orgInviteToken,
|
||||
Guid organizationUserId,
|
||||
string masterPasswordHash,
|
||||
string masterKeyWrappedUserKey,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey)
|
||||
{
|
||||
var kdfData = new KdfRequestModel
|
||||
{
|
||||
KdfType = KdfType.Argon2id,
|
||||
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
Memory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
|
||||
};
|
||||
|
||||
// Arrange: new-form model (MasterPasswordAuthenticationData + MasterPasswordUnlockData)
|
||||
var newModel = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
OrgInviteToken = orgInviteToken,
|
||||
OrganizationUserId = organizationUserId,
|
||||
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
|
||||
{
|
||||
Kdf = kdfData,
|
||||
MasterPasswordAuthenticationHash = masterPasswordHash,
|
||||
Salt = email
|
||||
},
|
||||
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
|
||||
{
|
||||
Kdf = kdfData,
|
||||
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
|
||||
Salt = email
|
||||
},
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
// Arrange: legacy-form model (MasterPasswordHash + legacy KDF + UserSymmetricKey)
|
||||
var legacyModel = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
OrgInviteToken = orgInviteToken,
|
||||
OrganizationUserId = organizationUserId,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Kdf = kdfData.KdfType,
|
||||
KdfIterations = kdfData.Iterations,
|
||||
KdfMemory = kdfData.Memory,
|
||||
KdfParallelism = kdfData.Parallelism,
|
||||
UserSymmetricKey = masterKeyWrappedUserKey,
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
var newUser = newModel.ToUser();
|
||||
var legacyUser = legacyModel.ToUser();
|
||||
|
||||
_registerUserCommand
|
||||
.RegisterUserViaOrganizationInviteToken(Arg.Any<User>(), masterPasswordHash, orgInviteToken, organizationUserId)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act
|
||||
var newResult = await _sut.PostRegisterFinish(newModel);
|
||||
var legacyResult = await _sut.PostRegisterFinish(legacyModel);
|
||||
|
||||
// Assert success
|
||||
Assert.NotNull(newResult);
|
||||
Assert.NotNull(legacyResult);
|
||||
|
||||
// Assert: effective users are equivalent
|
||||
Assert.Equal(legacyUser.Email, newUser.Email);
|
||||
Assert.Equal(legacyUser.MasterPasswordHint, newUser.MasterPasswordHint);
|
||||
Assert.Equal(legacyUser.Kdf, newUser.Kdf);
|
||||
Assert.Equal(legacyUser.KdfIterations, newUser.KdfIterations);
|
||||
Assert.Equal(legacyUser.KdfMemory, newUser.KdfMemory);
|
||||
Assert.Equal(legacyUser.KdfParallelism, newUser.KdfParallelism);
|
||||
Assert.Equal(legacyUser.Key, newUser.Key);
|
||||
Assert.Equal(legacyUser.PublicKey, newUser.PublicKey);
|
||||
Assert.Equal(legacyUser.PrivateKey, newUser.PrivateKey);
|
||||
|
||||
// Assert: hash forwarded identically from both inputs
|
||||
await _registerUserCommand.Received(2).RegisterUserViaOrganizationInviteToken(
|
||||
Arg.Is<User>(u =>
|
||||
u.Email == newUser.Email &&
|
||||
u.Kdf == newUser.Kdf &&
|
||||
u.KdfIterations == newUser.KdfIterations &&
|
||||
u.KdfMemory == newUser.KdfMemory &&
|
||||
u.KdfParallelism == newUser.KdfParallelism &&
|
||||
u.Key == newUser.Key),
|
||||
masterPasswordHash,
|
||||
orgInviteToken,
|
||||
organizationUserId);
|
||||
|
||||
await _registerUserCommand.Received(2).RegisterUserViaOrganizationInviteToken(
|
||||
Arg.Is<User>(u =>
|
||||
u.Email == legacyUser.Email &&
|
||||
u.Kdf == legacyUser.Kdf &&
|
||||
u.KdfIterations == legacyUser.KdfIterations &&
|
||||
u.KdfMemory == legacyUser.KdfMemory &&
|
||||
u.KdfParallelism == legacyUser.KdfParallelism &&
|
||||
u.Key == legacyUser.Key),
|
||||
masterPasswordHash,
|
||||
orgInviteToken,
|
||||
organizationUserId);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostRegisterFinish_NewForm_UsesUnlockDataForKdfAndKey_WhenRootFieldsNull(
|
||||
string email,
|
||||
string emailVerificationToken,
|
||||
string masterPasswordHash,
|
||||
string masterKeyWrappedUserKey,
|
||||
int iterations,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey)
|
||||
{
|
||||
// Arrange: Provide only unlock-data KDF + key; leave root KDF fields null
|
||||
var unlockKdf = new KdfRequestModel
|
||||
{
|
||||
KdfType = KdfType.PBKDF2_SHA256,
|
||||
Iterations = iterations
|
||||
};
|
||||
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
EmailVerificationToken = emailVerificationToken,
|
||||
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
|
||||
{
|
||||
// present but not used by ToUser for KDF/Key
|
||||
Kdf = unlockKdf,
|
||||
MasterPasswordAuthenticationHash = masterPasswordHash,
|
||||
Salt = email
|
||||
},
|
||||
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
|
||||
{
|
||||
Kdf = unlockKdf,
|
||||
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
|
||||
Salt = email
|
||||
},
|
||||
// root KDF fields intentionally null
|
||||
Kdf = null,
|
||||
KdfIterations = null,
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
_registerUserCommand
|
||||
.RegisterUserViaEmailVerificationToken(Arg.Any<User>(), masterPasswordHash, emailVerificationToken)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act
|
||||
var _ = await _sut.PostRegisterFinish(model);
|
||||
|
||||
// Assert: The user passed to command uses unlock-data values
|
||||
await _registerUserCommand.Received(1).RegisterUserViaEmailVerificationToken(
|
||||
Arg.Is<User>(u =>
|
||||
u.Email == email &&
|
||||
u.Kdf == unlockKdf.KdfType &&
|
||||
u.KdfIterations == unlockKdf.Iterations &&
|
||||
u.Key == masterKeyWrappedUserKey),
|
||||
masterPasswordHash,
|
||||
emailVerificationToken);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostRegisterFinish_LegacyForm_UsesRootFields_WhenUnlockDataNull(
|
||||
string email,
|
||||
string emailVerificationToken,
|
||||
string masterPasswordHash,
|
||||
string legacyKey,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey)
|
||||
{
|
||||
// Arrange: Provide only legacy root KDF + key; no unlock-data provided
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
EmailVerificationToken = emailVerificationToken,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
UserSymmetricKey = legacyKey,
|
||||
MasterPasswordUnlock = null,
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
_registerUserCommand
|
||||
.RegisterUserViaEmailVerificationToken(Arg.Any<User>(), masterPasswordHash, emailVerificationToken)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
|
||||
// Act
|
||||
var _ = await _sut.PostRegisterFinish(model);
|
||||
|
||||
// Assert: The user passed to command uses root values
|
||||
await _registerUserCommand.Received(1).RegisterUserViaEmailVerificationToken(
|
||||
Arg.Is<User>(u =>
|
||||
u.Email == email &&
|
||||
u.Kdf == KdfType.PBKDF2_SHA256 &&
|
||||
u.KdfIterations == AuthConstants.PBKDF2_ITERATIONS.Default &&
|
||||
u.Key == legacyKey),
|
||||
masterPasswordHash,
|
||||
emailVerificationToken);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void RegisterFinishRequestModel_Validate_Throws_WhenUnlockAndAuthDataMismatch(
|
||||
string email,
|
||||
string authHash,
|
||||
string masterKeyWrappedUserKey,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey)
|
||||
{
|
||||
// Arrange: authentication and unlock have different KDF and/or salt
|
||||
var authKdf = new KdfRequestModel
|
||||
{
|
||||
KdfType = KdfType.PBKDF2_SHA256,
|
||||
Iterations = AuthConstants.PBKDF2_ITERATIONS.Default
|
||||
};
|
||||
var unlockKdf = new KdfRequestModel
|
||||
{
|
||||
KdfType = KdfType.Argon2id,
|
||||
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
Memory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
|
||||
};
|
||||
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
|
||||
{
|
||||
Kdf = authKdf,
|
||||
MasterPasswordAuthenticationHash = authHash,
|
||||
Salt = email
|
||||
},
|
||||
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
|
||||
{
|
||||
Kdf = unlockKdf,
|
||||
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
|
||||
Salt = email
|
||||
},
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
// Provide a minimal valid token type to satisfy model-level token validation
|
||||
model.EmailVerificationToken = "test-token";
|
||||
|
||||
var ctx = new ValidationContext(model);
|
||||
|
||||
// Act
|
||||
var results = model.Validate(ctx).ToList();
|
||||
|
||||
// Assert mismatched auth/unlock is allowed
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void RegisterFinishRequestModel_Validate_Throws_WhenSaltMismatch(
|
||||
string email,
|
||||
string authHash,
|
||||
string masterKeyWrappedUserKey,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey)
|
||||
{
|
||||
var unlockKdf = new KdfRequestModel
|
||||
{
|
||||
KdfType = KdfType.Argon2id,
|
||||
Iterations = AuthConstants.ARGON2_ITERATIONS.Default,
|
||||
Memory = AuthConstants.ARGON2_MEMORY.Default,
|
||||
Parallelism = AuthConstants.ARGON2_PARALLELISM.Default
|
||||
};
|
||||
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
|
||||
{
|
||||
Kdf = unlockKdf,
|
||||
MasterPasswordAuthenticationHash = authHash,
|
||||
Salt = email
|
||||
},
|
||||
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
|
||||
{
|
||||
Kdf = unlockKdf,
|
||||
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
|
||||
// Intentionally different salt to force mismatch
|
||||
Salt = email + ".mismatch"
|
||||
},
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
// Provide a minimal valid token type to satisfy model-level token validation
|
||||
model.EmailVerificationToken = "test-token";
|
||||
|
||||
var ctx = new ValidationContext(model);
|
||||
|
||||
// Act
|
||||
var results = model.Validate(ctx).ToList();
|
||||
|
||||
// Assert mismatched salts between auth/unlock are allowed
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void RegisterFinishRequestModel_Validate_Throws_WhenAuthHashAndRootHashMismatch(
|
||||
string email,
|
||||
string authHash,
|
||||
string differentRootHash,
|
||||
string masterKeyWrappedUserKey,
|
||||
string publicKey,
|
||||
string encryptedPrivateKey)
|
||||
{
|
||||
// Arrange: same KDF/salt, but authentication hash differs from legacy root hash
|
||||
var kdf = new KdfRequestModel
|
||||
{
|
||||
KdfType = KdfType.PBKDF2_SHA256,
|
||||
Iterations = AuthConstants.PBKDF2_ITERATIONS.Default
|
||||
};
|
||||
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordAuthentication = new MasterPasswordAuthenticationDataRequestModel
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterPasswordAuthenticationHash = authHash,
|
||||
Salt = email
|
||||
},
|
||||
MasterPasswordUnlock = new MasterPasswordUnlockDataRequestModel
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterKeyWrappedUserKey = masterKeyWrappedUserKey,
|
||||
Salt = email
|
||||
},
|
||||
// Intentionally set the legacy field to a different value to trigger the throw
|
||||
MasterPasswordHash = differentRootHash,
|
||||
UserAsymmetricKeys = new KeysRequestModel
|
||||
{
|
||||
PublicKey = publicKey,
|
||||
EncryptedPrivateKey = encryptedPrivateKey
|
||||
}
|
||||
};
|
||||
|
||||
// Provide a minimal valid token type to satisfy model-level token validation
|
||||
model.EmailVerificationToken = "test-token";
|
||||
|
||||
var ctx = new ValidationContext(model);
|
||||
|
||||
// Act
|
||||
var results = model.Validate(ctx).ToList();
|
||||
|
||||
// Assert: validation result exists with expected message and member names
|
||||
var mismatchResult = Assert.Single(results.Where(r =>
|
||||
r.ErrorMessage ==
|
||||
"MasterPasswordAuthenticationHash and root level MasterPasswordHash provided and are not equal. Only provide one."));
|
||||
Assert.Contains("MasterPasswordAuthenticationHash", mismatchResult.MemberNames);
|
||||
Assert.Contains("MasterPasswordHash", mismatchResult.MemberNames);
|
||||
}
|
||||
|
||||
private void SetDefaultKdfHmacKey(byte[]? newKey)
|
||||
{
|
||||
var fieldInfo = typeof(AccountsController).GetField("_defaultKdfHmacKey", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
|
||||
Reference in New Issue
Block a user