This commit is contained in:
2026-01-21 03:07:35 +09:00
commit 5c7516f60f
19 changed files with 15472 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.svn

1472
Form1.Designer.cs generated Normal file

File diff suppressed because it is too large Load Diff

2040
Form1.cs Normal file

File diff suppressed because it is too large Load Diff

431
Form1.en.resx Normal file
View File

@@ -0,0 +1,431 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="pwCopyLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>39, 23</value>
</data>
<data name="pwDoneButton.Location" type="System.Drawing.Point, System.Drawing">
<value>555, 291</value>
</data>
<data name="pwDoneButton.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="pwShowButton.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 291</value>
</data>
<data name="pwShowButton.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="pwNotesLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>354, 126</value>
</data>
<data name="pwCopyPasswordButton.Location" type="System.Drawing.Point, System.Drawing">
<value>555, 343</value>
</data>
<data name="pwCopyPasswordButton.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="pwCopyUsernameButton.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 343</value>
</data>
<data name="pwCopyUsernameButton.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="pinSearchTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>55, 6</value>
</data>
<data name="pinSearchTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>629, 23</value>
</data>
<data name="pinSearchLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>6, 9</value>
</data>
<data name="pinShowButton.Location" type="System.Drawing.Point, System.Drawing">
<value>515, 270</value>
</data>
<data name="pinNoteTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 94</value>
</data>
<data name="pinNoteTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 141</value>
</data>
<data name="pinNoteLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>354, 97</value>
</data>
<data name="pinCodeLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>354, 68</value>
</data>
<data name="pinWebsiteTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 36</value>
</data>
<data name="pinWebsiteTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="pinWebsiteLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>354, 39</value>
</data>
<data name="pinDoneButton.Location" type="System.Drawing.Point, System.Drawing">
<value>596, 270</value>
</data>
<data name="pinCopyButton.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 270</value>
</data>
<data name="pinCodeTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 65</value>
</data>
<data name="pinCodeTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="pinEditButton.Location" type="System.Drawing.Point, System.Drawing">
<value>515, 241</value>
</data>
<data name="pinDeleteButton.Location" type="System.Drawing.Point, System.Drawing">
<value>596, 241</value>
</data>
<data name="pinAddButton.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 241</value>
</data>
<data name="pinListbox.Location" type="System.Drawing.Point, System.Drawing">
<value>6, 35</value>
</data>
<data name="pinListbox.Size" type="System.Drawing.Size, System.Drawing">
<value>342, 604</value>
</data>
<data name="qaNoteTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 119</value>
</data>
<data name="qaNoteTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 133</value>
</data>
<data name="qaNoteLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>354, 122</value>
</data>
<data name="qaWebsiteTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 36</value>
</data>
<data name="qaWebsiteTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="qaWebsiteLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>354, 39</value>
</data>
<data name="qaDoneButton.Location" type="System.Drawing.Point, System.Drawing">
<value>596, 287</value>
</data>
<data name="qaShowButton.Location" type="System.Drawing.Point, System.Drawing">
<value>515, 287</value>
</data>
<data name="qaCopyButton.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 287</value>
</data>
<data name="qaEditButton.Location" type="System.Drawing.Point, System.Drawing">
<value>596, 258</value>
</data>
<data name="qaDeleteButton.Location" type="System.Drawing.Point, System.Drawing">
<value>515, 258</value>
</data>
<data name="qaAddButton.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 258</value>
</data>
<data name="qaAnswerTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 90</value>
</data>
<data name="qaAnswerTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="qaQuestionTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 61</value>
</data>
<data name="qaQuestionTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="qaSearchTextbox.Location" type="System.Drawing.Point, System.Drawing">
<value>55, 6</value>
</data>
<data name="qaSearchTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>629, 23</value>
</data>
<data name="qaListbox.Location" type="System.Drawing.Point, System.Drawing">
<value>6, 35</value>
</data>
<data name="qaListbox.Size" type="System.Drawing.Size, System.Drawing">
<value>342, 604</value>
</data>
<data name="qaAnswerLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>354, 93</value>
</data>
<data name="qaQuestionLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>354, 64</value>
</data>
<data name="qaSearchLabel.Location" type="System.Drawing.Point, System.Drawing">
<value>6, 9</value>
</data>
<data name="ccHolderLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 18</value>
</data>
<data name="ccHolderTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="ccDoneButton.Location" type="System.Drawing.Point, System.Drawing">
<value>555, 379</value>
</data>
<data name="ccDoneButton.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="ccShowButton.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 379</value>
</data>
<data name="ccShowButton.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="ccEditButton.Text" xml:space="preserve">
<value>Edit</value>
</data>
<assembly alias="mscorlib" name="mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="ccNotesTextbox.Multiline" type="System.Boolean, mscorlib">
<value>True</value>
</data>
<data name="ccNotesTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 164</value>
</data>
<data name="ccNotesLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 18</value>
</data>
<data name="ccCvcLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 18</value>
</data>
<data name="ccExpirationLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 18</value>
</data>
<data name="ccNumberLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 18</value>
</data>
<data name="ccCvcTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="ccExpirationTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="ccNumberTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="ccBrandLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>52, 15</value>
</data>
<data name="ccBrandCombobox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="ccListbox.ItemHeight" type="System.Int32, mscorlib">
<value>15</value>
</data>
<data name="ccListbox.Size" type="System.Drawing.Size, System.Drawing">
<value>342, 604</value>
</data>
<data name="ccSearchLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>45, 15</value>
</data>
<data name="ccSearchTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>629, 23</value>
</data>
<data name="cryptoPasswordLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoPasswordTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="cryptoHeightLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoHeightTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="cryptoSpendkeyLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoSpendkeyTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="cryptoAddressLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoAddressTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="cryptoDoneButton.Location" type="System.Drawing.Point, System.Drawing">
<value>555, 465</value>
</data>
<data name="cryptoDoneButton.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="cryptoShowButton.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 465</value>
</data>
<data name="cryptoShowButton.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="cryptoNotesTextbox.Multiline" type="System.Boolean, mscorlib">
<value>True</value>
</data>
<data name="cryptoNotesTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 164</value>
</data>
<data name="cryptoNotesLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoViewkeyLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoSeedLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoNameLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoViewkeyTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="cryptoSeedTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="cryptoNameTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="cryptoCurrencyLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>74, 23</value>
</data>
<data name="cryptoCurrencyCombobox.Location" type="System.Drawing.Point, System.Drawing">
<value>434, 36</value>
</data>
<data name="cryptoCurrencyCombobox.Size" type="System.Drawing.Size, System.Drawing">
<value>250, 23</value>
</data>
<data name="cryptoListbox.ItemHeight" type="System.Int32, mscorlib">
<value>15</value>
</data>
<data name="cryptoListbox.Size" type="System.Drawing.Size, System.Drawing">
<value>342, 604</value>
</data>
<data name="cryptoSearchLabel.Size" type="System.Drawing.Size, System.Drawing">
<value>45, 15</value>
</data>
<data name="cryptoSearchTextbox.Size" type="System.Drawing.Size, System.Drawing">
<value>629, 23</value>
</data>
</root>

