さて、良いというのは主観的なものですが、タイトルにぴったりです。私が本当に知りたいのは、このパスワード管理/認証戦略に明らかな欠陥があるかどうかです. パフォーマンスも気になります。認証に PBKDF2 を使用しようとしていますが、それが RESTful Web サービスに適しているかどうかはよくわかりません。この質問のいくつかの部分があちこちで答えられているのを見つけることができますが、上から下までの包括的な答えを見つけたことはありません。これが私の戦略のすべてです。
バックグラウンド:
- Service は C# の ASP.NET Web Api プロジェクトです
- バックエンドは MS SQL サーバー
- 資格情報は、基本認証を使用して HTTPS 経由で送信されます。つまり、「承認: 基本ユーザー名:パスワード」です。
- API は Andriod アプリ、iOS アプリ、Web サイトで使用されます
まず、バックエンド:
create table [dbo].[User] (
[Name] varchar(50) collate SQL_Latin1_General_CP1_CI_AI not NULL primary key clustered,
[Password] varchar(28) collate SQL_Latin1_General_CP1_CS_AS not NULL,
[Salt] varchar(28) not NULL)
ユーザーを作成するためのストアド プロシージャ:
create procedure [dbo].[User_Create]
@Name varchar(50),
@Salt varchar(50),
@Password varchar(50)
as
insert into [dbo].[User]([Name], [Salt], [Password])
values(@Name, @Salt, @Password)
ユーザーを取得するためのストアド プロシージャ:
create procedure [dbo].[User_Get]
@Name varchar(50)
as
select *
from [dbo].[User]
where [Name] = @Name
バックエンドでは、名前、パスワード、およびソルトに選択されたデータ型が適切かどうかに興味があります。
これは、新しいユーザーを作成してバックエンドに永続化するコードです。これはおそらく、セキュリティとパフォーマンスの両方の観点から、私が最も懸念していることです。
public void CreateUser(string username, string password)
{
int hashLength = 20;
int saltLength = 20;
int hashIterations = 1000;
using(SqlConnection connection = new SqlConnection(this._ConnectionString))
using (SqlCommand command = new SqlCommand("[dbo].[User_Create]"))
{
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, saltLength, hashIterations);
string salt = Convert.ToBase64String(pbkdf2.Salt);
string hashPassword = Convert.ToBase64String(pbkdf2.GetBytes(hashLength));
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@Name", username);
command.Parameters.AddWithValue("@Salt", salt);
command.Parameters.AddWithValue("@Password", hashPassword);
connection.Open();
command.ExecuteNonQuery();
}
}
そして、ここにユーザーを認証するためのコードがあります。これは基本的に、ストアド プロシージャValidatePassword
を実行し、PBKDF2 を実行するための呼び出しを行い、User
プリンシパルを設定する Web API メッセージ ハンドラーにオブジェクトを返します (ロールはまだ実装されていません)。
public User AuthenticateUser(string username, string password)
{
using (SqlConnection connection = new SqlConnection(this._ConnectionString))
using (SqlCommand command = new SqlCommand("[dbo].[User_Get]"))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("@Name", username);
command.Connection = connection;
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
if (reader.Read() && this.ValidatePassword(password, reader["Salt"].ToString(), reader["Password"].ToString()))
{
return new User()
{
Name = username,
CustomerId = reader["CustomerId"].ToString()
};
}
else
{
return null;
}
}
}
}
ValidatePassword
上記のコードがパスワードを検証するために依存している は次のとおりです (明らかでない場合に備えて) 。また、これが正しいことを確認したいと思います。
private bool ValidatePassword(string password, string salt, string hashedPassword)
{
int hashLength = 20;
int hashIterations = 1000;
byte[] saltBytes = Convert.FromBase64String(salt);
Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, hashIterations);
byte[] hashBytes = pbkdf2.GetBytes(hashLength);
string hash = Convert.ToBase64String(hashBytes);
// Security Decisions For String Comparisons
//
// If you are making a security decision (such as whether to allow access to a system resource) based on the
// result of a string comparison or a case change, you should not use the invariant culture. Instead, you
// should perform a case-sensitive or case-insensitive ordinal comparison by calling a method that includes
// a StringComparison parameter and supplying either StringComparison.Ordinal or
// StringComparison.OrdinalIgnoreCase as an argument. Code that performs culture-sensitive string operations
// can cause security vulnerabilities if the current culture is changed or if the culture on the computer
// that is running the code differs from the culture that is used to test the code. In contrast, an ordinal
// comparison depends solely on the binary value of the compared characters.
//
// Source: http://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.invariantculture.aspx
return hash.Equals(hashedPassword, StringComparison.Ordinal);
}