223 lines
7.3 KiB
C#
223 lines
7.3 KiB
C#
using System.Security.Cryptography;
|
|
using Microsoft.Data.Sqlite;
|
|
using Isopoh.Cryptography.Argon2;
|
|
|
|
namespace SimPas2_Windows
|
|
{
|
|
internal class MasterPasswordForm : Form
|
|
{
|
|
private readonly string mDatabasePath;
|
|
public byte[] EncryptionKey { get; private set; }
|
|
public bool IsValid { get; private set; }
|
|
|
|
public MasterPasswordForm(string databasePath)
|
|
{
|
|
mDatabasePath = databasePath;
|
|
InitializeComponent();
|
|
this.Icon = new Icon("simpas.ico");
|
|
}
|
|
|
|
private void InitializeComponent()
|
|
{
|
|
this.Text = "マスターパスワード";
|
|
this.FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
this.MaximizeBox = false;
|
|
this.MinimizeBox = false;
|
|
this.StartPosition = FormStartPosition.CenterScreen;
|
|
this.Width = 300;
|
|
this.Height = 150;
|
|
|
|
var layout = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
ColumnCount = 1,
|
|
RowCount = 2,
|
|
Padding = new Padding(10)
|
|
};
|
|
layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
|
layout.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
|
|
|
TextBox masPasswordTextbox = new TextBox { Dock = DockStyle.Fill, UseSystemPasswordChar = true };
|
|
Button masOkButton = new Button { Text = "OK", Dock = DockStyle.Right, Width = 80 };
|
|
Button masCancelButton = new Button { Text = "キャンセル", Dock = DockStyle.Right, Width = 80 };
|
|
|
|
masOkButton.Click += (s, e) =>
|
|
{
|
|
if (string.IsNullOrWhiteSpace(masPasswordTextbox.Text))
|
|
{
|
|
MessageBox.Show("マスターパスワードは必須です。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (VerifyOrSetMasterPassword(masPasswordTextbox.Text))
|
|
{
|
|
EncryptionKey = DeriveKey(masPasswordTextbox.Text);
|
|
IsValid = VerifyKey(EncryptionKey);
|
|
DialogResult = DialogResult.OK;
|
|
Close();
|
|
}
|
|
else
|
|
{
|
|
MessageBox.Show("不正なマスターパスワード。もう一度試してみて下さい。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"パスワードエラー:{ex.Message}", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
};
|
|
|
|
masCancelButton.Click += (s, e) => { DialogResult = DialogResult.Cancel; Close(); };
|
|
|
|
layout.Controls.Add(masPasswordTextbox, 1, 0);
|
|
|
|
FlowLayoutPanel buttonPanel = new FlowLayoutPanel
|
|
{
|
|
Dock = DockStyle.Fill,
|
|
FlowDirection = FlowDirection.RightToLeft
|
|
};
|
|
buttonPanel.Controls.Add(masCancelButton);
|
|
buttonPanel.Controls.Add(masOkButton);
|
|
|
|
layout.Controls.Add(buttonPanel, 0, 1);
|
|
layout.SetColumnSpan(buttonPanel, 2);
|
|
|
|
this.Controls.Add(layout);
|
|
this.AcceptButton = masOkButton;
|
|
this.CancelButton = masCancelButton;
|
|
}
|
|
|
|
private bool VerifyOrSetMasterPassword(string password)
|
|
{
|
|
using (SqliteConnection conn = new SqliteConnection($"Data Source={mDatabasePath}"))
|
|
{
|
|
conn.Open();
|
|
|
|
SqliteCommand createTable = conn.CreateCommand();
|
|
createTable.CommandText = @"
|
|
CREATE TABLE IF NOT EXISTS Config (
|
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
Key VARCHAR(255) NOT NULL,
|
|
Value TEXT
|
|
)";
|
|
createTable.ExecuteNonQuery();
|
|
|
|
SqliteCommand select = conn.CreateCommand();
|
|
select.CommandText = "SELECT Value FROM Config WHERE Key = 'MasterPasswordHash'";
|
|
using (SqliteDataReader reader = select.ExecuteReader())
|
|
{
|
|
if (reader.Read())
|
|
{
|
|
string storedHash = reader.GetString(0);
|
|
return Argon2.Verify(storedHash, password);
|
|
}
|
|
}
|
|
|
|
string passwordHash = Argon2.Hash(password);
|
|
SqliteCommand insert = conn.CreateCommand();
|
|
insert.CommandText = "INSERT INTO Config (Key, Value) VALUES ('MasterPasswordHash', $value)";
|
|
insert.Parameters.AddWithValue("$value", passwordHash);
|
|
insert.ExecuteNonQuery();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private byte[] DeriveKey(string password)
|
|
{
|
|
byte[] salt = GetOrCreateSalt();
|
|
using (Rfc2898DeriveBytes derive = new Rfc2898DeriveBytes(password, salt, 100000, HashAlgorithmName.SHA256))
|
|
{
|
|
return derive.GetBytes(32); // AES-256向け32バイト
|
|
}
|
|
}
|
|
|
|
private byte[] GetOrCreateSalt()
|
|
{
|
|
using (SqliteConnection conn = new SqliteConnection($"Data Source={mDatabasePath}"))
|
|
{
|
|
conn.Open();
|
|
|
|
SqliteCommand createTable = conn.CreateCommand();
|
|
createTable.CommandText = @"
|
|
CREATE TABLE IF NOT EXISTS Config (
|
|
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
Key VARCHAR(255) NOT NULL,
|
|
Value TEXT
|
|
)";
|
|
createTable.ExecuteNonQuery();
|
|
|
|
SqliteCommand select = conn.CreateCommand();
|
|
select.CommandText = "SELECT Value FROM Config WHERE Key = 'Salt'";
|
|
using (SqliteDataReader reader = select.ExecuteReader())
|
|
{
|
|
if (reader.Read())
|
|
{
|
|
byte[] salt = new byte[16];
|
|
reader.GetBytes(0, 0, salt, 0, 16);
|
|
return salt;
|
|
}
|
|
}
|
|
|
|
byte[] newSalt = RandomNumberGenerator.GetBytes(16);
|
|
SqliteCommand insert = conn.CreateCommand();
|
|
insert.CommandText = "INSERT INTO Config (Key, Value) VALUES ('Salt', $value)";
|
|
insert.Parameters.AddWithValue("$value", newSalt);
|
|
insert.ExecuteNonQuery();
|
|
|
|
return newSalt;
|
|
}
|
|
}
|
|
|
|
private bool VerifyKey(byte[] key)
|
|
{
|
|
using (SqliteConnection conn = new SqliteConnection($"Data Source={mDatabasePath}"))
|
|
{
|
|
conn.Open();
|
|
SqliteCommand com = conn.CreateCommand();
|
|
com.CommandText = "SELECT COUNT(*) FROM Passwords";
|
|
long count = (long)com.ExecuteScalar();
|
|
if (count == 0) return true;
|
|
|
|
com.CommandText = "SELECT Password FROM Passwords LIMIT 1";
|
|
using (SqliteDataReader reader = com.ExecuteReader())
|
|
{
|
|
if (reader.Read())
|
|
{
|
|
try
|
|
{
|
|
string encryptedPassword = reader.GetString(0);
|
|
byte[] combined = Convert.FromBase64String(encryptedPassword);
|
|
byte[] iv = new byte[16];
|
|
byte[] encryptedBytes = new byte[combined.Length - iv.Length];
|
|
|
|
Buffer.BlockCopy(combined, 0, iv, 0, iv.Length);
|
|
Buffer.BlockCopy(combined, iv.Length, encryptedBytes, 0, encryptedBytes.Length);
|
|
|
|
using (Aes aes = Aes.Create())
|
|
{
|
|
aes.Key = key;
|
|
aes.IV = iv;
|
|
using (ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
|
|
{
|
|
decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|