5246
Form1.ja-JP.resx Normal file

File diff suppressed because it is too large Load Diff

4126
Form1.resx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,212 @@
using Microsoft.Data.Sqlite;
using System.Security.Cryptography;
using System.Text;
namespace SimPas2_Windows.Managers
{
internal class CreditcardManager
{
private readonly string mConnectionString;
private readonly byte[] mEncryptionKey;
private readonly string mCulture;
private string mJpLang;
public CreditcardManager(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 cardnumber, int? excludeId = null)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
SELECT COUNT(*) FROM Cc
WHERE UPPER(Cardnumber) = UPPER(@cardnumber)";
com.Parameters.AddWithValue("@cardnumber", cardnumber);
if (excludeId.HasValue)
{
com.CommandText += " AND Id != @excludeId";
com.Parameters.AddWithValue("@excludeId", excludeId.Value);
}
return Convert.ToInt32(com.ExecuteScalar()) > 0;
}
}
public void AddCc(string brand, string cardnumber, string expiration, string cvc, string holdername, string note)
{
if (string.IsNullOrWhiteSpace(brand) || string.IsNullOrWhiteSpace(cardnumber) || string.IsNullOrWhiteSpace(expiration) || string.IsNullOrWhiteSpace(cvc) || string.IsNullOrWhiteSpace(holdername))
{
string err = mCulture == mJpLang
? "ブランド、カード番号、氏名、有効期限及び、CVCを御入力下さい。"
: "Please fill in the brand, card number, full name, expiration, and CVC.";
throw new ArgumentException(err);
}
string encryptedCvc = EncryptCvc(cvc);
if (AlreadyExists(cardnumber))
{
string err = mCulture == mJpLang
? $"カード「{brand}/{holdername} ({expiration})」は既に存在します。"
: $"The card '{brand}/{holdername} ({expiration})' already exists.";
throw new ArgumentException(err);
}
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
INSERT INTO Cc (Brand, Cardnumber, Expiration, Cvc, Holdername, Note)
VALUES ($brand, $cardnumber, $expiration, $cvc, $holdername, $note)";
com.Parameters.AddWithValue("$brand", brand);
com.Parameters.AddWithValue("$cardnumber", cardnumber);
com.Parameters.AddWithValue("$expiration", expiration);
com.Parameters.AddWithValue("$cvc", encryptedCvc);
com.Parameters.AddWithValue("$holdername", holdername);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
com.ExecuteNonQuery();
}
}
public bool EditCc(int id, string brand, string cardnumber, string expiration, string cvc, string holdername, string note)
{
if (string.IsNullOrWhiteSpace(brand) || string.IsNullOrWhiteSpace(cardnumber) || string.IsNullOrWhiteSpace(expiration) || string.IsNullOrWhiteSpace(cvc) || string.IsNullOrWhiteSpace(holdername))
{
string err = mCulture == mJpLang
? "ブランド、カード番号、氏名、有効期限及び、CVCを御入力下さい。"
: "Please fill in the brand, card number, full name, expiration, and CVC.";
throw new ArgumentException(err);
}
string encryptedCvc = EncryptCvc(cvc);
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
UPDATE Cc
SET Brand = $brand, Cardnumber = $cardnumber, Expiration = $expiration, Cvc = $cvc, Holdername = $holdername, Note = $note
WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
com.Parameters.AddWithValue("$brand", brand);
com.Parameters.AddWithValue("$cardnumber", cardnumber);
com.Parameters.AddWithValue("$expiration", expiration);
com.Parameters.AddWithValue("$cvc", encryptedCvc);
com.Parameters.AddWithValue("$holdername", holdername);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
return com.ExecuteNonQuery() > 0;
}
}
public bool DeleteCc(int id)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = "DELETE FROM Cc WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
return com.ExecuteNonQuery() > 0;
}
}
public List<(int Id, string Brand, string Cardnumber, string Expiration, string Cvc, string Holdername, string Note)> GetAll(string keyword = "")
{
var ccs = new List<(int, string, string, string, string, string, string)>();
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
if (string.IsNullOrWhiteSpace(keyword))
{
com.CommandText = @"
SELECT Id, Brand, Cardnumber, Expiration, Cvc, Holdername, Note FROM Cc
ORDER BY Brand ASC";
}
else
{
com.CommandText = @"
SELECT Id, Brand, Cardnumber, Expiration, Cvc, Holdername, Note FROM Cc
WHERE Brand LIKE @keyword OR Holdername LIKE @keyword OR Cardnumber LIKE @keyword
ORDER BY Brand ASC";
com.Parameters.AddWithValue("@keyword", $"%{keyword}%");
}
using (SqliteDataReader reader = com.ExecuteReader())
{
while (reader.Read())
{
string encryptedCvc = reader.GetString(4);
string decryptedCvc = DecryptCvc(encryptedCvc);
ccs.Add((
reader.GetInt32(0),
reader.GetString(1),
reader.GetString(2),
reader.GetString(3),
decryptedCvc,
reader.GetString(5),
reader.IsDBNull(6) ? string.Empty : reader.GetString(6)
));
}
}
}
return ccs;
}
private string EncryptCvc(string answer)
{
using (Aes aes = Aes.Create())
{
aes.Key = mEncryptionKey;
aes.GenerateIV();
byte[] iv = aes.IV;
using (ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, iv))
{
byte[] plainBytes = Encoding.UTF8.GetBytes(answer);
byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
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 DecryptCvc(string encryptedAnswer)
{
byte[] combined = Convert.FromBase64String(encryptedAnswer);
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 = mEncryptionKey;
aes.IV = iv;
using (ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
{
byte[] decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
}
}
}

231
Managers/CryptoManager.cs Normal file
View File

@@ -0,0 +1,231 @@
using Microsoft.Data.Sqlite;
using System.Security.Cryptography;
using System.Text;
namespace SimPas2_Windows.Managers
{
internal class CryptoManager
{
private readonly string mConnectionString;
private readonly byte[] mEncryptionKey;
private readonly string mCulture;
private string mJpLang;
public CryptoManager(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 currency, string name, int? excludeId = null)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
SELECT COUNT(*) FROM Crypto
WHERE UPPER(Currency) = UPPER(@currency) AND UPPER(Name) = UPPER(@name)";
com.Parameters.AddWithValue("@currency", currency);
com.Parameters.AddWithValue("@name", name);
if (excludeId.HasValue)
{
com.CommandText += " AND Id != @excludeId";
com.Parameters.AddWithValue("@excludeId", excludeId.Value);
}
return Convert.ToInt32(com.ExecuteScalar()) > 0;
}
}
public void AddCrypto(string currency, string name, string address, string seed, string viewkey, string spendkey, string height, string password, string note)
{
if (string.IsNullOrWhiteSpace(currency) || string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(address))
{
string err = mCulture == mJpLang
? "通貨、ウォレット名及び、住所を御入力下さい。"
: "Please fill in the currency, wallet name, and address.";
throw new ArgumentException(err);
}
string encryptedPassword = !string.IsNullOrWhiteSpace(password) ? EncryptInfo(password) : "";
string encryptedSeed = !string.IsNullOrWhiteSpace(seed) ? EncryptInfo(seed) : "";
string encryptedViewkey = !string.IsNullOrWhiteSpace(viewkey) ? EncryptInfo(viewkey) : "";
string encryptedSpendkey = !string.IsNullOrWhiteSpace(spendkey) ? EncryptInfo(spendkey) : "";
if (AlreadyExists(currency, name))
{
string err = mCulture == mJpLang
? $"通貨及びウォレット名「{currency}/{name}」エントリは既に存在します。"
: $"An entry with the currency and wallet name for '{currency}/{name}' already exists.";
throw new ArgumentException(err);
}
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
INSERT INTO Crypto (Currency, Name, Address, Seed, Viewkey, Spendkey, Height, Password, Note)
VALUES ($currency, $name, $address, $seed, $viewkey, $spendkey, $height, $password, $note)";
com.Parameters.AddWithValue("$currency", currency);
com.Parameters.AddWithValue("$name", name);
com.Parameters.AddWithValue("$address", address);
com.Parameters.AddWithValue("$seed", string.IsNullOrEmpty(seed) ? DBNull.Value : encryptedSeed);
com.Parameters.AddWithValue("$viewkey", string.IsNullOrEmpty(viewkey) ? DBNull.Value : encryptedViewkey);
com.Parameters.AddWithValue("$spendkey", string.IsNullOrEmpty(spendkey) ? DBNull.Value : encryptedSpendkey);
com.Parameters.AddWithValue("$height", string.IsNullOrEmpty(height) ? DBNull.Value : height);
com.Parameters.AddWithValue("$password", string.IsNullOrEmpty(password) ? DBNull.Value : encryptedPassword);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
com.ExecuteNonQuery();
}
}
public bool EditCrypto(int id, string currency, string name, string address, string seed, string viewkey, string spendkey, string height, string password, string note)
{
if (string.IsNullOrWhiteSpace(currency) || string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(address))
{
string err = mCulture == mJpLang
? "通貨、ウォレット名及び、住所を御入力下さい。"
: "Please fill in the currency, wallet name, and address.";
throw new ArgumentException(err);
}
string encryptedPassword = string.IsNullOrEmpty(password) ? string.Empty : EncryptInfo(password);
string encryptedSeed = string.IsNullOrEmpty(seed) ? string.Empty : EncryptInfo(seed);
string encryptedViewkey = string.IsNullOrEmpty(viewkey) ? string.Empty : EncryptInfo(viewkey);
string encryptedSpendkey = string.IsNullOrEmpty(spendkey) ? string.Empty : EncryptInfo(spendkey);
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
UPDATE Crypto
SET Currency = $currency, Name = $name, Address = $address, Seed = $seed, Viewkey = $viewkey, Spendkey = $spendkey, Height = $height, Password = $password, Note = $note
WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
com.Parameters.AddWithValue("$currency", currency);
com.Parameters.AddWithValue("$name", name);
com.Parameters.AddWithValue("$address", address);
com.Parameters.AddWithValue("$seed", string.IsNullOrEmpty(seed) ? DBNull.Value : encryptedSeed);
com.Parameters.AddWithValue("$viewkey", string.IsNullOrEmpty(viewkey) ? DBNull.Value : encryptedViewkey);
com.Parameters.AddWithValue("$spendkey", string.IsNullOrEmpty(spendkey) ? DBNull.Value : encryptedSpendkey);
com.Parameters.AddWithValue("$height", string.IsNullOrEmpty(height) ? DBNull.Value : height);
com.Parameters.AddWithValue("$password", string.IsNullOrEmpty(password) ? DBNull.Value : encryptedPassword);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
return com.ExecuteNonQuery() > 0;
}
}
public bool DeleteCrypto(int id)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = "DELETE FROM Crypto WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
return com.ExecuteNonQuery() > 0;
}
}
public List<(int Id, string Currency, string Name, string Address, string Seed, string Viewkey, string Spendkey, string Height, string Password, string Note)> GetAll(string keyword = "")
{
var cryptos = new List<(int, string, string, string, string, string, string, string, string, string)>();
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
if (string.IsNullOrWhiteSpace(keyword))
{
com.CommandText = @"
SELECT Id, Currency, Name, Address, Seed, Viewkey, Spendkey, Height, Password, Note FROM Crypto
ORDER BY Currency DESC";
}
else
{
com.CommandText = @"
SELECT Id, Currency, Name, Address, Seed, Viewkey, Spendkey, Height, Password, Note FROM Crypto
WHERE Currency LIKE @keyword OR Name LIKE @keyword
ORDER BY Currency DESC";
com.Parameters.AddWithValue("@keyword", $"%{keyword}%");
}
using (SqliteDataReader reader = com.ExecuteReader())
{
while (reader.Read())
{
string decryptedSeed = DecryptInfo(reader.GetString(4));
string decryptedViewkey = reader.IsDBNull(5) ? string.Empty : DecryptInfo(reader.GetString(5));
string decryptedSpendkey = reader.IsDBNull(6) ? string.Empty : DecryptInfo(reader.GetString(6));
string decryptedPassword = reader.IsDBNull(8) ? string.Empty : DecryptInfo(reader.GetString(8));
cryptos.Add((
reader.GetInt32(0),
reader.GetString(1),
reader.GetString(2),
reader.GetString(3),
decryptedSeed,
reader.IsDBNull(5) ? string.Empty : decryptedViewkey,
reader.IsDBNull(6) ? string.Empty : decryptedSpendkey,
reader.IsDBNull(7) ? string.Empty : reader.GetString(7),
reader.IsDBNull(8) ? string.Empty : decryptedPassword,
reader.IsDBNull(9) ? string.Empty : reader.GetString(9)
));
}
}
}
return cryptos;
}
private string EncryptInfo(string answer)
{
using (Aes aes = Aes.Create())
{
aes.Key = mEncryptionKey;
aes.GenerateIV();
byte[] iv = aes.IV;
using (ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, iv))
{
byte[] plainBytes = Encoding.UTF8.GetBytes(answer);
byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
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 DecryptInfo(string encryptedAnswer)
{
byte[] combined = Convert.FromBase64String(encryptedAnswer);
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 = mEncryptionKey;
aes.IV = iv;
using (ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
{
byte[] decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Security.Cryptography;
namespace SimPas2_Windows.Managers
{
public class GeneratorManager
{
public GeneratorManager()
{
}
public string GeneratePassword(bool isSecure, int length)
{
const string charset_risky = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const string charset_secure = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#$%&'()=~-^\\|_@`[{]};:+*<>,./?";
string charset = isSecure ? charset_secure : charset_risky;
byte[] random = RandomNumberGenerator.GetBytes(length);
char[] res = new char[length];
for (int i = 0; i < length; i++)
{
res[i] = charset[random[i] % charset.Length];
}
return new string(res);
}
}
}

172
Managers/NoteManager.cs Normal file
View File

@@ -0,0 +1,172 @@
using Microsoft.Data.Sqlite;
namespace SimPas2_Windows.Managers
{
public class NoteManager
{
private readonly string mConnectionString;
private readonly string mCulture;
private string mJpLang;
public NoteManager(string databasePath, string culture)
{
mConnectionString = $"Data Source={databasePath}";
mCulture = culture;
mJpLang = "ja-JP";
}
private bool AlreadyExists(string name, int? excludeId = null)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
SELECT COUNT(*) FROM Notes
WHERE UPPER(Name) = UPPER(@name)";
com.Parameters.AddWithValue("@name", name);
if (excludeId.HasValue)
{
com.CommandText += " AND Id != @excludeId";
com.Parameters.AddWithValue("@excludeId", excludeId.Value);
}
return Convert.ToInt32(com.ExecuteScalar()) > 0;
}
}
public void AddNote(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
string err = mCulture == mJpLang
? "ファイル名を御入力下さい。"
: "Please fill in the filename.";
throw new ArgumentException(err);
}
if (AlreadyExists(name))
{
string err = mCulture == mJpLang
? $"ファイル名「{name}」付きメモは既に存在します。"
: $"A note with the filename '{name}' already exists.";
throw new ArgumentException(err);
}
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
INSERT INTO Notes (Name)
VALUES ($name)";
com.Parameters.AddWithValue("$name", name);
com.ExecuteNonQuery();
}
}
public bool EditNote(int id, string name)
{
if (string.IsNullOrWhiteSpace(name))
{
string err = mCulture == mJpLang
? "ファイル名を御入力下さい。" :
"Please fill in the filename.";
throw new ArgumentException(err);
}
if (AlreadyExists(name, id))
{
string err = mCulture == mJpLang
? $"ファイル名「{name}」付きメモは既に存在します。"
: $"A note with the filename '{name}' already exists.";
throw new ArgumentException(err);
}
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
UPDATE Notes
SET Name = $name
WHERE Id = $id";
com.Parameters.AddWithValue("id", id);
com.Parameters.AddWithValue("$name", name);
return com.ExecuteNonQuery() > 0;
}
}
public bool SaveNote(int id, string text)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
UPDATE Notes
SET Text = @text
WHERE Id = @id";
com.Parameters.AddWithValue("@id", id);
com.Parameters.AddWithValue("@text", text ?? (object)DBNull.Value);
return com.ExecuteNonQuery() > 0;
}
}
public bool DeleteNote(int id)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = "DELETE FROM Notes WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
return com.ExecuteNonQuery() > 0;
}
}
public List<(int Id, string Name, string Text)> GetAll(string keyword = "")
{
var notes = new List<(int, string, string)>();
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
if (string.IsNullOrWhiteSpace(keyword))
{
com.CommandText = @"
SELECT Id, Name, Text FROM Notes
ORDER BY Name ASC";
}
else
{
com.CommandText = @"
SELECT Id, Name, Text FROM Notes
WHERE Name LIKE @keyword
ORDER BY Name ASC";
com.Parameters.AddWithValue("@keyword", $"%{keyword}%");
}
using (SqliteDataReader reader = com.ExecuteReader())
{
while (reader.Read())
{
notes.Add((
reader.GetInt32(0),
reader.GetString(1),
reader.IsDBNull(2) ? string.Empty : reader.GetString(2)
));
}
}
}
return notes;
}
}
}

