using Microsoft.Data.Sqlite; using System.Security.Cryptography; using System.Text; namespace SimPas2_Windows.Managers { public class PasswordManager { private readonly string mConnectionString; private readonly byte[] mEncryptionKey; private readonly string mCulture; private string mJpLang; public PasswordManager(string databasePath, byte[] encryptionKey, string culture) { mConnectionString = $"Data Source={databasePath}"; mEncryptionKey = encryptionKey ?? throw new ArgumentNullException(nameof(encryptionKey)); mCulture = culture; mJpLang = "ja-JP"; } private bool AlreadyExists(string website, string username, int? excludeId = null) { using (SqliteConnection conn = new SqliteConnection(mConnectionString)) { conn.Open(); SqliteCommand com = conn.CreateCommand(); com.CommandText = @" SELECT COUNT(*) FROM Passwords WHERE UPPER(Website) = UPPER(@website) AND UPPER(Username) = UPPER(@username)"; com.Parameters.AddWithValue("@website", website); com.Parameters.AddWithValue("@username", username); if (excludeId.HasValue) { com.CommandText += " AND Id != @excludeId"; com.Parameters.AddWithValue("@excludeId", excludeId.Value); } return Convert.ToInt32(com.ExecuteScalar()) > 0; } } public void AddPassword(string website, string username, string password, string note) { if (string.IsNullOrWhiteSpace(website) || string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { string err = mCulture == mJpLang ? "ウェブサイト、ユーザー名及び、パスワードを御入力下さい。" : "Please fill in the website, user/email, and password."; throw new ArgumentException(err); } if (AlreadyExists(website, username)) { string err = mCulture == mJpLang ? $"ウェブサイトとユーザー名「{website}/{username}」付きパスワードは既に存在します。" : $"A password with the website and user/email of '{website}/{username}' already exists."; throw new ArgumentException(err); } string encryptedPassword = EncryptPassword(password); using (SqliteConnection conn = new SqliteConnection(mConnectionString)) { conn.Open(); SqliteCommand com = conn.CreateCommand(); com.CommandText = @" INSERT INTO Passwords (Website, Username, Password, Note) VALUES ($website, $username, $password, $note)"; com.Parameters.AddWithValue("$website", website); com.Parameters.AddWithValue("$username", username); com.Parameters.AddWithValue("$password", encryptedPassword); com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note); com.ExecuteNonQuery(); } } public bool EditPassword(int id, string website, string username, string password, string note) { if (string.IsNullOrWhiteSpace(website) || string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { string err = mCulture == "ja-JP" ? "ウェブサイト、ユーザー名及び、パスワードを御入力下さい。" : "Please fill in the website, user/email, and password."; throw new ArgumentException(err); } if (AlreadyExists(website, username, id)) { string err = mCulture == mJpLang ? $"ウェブサイトとユーザー名「{website}/{username}」付きパスワードは既に存在します。" : $"A password with the website and user/email of '{website}/{username}' already exists."; throw new ArgumentException(err); } string encryptedPassword = EncryptPassword(password); using (SqliteConnection conn = new SqliteConnection(mConnectionString)) { conn.Open(); SqliteCommand com = conn.CreateCommand(); com.CommandText = @" UPDATE Passwords SET Website = $website, Username = $username, Password = $password, Note = $note WHERE Id = $id"; com.Parameters.AddWithValue("id", id); com.Parameters.AddWithValue("$website", website); com.Parameters.AddWithValue("$username", username); com.Parameters.AddWithValue("$password", encryptedPassword); com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note); return com.ExecuteNonQuery() > 0; } } public bool DeletePassword(int id) { using (SqliteConnection conn = new SqliteConnection(mConnectionString)) { conn.Open(); SqliteCommand com = conn.CreateCommand(); com.CommandText = "DELETE FROM Passwords WHERE Id = $id"; com.Parameters.AddWithValue("$id", id); return com.ExecuteNonQuery() > 0; } } public List<(int Id, string Website, string Username, string Password, string Note)> GetAll(string keyword = "") { var passwords = new List<(int, string, string, string, string)>(); using (SqliteConnection conn = new SqliteConnection(mConnectionString)) { conn.Open(); SqliteCommand com = conn.CreateCommand(); if (string.IsNullOrWhiteSpace(keyword)) { com.CommandText = @" SELECT Id, Website, Username, Password, Note FROM Passwords ORDER BY Website ASC"; } else { com.CommandText = @" SELECT Id, Website, Username, Password, Note FROM Passwords WHERE Website LIKE @keyword OR Username LIKE @keyword ORDER BY Website ASC"; com.Parameters.AddWithValue("@keyword", $"%{keyword}%"); } using (SqliteDataReader reader = com.ExecuteReader()) { while (reader.Read()) { string encryptedPassword = reader.GetString(3); string decryptedPassword = DecryptPassword(encryptedPassword); passwords.Add(( reader.GetInt32(0), reader.GetString(1), reader.GetString(2), decryptedPassword, reader.IsDBNull(4) ? string.Empty : reader.GetString(4) )); } } } return passwords; } private string EncryptPassword(string plainPassword) { using (Aes aes = Aes.Create()) { aes.Key = mEncryptionKey; aes.GenerateIV(); byte[] iv = aes.IV; using (var encryptor = aes.CreateEncryptor(aes.Key, iv)) { byte[] plainBytes = Encoding.UTF8.GetBytes(plainPassword); byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); // Combine IV and encrypted data for storage byte[] result = new byte[iv.Length + encryptedBytes.Length]; Buffer.BlockCopy(iv, 0, result, 0, iv.Length); Buffer.BlockCopy(encryptedBytes, 0, result, iv.Length, encryptedBytes.Length); return Convert.ToBase64String(result); } } } private string DecryptPassword(string encryptedPassword) { byte[] combined = Convert.FromBase64String(encryptedPassword); byte[] iv = new byte[16]; // AES IV is 16 bytes 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 = mEncryptionKey; aes.IV = iv; using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) { byte[] decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length); return Encoding.UTF8.GetString(decryptedBytes); } } } } }