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; } } } }