479
Managers/OtpManager.cs Normal file
View File

@@ -0,0 +1,479 @@
using Microsoft.Data.Sqlite;
using System.Security.Cryptography;
using System.Text;
namespace SimPas2_Windows.Managers
{
public class OtpManager
{
private readonly string mConnectionString;
private readonly byte[] mEncryptionKey;
private readonly string mCulture;
private string mJpLang;
public OtpManager(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 secret, int? excludeId = null)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
SELECT COUNT(*) FROM Otp
WHERE UPPER(Website) = UPPER(@website) AND UPPER(Secret) = UPPER(@secret)";
com.Parameters.AddWithValue("@website", website);
com.Parameters.AddWithValue("@secret", secret);
if (excludeId.HasValue)
{
com.CommandText += " AND Id != @excludeId";
com.Parameters.AddWithValue("@excludeId", excludeId.Value);
}
return Convert.ToInt32(com.ExecuteScalar()) > 0;
}
}
public void AddOtp(string website, string secret, string issuer, string algorithm, int duration, int digits, string note)
{
if (string.IsNullOrWhiteSpace(website) || string.IsNullOrWhiteSpace(secret) || string.IsNullOrWhiteSpace(issuer))
{
string err = mCulture == mJpLang
? "ウェブサイト、秘密鍵及び、発行者を御入力下さい。"
: "Please fill in the website, secret, and issuer.";
throw new ArgumentException(err);
}
string encryptedSecret = EncryptSecret(secret);
if (AlreadyExists(website, encryptedSecret))
{
string err = mCulture == mJpLang
? $"ウェブサイト「{website}」向け秘密鍵は既に存在します。"
: $"An OTP with the website and secret for '{website}' already exists.";
throw new ArgumentException(err);
}
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
INSERT INTO Otp (Website, Secret, Issuer, Algorithm, Duration, Digits, Note)
VALUES ($website, $secret, $issuer, $algorithm, $duration, $digits, $note)";
com.Parameters.AddWithValue("$website", website);
com.Parameters.AddWithValue("$secret", encryptedSecret);
com.Parameters.AddWithValue("$issuer", issuer);
com.Parameters.AddWithValue("$algorithm", algorithm);
com.Parameters.AddWithValue("$duration", duration);
com.Parameters.AddWithValue("$digits", digits);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
com.ExecuteNonQuery();
}
}
public bool EditOtp(int id, string website, string secret, string issuer, string algorithm, int duration, int digits, string note)
{
if (string.IsNullOrWhiteSpace(website) || string.IsNullOrWhiteSpace(secret) || string.IsNullOrWhiteSpace(issuer))
{
string err = mCulture == mJpLang
? "ウェブサイト、秘密鍵及び、発行者を御入力下さい。"
: "Please fill in the website, secret, and issuer.";
throw new ArgumentException(err);
}
string encryptedSecret = EncryptSecret(secret);
if (AlreadyExists(website, encryptedSecret, id))
{
string err = mCulture == mJpLang
? $"ウェブサイト「{website}」向け秘密鍵は既に存在します。"
: $"An OTP with the website and secret for '{website}' already exists.";
throw new ArgumentException(err);
}
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
UPDATE Otp
SET Website = $website, Secret = $secret, Issuer = $issuer, Algorithm = $algorithm, Duration = $duration, Digits = $digits, Note = $note
WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
com.Parameters.AddWithValue("$website", website);
com.Parameters.AddWithValue("$secret", encryptedSecret);
com.Parameters.AddWithValue("$issuer", issuer);
com.Parameters.AddWithValue("$algorithm", algorithm);
com.Parameters.AddWithValue("$duration", duration);
com.Parameters.AddWithValue("$digits", digits);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
return com.ExecuteNonQuery() > 0;
}
}
public bool DeleteOtp(int id)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = "DELETE FROM Otp WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
return com.ExecuteNonQuery() > 0;
}
}
public List<(int Id, string Website, string Secret, string Issuer, string Algorithm, int Duration, int Digits, string Note)> GetAll(string keyword = "")
{
var otps = new List<(int, string, string, string, string, int, int, string)>();
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
if (string.IsNullOrWhiteSpace(keyword))
{
com.CommandText = @"
SELECT Id, Website, Secret, Issuer, Algorithm, Duration, Digits, Note FROM Otp
ORDER BY Website ASC";
}
else
{
com.CommandText = @"
SELECT Id, Website, Secret, Issuer, Algorithm, Duration, Digits, Note FROM Otp
WHERE Website LIKE @keyword OR Issuer LIKE @keyword
ORDER BY Website ASC";
com.Parameters.AddWithValue("@keyword", $"%{keyword}%");
}
using (SqliteDataReader reader = com.ExecuteReader())
{
while (reader.Read())
{
string encryptedSecret = reader.GetString(2);
string decryptedSecret = DecryptSecret(encryptedSecret);
otps.Add((
reader.GetInt32(0),
reader.GetString(1),
decryptedSecret,
reader.GetString(3),
reader.GetString(4),
reader.GetInt32(5),
reader.GetInt32(6),
reader.IsDBNull(7) ? string.Empty : reader.GetString(7)
));
}
}
}
return otps;
}
public (string Code, string Error) GenerateTotp(string secret, int digits, string algorithm, int duration = 30)
{
try
{
var (sec, error) = ExtractSecret(secret);
if (!string.IsNullOrEmpty(error))
{
return ("", error);
}
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
ulong counter = (ulong)(currentTime / duration);
uint totp = GenerateTotpCode(sec, counter, digits, algorithm);
return (totp.ToString($"D{digits}"), "");
}
catch (Exception ex)
{
return ("", $"TOTP generation error: {ex.Message}");
}
}
private (byte[]? Secret, string Error) ExtractSecret(string otpauthUrl)
{
if (string.IsNullOrEmpty(otpauthUrl))
{
string err = mCulture == mJpLang
? "OTP Authを御入力下さい。"
: "Please fill in the OTP Auth.";
throw new ArgumentException(err);
}
if (!otpauthUrl.StartsWith("otpauth://totp/", StringComparison.OrdinalIgnoreCase))
{
try
{
byte[] secretDecoded = Base32Decode(otpauthUrl);
return (secretDecoded, "");
}
catch (Exception ex)
{
string err = mCulture == mJpLang
? $"TOTP秘密鍵を復号化に失敗{ex.Message}"
: $"Failed to decode TOTP secret: {ex.Message}";
return (null, err);
}
}
int secretStart = otpauthUrl.IndexOf("secret=", StringComparison.OrdinalIgnoreCase);
if (secretStart == -1)
{
string err = mCulture == mJpLang
? "TOTP秘密鍵をURLの中に見つけられませんでした。"
: "TOTP secret not found in URL";
return (null, err);
}
secretStart += 7;
int secretEnd = otpauthUrl.IndexOf('&', secretStart);
if (secretEnd == -1)
{
secretEnd = otpauthUrl.Length;
}
if (secretEnd <= secretStart)
{
string err = mCulture == mJpLang
? "不正なTOTP秘密枠"
: "Invalid TOTP secret range";
return (null, err);
}
string secretEncoded = otpauthUrl.Substring(secretStart, secretEnd - secretStart);
try
{
byte[] secretDecoded = Base32Decode(secretEncoded);
return (secretDecoded, "");
}
catch (Exception ex)
{
string err = mCulture == mJpLang
? $"TOTP秘密鍵を復号化に失敗{ex.Message}"
: $"Failed to decode TOTP secret: {ex.Message}";
return (null, err);
}
}
public (string Secret, string Issuer, string Algorithm, int Duration, int Digits) ParseOtpAuthUrl(string otpAuthUrl)
{
if (string.IsNullOrEmpty(otpAuthUrl) || !otpAuthUrl.StartsWith("otpauth://totp/", StringComparison.OrdinalIgnoreCase))
{
string err = mCulture == mJpLang
? "不正なOTP Auth URL"
: "Invalid OTP Auth URL";
throw new ArgumentException(err);
}
string issuer = "";
string algorithm = "SHA1";
int duration = 30;
int digits = 6;
int labelStart = "otpauth://totp/".Length;
int labelEnd = otpAuthUrl.IndexOf('?', labelStart);
if (labelEnd != -1)
{
issuer = otpAuthUrl.Substring(labelStart, labelEnd - labelStart);
if (issuer.Contains(':'))
{
issuer = issuer.Substring(0, issuer.IndexOf(':'));
}
}
int secretStart = otpAuthUrl.IndexOf("secret=", StringComparison.OrdinalIgnoreCase);
if (secretStart == -1)
{
string err = mCulture == mJpLang
? "TOTP秘密鍵をURLの中に見つけられませんでした。"
: "TOTP secret not found in URL";
throw new ArgumentException(err);
}
secretStart += 7;
int secretEnd = otpAuthUrl.IndexOf('&', secretStart);
if (secretEnd == -1)
{
secretEnd = otpAuthUrl.Length;
}
string secret = otpAuthUrl.Substring(secretStart, secretEnd - secretStart);
var queryParams = otpAuthUrl.Substring(labelEnd + 1).Split('&').Select(p => p.Split('=')).ToDictionary(p => p[0].ToLower(), p => p.Length > 1 ? p[1] : "");
if (queryParams.ContainsKey("issuer"))
{
issuer = queryParams["issuer"];
}
if (queryParams.ContainsKey("algorithm"))
{
algorithm = queryParams["algorithm"].ToUpper();
if (algorithm != "SHA1" && algorithm != "SHA256" && algorithm != "SHA512")
{
algorithm = "SHA1";
}
}
if (queryParams.ContainsKey("period") && int.TryParse(queryParams["period"], out int parsedPeriod))
{
duration = parsedPeriod;
}
if (queryParams.ContainsKey("digits") && int.TryParse(queryParams["digits"], out int parsedDigits))
{
digits = parsedDigits;
}
return (secret, issuer, algorithm, duration, digits);
}
private uint GenerateTotpCode(byte[] secret, ulong counter, int digits, string algorithm)
{
byte[] counterBytes = BitConverter.GetBytes(counter);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(counterBytes);
}
using (HMAC hmac = algorithm.ToUpper() switch
{
"SHA256" => new HMACSHA256(secret),
"SHA512" => new HMACSHA512(secret),
_ => new HMACSHA1(secret),
})
{
byte[] hash = hmac.ComputeHash(counterBytes);
int offset = hash[hash.Length - 1] & 0x0F;
uint truncatedHash =
(uint)(hash[offset] & 0x7F) << 24 |
(uint)(hash[offset + 1] & 0xFF) << 16 |
(uint)(hash[offset + 2] & 0xFF) << 8 |
(uint)(hash[offset + 3] & 0xFF);
return truncatedHash % (uint)Math.Pow(10, digits);
}
}
private byte[] Base32Decode(string base32)
{
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
if (string.IsNullOrEmpty(base32))
{
string err = mCulture == mJpLang
? "Base32文字はnullや空です。"
: "Base32 string cannot be null or empty";
throw new ArgumentException(err);
}
// Remove spaces, hyphens, and convert to uppercase
base32 = base32.Trim().Replace(" ", "").Replace("-", "").ToUpper();
// Count padding characters
int padding = 0;
for (int i = base32.Length - 1; i >= 0 && base32[i] == '='; i--)
{
padding++;
}
// Remove padding for processing
string cleanBase32 = base32.Substring(0, base32.Length - padding);
if (cleanBase32.Any(c => !alphabet.Contains(c)))
{
string err = mCulture == mJpLang
? "Base32文字は不正文字が含みます。"
: "Base32 string contains invalid characters";
throw new ArgumentException(err);
}
// Calculate output length
int byteCount = cleanBase32.Length * 5 / 8;
if (byteCount == 0)
{
return Array.Empty<byte>();
}
byte[] buffer = new byte[byteCount];
int bufferIndex = 0;
int bits = 0;
int bitCount = 0;
foreach (char c in cleanBase32)
{
int value = alphabet.IndexOf(c);
if (value < 0)
{
string err = mCulture == mJpLang
? "Base32文字は不正文字が含みます。"
: "Base32 string contains invalid characters";
throw new ArgumentException(err);
}
bits = bits << 5 | value;
bitCount += 5;
if (bitCount >= 8)
{
buffer[bufferIndex++] = (byte)(bits >> bitCount - 8);
bitCount -= 8;
}
}
// Resize to actual bytes written
if (bufferIndex < byteCount)
{
Array.Resize(ref buffer, bufferIndex);
}
return buffer;
}
private string EncryptSecret(string secret)
{
using (Aes aes = Aes.Create())
{
aes.Key = mEncryptionKey;
aes.GenerateIV();
byte[] iv = aes.IV;
using (ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, iv))
{
byte[] plainBytes = Encoding.UTF8.GetBytes(secret);
byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
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 DecryptSecret(string encryptedSecret)
{
byte[] combined = Convert.FromBase64String(encryptedSecret);
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 = mEncryptionKey;
aes.IV = iv;
using (ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
{
byte[] decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
}
}
}

214
Managers/PasswordManager.cs Normal file
View File

@@ -0,0 +1,214 @@
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);
}
}
}
}
}

208
Managers/PinManager.cs Normal file
View File

@@ -0,0 +1,208 @@
using Microsoft.Data.Sqlite;
using System.Security.Cryptography;
using System.Text;
namespace SimPas2_Windows.Managers
{
public class PinManager
{
private readonly string mConnectionString;
private readonly byte[] mEncryptionKey;
private readonly string mCulture;
private string mJpLang;
public PinManager(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, int? excludeId = null)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
SELECT COUNT(*) FROM Pin
WHERE UPPER(Website) = UPPER(@website)";
com.Parameters.AddWithValue("@website", website);
if (excludeId.HasValue)
{
com.CommandText += " AND Id != @excludeId";
com.Parameters.AddWithValue("@excludeId", excludeId.Value);
}
return Convert.ToInt32(com.ExecuteScalar()) > 0;
}
}
public void AddPin(string website, string pincode, string note)
{
if (string.IsNullOrWhiteSpace(website) || string.IsNullOrWhiteSpace(pincode))
{
string err = mCulture == mJpLang
? "ウェブサイト及び暗証番号を御入力下さい。"
: "Please fill in the website and pincode.";
throw new ArgumentException(err);
}
if (AlreadyExists(website))
{
string err = mCulture == mJpLang
? $"ウェブサイト「{website}」付き暗証番号は既に存在します。"
: $"A pincode with the website '{website}' already exists.";
throw new ArgumentException(err);
}
string encryptedPin = EncryptPin(pincode);
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"INSERT INTO Pin (Website, Pincode, Note) VALUES ($website, $pincode, $note)";
com.Parameters.AddWithValue("$website", website);
com.Parameters.AddWithValue("$pincode", encryptedPin);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
com.ExecuteNonQuery();
}
}
public bool EditPin(int id, string website, string pincode, string note)
{
if (string.IsNullOrWhiteSpace(website) || string.IsNullOrWhiteSpace(pincode))
{
string err = mCulture == mJpLang
? "ウェブサイト及び暗証番号を御入力下さい。"
: "Please fill in the website and pincode.";
throw new ArgumentException(err);
}
if (AlreadyExists(website, id))
{
string err = mCulture == mJpLang
? $"ウェブサイト「{website}」付き暗証番号は既に存在します。"
: $"A pincode with the website '{website}' already exists.";
throw new ArgumentException(err);
}
string encryptedPin = EncryptPin(pincode);
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"UPDATE Pin SET Website = $website, Pincode = $pincode, Note = $note WHERE Id = $id";
com.Parameters.AddWithValue("id", id);
com.Parameters.AddWithValue("$website", website);
com.Parameters.AddWithValue("$pincode", encryptedPin);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
return com.ExecuteNonQuery() > 0;
}
}
public bool DeletePin(int id)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = "DELETE FROM Pin WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
return com.ExecuteNonQuery() > 0;
}
}
public List<(int Id, string Website, string Pincode, string Note)> GetAll(string keyword = "")
{
var pins = new List<(int, string, string, string)>();
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
if (string.IsNullOrWhiteSpace(keyword))
{
com.CommandText = @"
SELECT Id, Website, Pincode, Note FROM Pin
ORDER BY Website ASC";
}
else
{
com.CommandText = @"
SELECT Id, Website, Pincode, Note FROM Pin
WHERE Website LIKE @keyword
ORDER BY Website ASC";
com.Parameters.AddWithValue("@keyword", $"%{keyword}%");
}
using (SqliteDataReader reader = com.ExecuteReader())
{
while (reader.Read())
{
string encryptedPin = reader.GetString(2);
string decryptedPin = DecryptPin(encryptedPin);
pins.Add((
reader.GetInt32(0),
reader.GetString(1),
decryptedPin,
reader.IsDBNull(3) ? string.Empty : reader.GetString(3)
));
}
}
}
return pins;
}
private string EncryptPin(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);
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 DecryptPin(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);
}
}
}
}
}

215
Managers/QaManager.cs Normal file
View File

@@ -0,0 +1,215 @@
using Microsoft.Data.Sqlite;
using System.Security.Cryptography;
using System.Text;
namespace SimPas2_Windows.Managers
{
public class QaManager
{
private readonly string mConnectionString;
private readonly byte[] mEncryptionKey;
private readonly string mCulture;
private string mJpLang;
public QaManager(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 question, int? excludeId = null)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
SELECT COUNT(*) FROM Qa
WHERE UPPER(Website) = UPPER(@website) AND UPPER(Question) = UPPER(@question)";
com.Parameters.AddWithValue("@website", website);
com.Parameters.AddWithValue("@question", question);
if (excludeId.HasValue)
{
com.CommandText += " AND Id != @excludeId";
com.Parameters.AddWithValue("@excludeId", excludeId.Value);
}
return Convert.ToInt32(com.ExecuteScalar()) > 0;
}
}
public void AddQa(string website, string question, string answer, string note)
{
if (string.IsNullOrWhiteSpace(website) || string.IsNullOrWhiteSpace(question) || string.IsNullOrWhiteSpace(answer))
{
string err = mCulture == mJpLang
? "ウェブサイト、質問及び、回答を御入力下さい。"
: "Please fill in the website, question, and answer.";
throw new ArgumentException(err);
}
string encryptedAnswer = EncryptAnswer(answer);
if (AlreadyExists(website, question))
{
string err = mCulture == mJpLang
? $"ウェブサイト及び質問「{website}/{question}」向け秘密質問は既に存在します。"
: $"An secret question with the website and question for '{website}/{question}' already exists.";
throw new ArgumentException(err);
}
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
INSERT INTO Qa (Website, Question, Answer, Note)
VALUES ($website, $question, $answer, $note)";
com.Parameters.AddWithValue("$website", website);
com.Parameters.AddWithValue("$question", question);
com.Parameters.AddWithValue("$answer", encryptedAnswer);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
com.ExecuteNonQuery();
}
}
public bool EditQa(int id, string website, string question, string answer, string note)
{
if (string.IsNullOrWhiteSpace(website) || string.IsNullOrWhiteSpace(question) || string.IsNullOrWhiteSpace(answer))
{
string err = mCulture == mJpLang
? "ウェブサイト、質問及び、回答を御入力下さい。"
: "Please fill in the website, question, and answer.";
throw new ArgumentException(err);
}
string encryptedAnswer = EncryptAnswer(answer);
if (AlreadyExists(website, question, id))
{
string err = mCulture == mJpLang
? $"ウェブサイト及び質問「{website}/{question}」向け秘密質問は既に存在します。"
: $"An secret question with the website and question for '{website}/{question}' already exists.";
throw new ArgumentException(err);
}
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
UPDATE Qa
SET Website = $website, Question = $question, Answer = $answer, Note = $note
WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
com.Parameters.AddWithValue("$website", website);
com.Parameters.AddWithValue("$question", question);
com.Parameters.AddWithValue("$answer", encryptedAnswer);
com.Parameters.AddWithValue("$note", string.IsNullOrEmpty(note) ? DBNull.Value : note);
return com.ExecuteNonQuery() > 0;
}
}
public bool DeleteQa(int id)
{
using (SqliteConnection conn = new SqliteConnection(mConnectionString))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = "DELETE FROM Qa WHERE Id = $id";
com.Parameters.AddWithValue("$id", id);
return com.ExecuteNonQuery() > 0;
}
}
public List<(int Id, string Website, string Question, string Answer, string Note)> GetAll(string keyword = "")
{
var qas = 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, Question, Answer, Note FROM Qa
ORDER BY Website ASC";
}
else
{
com.CommandText = @"
SELECT Id, Website, Question, Answer, Note FROM Qa
WHERE Website LIKE @keyword OR Question LIKE @keyword
ORDER BY Website ASC";
com.Parameters.AddWithValue("@keyword", $"%{keyword}%");
}
using (SqliteDataReader reader = com.ExecuteReader())
{
while (reader.Read())
{
string encryptedAnswer = reader.GetString(3);
string decryptedAnswer = DecryptAnswer(encryptedAnswer);
qas.Add((
reader.GetInt32(0),
reader.GetString(1),
reader.GetString(2),
decryptedAnswer,
reader.IsDBNull(4) ? string.Empty : reader.GetString(4)
));
}
}
}
return qas;
}
private string EncryptAnswer(string answer)
{
using (Aes aes = Aes.Create())
{
aes.Key = mEncryptionKey;
aes.GenerateIV();
byte[] iv = aes.IV;
using (ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, iv))
{
byte[] plainBytes = Encoding.UTF8.GetBytes(answer);
byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
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 DecryptAnswer(string encryptedAnswer)
{
byte[] combined = Convert.FromBase64String(encryptedAnswer);
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 = mEncryptionKey;
aes.IV = iv;
using (ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
{
byte[] decryptedBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
return Encoding.UTF8.GetString(decryptedBytes);
}
}
}
}
}

222
MasterPasswordForm.cs Normal file
View File

@@ -0,0 +1,222 @@
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;
}
}
}
}

122
Program.cs Normal file
View File

@@ -0,0 +1,122 @@
using Microsoft.Data.Sqlite;
namespace SimPas2_Windows
{
internal static class Program
{
private static readonly string AppDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"076Soft", "SimPas");
private static readonly string DatabasePath = Path.Combine(AppDataPath, "SimPas.db");
[STAThread]
static void Main()
{
//Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en");
//Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en");
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("ja-JP");
Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("ja-JP");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
InitializeDatabase();
ApplicationConfiguration.Initialize();
Application.Run(new SimPas2());
}
private static void InitializeDatabase()
{
try
{
if (!Directory.Exists(AppDataPath))
{
Directory.CreateDirectory(AppDataPath);
}
if (!File.Exists(DatabasePath))
{
using (var conn = new SqliteConnection($"Data Source={DatabasePath}"))
{
conn.Open();
SqliteCommand com = conn.CreateCommand();
com.CommandText = @"
CREATE TABLE IF NOT EXISTS Settings (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Key VARCHAR(255) NOT NULL,
Value TEXT
);
CREATE TABLE IF NOT EXISTS Passwords (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Website VARCHAR(255) NOT NULL,
Username VARCHAR(255) NOT NULL,
Password VARCHAR(255) NOT NULL,
Note TEXT
);
CREATE TABLE IF NOT EXISTS Otp (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Website VARCHAR(255) NOT NULL,
Secret VARCHAR(255) NOT NULL,
Issuer VARCHAR(255) NOT NULL,
Algorithm VARCHAR(255) NOT NULL,
Duration INTEGER NOT NULL,
Digits INTEGER NOT NULL,
Note TEXT
);
CREATE TABLE IF NOT EXISTS Pin (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Website VARCHAR(255) NOT NULL,
Pincode VARCHAR(6) NOT NULL,
Note TEXT
);
CREATE TABLE IF NOT EXISTS Qa (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Website VARCHAR(255) NOT NULL,
Question VARCHAR(255) NOT NULL,
Answer VARCHAR(255) NOT NULL,
Note TEXT
);
CREATE TABLE IF NOT EXISTS Cc (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Brand VARCHAR(50) NOT NULL,
Cardnumber VARCHAR(20) NOT NULL,
Expiration VARCHAR(5) NOT NULL,
Cvc VARCHAR(4) NOT NULL,
Holdername VARCHAR(255) NOT NULL,
Note TEXT
);
CREATE TABLE IF NOT EXISTS Crypto (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Currency VARCHAR(20) NOT NULL,
Name VARCHAR(20) NOT NULL,
Address VARCHAR(255) NOT NULL,
Seed TEXT NOT NULL,
Viewkey VARCHAR(255),
Spendkey VARCHAR(255),
Height VARCHAR(255),
Password VARCHAR(255),
Note TEXT
);
CREATE TABLE IF NOT EXISTS Notes (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name VARCHAR(255) NOT NULL,
Text TEXT
);
";
com.ExecuteNonQuery();
}
}
}
catch (Exception e)
{
MessageBox.Show(
$"Failed to initialize database: {e.Message}",
"Database Error",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
}
}

28
SimPas2-Windows.csproj Normal file
View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>SimPas2_Windows</RootNamespace>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>simpas.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="simpas.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.9" />
</ItemGroup>
<ItemGroup>
<None Update="simpas.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

25
SimPas2-Windows.sln Normal file
View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36414.22 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimPas2-Windows", "SimPas2-Windows.csproj", "{DA781B9D-51EB-4CD7-9CDA-CBD8E601AB2F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DA781B9D-51EB-4CD7-9CDA-CBD8E601AB2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA781B9D-51EB-4CD7-9CDA-CBD8E601AB2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA781B9D-51EB-4CD7-9CDA-CBD8E601AB2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA781B9D-51EB-4CD7-9CDA-CBD8E601AB2F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B269DEAE-3AE2-43D9-A99A-D94026802BFC}
EndGlobalSection
EndGlobal

BIN
simpas.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB