I wrote a password manager with the goal of being able to recover your passwords without connecting to an external resource. I used Electrum's recoverable bitcoin wallet for inspiration. When you create a profile in the password manager, a random 12 word phrase is generated, hashed, encrypted with the profile password and saved in the database. When you log in to a profile, the phrase hash is decrypted and saved in memory. When you add an account, you give the name of the website the account is for (service) and your username. The password is a hash of service+username with phrase hash as seed. No password is actually saved unless you want to use your own, in which case it is encrypted with the phrase hash. When you go to request the password for an account the program checks if there is a password saved (indicating you used your own password). If there is a password saved it is decrypted and given to the user, otherwise it is generated again with the phrase hash. If you lose the password manager or accidentally delete a profile you only need to know the phrase and the service+username of each account to recover your passwords. Passwords that were input by the user of course can not be recovered. I want to know if my method of generating passwords and retrieving them is secure.
Database.cs
public class Database
{
public class DBProfile
{
public DBProfile(string name, string phrasehash)
{
Name = name;
EncryptedPhraseHash = phrasehash;
Accounts = new List<Account>();
}
[JsonProperty("Name")]
public string Name { get; set; }
[JsonProperty("EncryptedPhraseHash")]
public string EncryptedPhraseHash { get; set; }
[JsonProperty("Accounts")]
public List<Account> Accounts { get; set; }
}
public class Account
{
public Account(string servicename, string username, string encryptedpassword)
{
ServiceName = servicename;
Username = username;
EncryptedPassword = encryptedpassword;
}
[JsonProperty("ServiceName")]
public string ServiceName { get; set; }
[JsonProperty("Username")]
public string Username { get; set; }
[JsonProperty("EncryptedPassword")]
public string EncryptedPassword { get; set; }
}
public class PhraseHashJson
{
public PhraseHashJson(string phrasehash)
{
PhraseHash = phrasehash;
}
[JsonProperty("PhraseHash")]
public string PhraseHash { get; set; }
}
public static bool IsProfile(string name)
{
return File.Exists(name + ".mpr");
}
public static DBProfile CreateProfile(string profilename, string profilepassword, string phrase)
{
var passhash = Crypto.GenerateHash(profilepassword);
var phrasehash = Crypto.GenerateHash(phrase);
var json = new PhraseHashJson(phrasehash);
var encryptedphrasehash = Crypto.EncryptStringAES(JsonConvert.SerializeObject(json), passhash);
var newProfile = new DBProfile(profilename, encryptedphrasehash);
var text = Crypto.EncryptStringAES(JsonConvert.SerializeObject(newProfile), "b_@_51C-$33d");
File.WriteAllText(profilename + ".mpr", text);
return newProfile;
}
public static void SaveProfile(DBProfile dbProfile)
{
var text = Crypto.EncryptStringAES(JsonConvert.SerializeObject(dbProfile), "b_@_51C-$33d");
File.WriteAllText(dbProfile.Name + ".mpr", text);
}
public static void DeleteProfile(string profilename)
{
File.Delete(profilename + ".mpr");
}
public static DBProfile GetProfile(string profilename)
{
var encryptedProfile = File.ReadAllText(profilename + ".mpr");
var json = Crypto.DecryptStringAES(encryptedProfile, "b_@_51C-$33d");
return JsonConvert.DeserializeObject<DBProfile>(json);
}
public static DBProfile GetProfileByPath(string path)
{
var encryptedProfile = File.ReadAllText(path);
var json = Crypto.DecryptStringAES(encryptedProfile, "b_@_51C-$33d");
return JsonConvert.DeserializeObject<DBProfile>(json);
}
public static List<DBProfile> GetProfiles()
{
var paths = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.mpr");
var profiles = paths.Select(GetProfileByPath).ToList();
return profiles;
}
public static string GeneratePhrase()
{
byte[] data = new byte[4];
var lphrase = new List<string>();
var wordlist = Properties.Resources.wordlist.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).ToList();
RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider();
for (var i = 0; i < 12; i++)
{
rngCsp.GetBytes(data);
int randomNum = BitConverter.ToInt32(data, 0);
int place = Mod(randomNum, wordlist.Count);
lphrase.Add(wordlist[place]);
}
return string.Join(" ", lphrase);
}
public static int Mod(int x, int m)
{
int r = x % m;
return r < 0 ? r + m : r;
}
}
Crypto.cs-- Encryption class I found on StackOverflow and modified a bit
public class Crypto
{
private static readonly byte[] Salt = Encoding.ASCII.GetBytes("P&s$w0<rd__>_^*_M6n2g#r"); //todo: do something about this
public static string GenerateHash(string plaintext)
{
var plainText = Encoding.UTF8.GetBytes(plaintext);
HashAlgorithm algorithm = new SHA256Managed();
var plainTextWithSaltBytes = new byte[plainText.Length + Salt.Length];
for (var i = 0; i < plainText.Length; i++)
plainTextWithSaltBytes[i] = plainText[i];
for (var i = 0; i < Salt.Length; i++)
plainTextWithSaltBytes[plainText.Length + i] = Salt[i];
return Convert.ToBase64String(algorithm.ComputeHash(plainTextWithSaltBytes));
}
public static string GenerateHashWithSeed(string plaintext, string salt)
{
HashAlgorithm algorithm = new SHA256Managed();
var plainText = Encoding.UTF8.GetBytes(plaintext);
var saltBytes = Encoding.UTF8.GetBytes(salt);
var plainTextWithSaltBytes = new byte[plainText.Length + saltBytes.Length];
for (var i = 0; i < plainText.Length; i++)
plainTextWithSaltBytes[i] = plainText[i];
for (var i = 0; i < saltBytes.Length; i++)
plainTextWithSaltBytes[plainText.Length + i] = saltBytes[i];
return Convert.ToBase64String(algorithm.ComputeHash(plainTextWithSaltBytes));
}
/// <summary>
/// Encrypt the given string using AES. The string can be decrypted using
/// DecryptStringAES(). The sharedSecret parameters must match.
/// </summary>
/// <param name="plainText">The text to encrypt.</param>
/// <param name="sharedSecret">A password used to generate a key for encryption.</param>
public static string EncryptStringAES(string plainText, string sharedSecret)
{
if (string.IsNullOrEmpty(plainText))
throw new ArgumentNullException("plainText");
if (string.IsNullOrEmpty(sharedSecret))
throw new ArgumentNullException("sharedSecret");
string outStr; // encrypted string to return
try
{
// generate the key from the shared secret and the salt
var key = new Rfc2898DeriveBytes(sharedSecret, Salt);
// Create a RijndaelManaged object
var aesAlg = new RijndaelManaged();
aesAlg.Key = key.GetBytes(aesAlg.KeySize/8);
// Create a decryptor to perform the stream transform.
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
// Create the streams used for encryption.
using (var msEncrypt = new MemoryStream())
{
// prepend the IV
msEncrypt.Write(BitConverter.GetBytes(aesAlg.IV.Length), 0, sizeof (int));
msEncrypt.Write(aesAlg.IV, 0, aesAlg.IV.Length);
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
using (var swEncrypt = new StreamWriter(csEncrypt))
swEncrypt.Write(plainText); //Write all data to the stream.
outStr = Convert.ToBase64String(msEncrypt.ToArray());
}
aesAlg.Clear();
}
catch (Exception ex)
{
Console.WriteLine("Error: {0}", ex);
return "";
}
// Return the encrypted bytes from the memory stream.
return outStr;
}
/// <summary>
/// Decrypt the given string. Assumes the string was encrypted using
/// EncryptStringAES(), using an identical sharedSecret.
/// </summary>
/// <param name="cipherText">The text to decrypt.</param>
/// <param name="sharedSecret">A password used to generate a key for decryption.</param>
public static string DecryptStringAES(string cipherText, string sharedSecret)
{
if (string.IsNullOrEmpty(cipherText))
throw new ArgumentNullException("cipherText");
if (string.IsNullOrEmpty(sharedSecret))
throw new ArgumentNullException("sharedSecret");
// Declare the string used to hold
// the decrypted text.
string plaintext;
try
{
// generate the key from the shared secret and the salt
var key = new Rfc2898DeriveBytes(sharedSecret, Salt);
// Create the streams used for decryption.
var bytes = Convert.FromBase64String(cipherText);
using (var msDecrypt = new MemoryStream(bytes))
{
var aesAlg = new RijndaelManaged(); // Create a RijndaelManaged object with the specified key and IV.
aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
// Get the initialization vector from the encrypted stream
aesAlg.IV = ReadByteArray(msDecrypt);
// Create a decrytor to perform the stream transform.
var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
using (var srDecrypt = new StreamReader(csDecrypt))
plaintext = srDecrypt.ReadToEnd(); // Read the decrypted bytes from the decrypting stream and place them in a string.
aesAlg.Clear();
}
}
catch (Exception ex)
{
Console.WriteLine("Error: {0}", ex);
return "";
}
return plaintext;
}
private static byte[] ReadByteArray(Stream s)
{
var rawLength = new byte[sizeof(int)];
if (s.Read(rawLength, 0, rawLength.Length) != rawLength.Length)
throw new SystemException("Stream did not contain properly formatted byte array");
var buffer = new byte[BitConverter.ToInt32(rawLength, 0)];
if (s.Read(buffer, 0, buffer.Length) != buffer.Length)
throw new SystemException("Did not read byte array properly");
return buffer;
}
}
Profile.cs
public class Profile
{
public string ProfileName;
public bool LoggedIn = false;
private Database.DBProfile _dbProfile;
private string _phraseHash;
public Profile(Database.DBProfile dbProfile)
{
_dbProfile = dbProfile;
ProfileName = _dbProfile.Name;
}
public Database.Account AddAccount(string servicename, string username, string password)
{
// If password was generated by the program we don't need to save it, just the variables we can use to regenerate it
var encryptedpassword = (password == null) ? null : Crypto.EncryptStringAES(password, _phraseHash);
var newAccount = new Database.Account(servicename, username, encryptedpassword); // Create account object
// Make sure profile does not already have an account with that name
if (_dbProfile.Accounts.Any(a => (a.Username == username) && (a.ServiceName == servicename))) return null;
// Add account to profile
_dbProfile.Accounts.Add(newAccount); // Add account to profile objects list
Database.SaveProfile(_dbProfile); // Save account in database
return newAccount;
}
public bool Login(string password)
{
var passwordHash = Crypto.GenerateHash(password); // Get password hash
try
{
var encryptedphrasehash = Crypto.DecryptStringAES(_dbProfile.EncryptedPhraseHash, passwordHash); // Try to decrypt phrase with given password, throws if invalid
_phraseHash = JsonConvert.DeserializeObject<Database.PhraseHashJson>(encryptedphrasehash).PhraseHash; // Get phrase hash from decrypted profile
LoggedIn = true;
return true;
}
catch (Exception) // Wrong password
{
return false;
}
}
public void Logout()
{
RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider();
for (var i = 0; i < 3; i++)
{
byte[] data = new byte[(int) Math.Round((double) (_phraseHash.Count()))];
rngCsp.GetBytes(data);
int randomNum = BitConverter.ToInt32(data, 0);
_phraseHash = randomNum.ToString();
}
LoggedIn = false;
_phraseHash = null;
}
public string GetAccountPassword(int accountIndex)
{
Database.Account account = _dbProfile.Accounts[accountIndex];
// If encrypted password is null the password was generated with the program so we just re-generate it
// If it isn't null the password was given by the user so we need to decrypt it
return (account.EncryptedPassword == null)
? Crypto.GenerateHashWithSeed(account.ServiceName + account.Username, _phraseHash)
: Crypto.DecryptStringAES(account.EncryptedPassword, _phraseHash);
}
public void Delete()
{
if (LoggedIn) Logout(); // Erase phrase hash
Database.DeleteProfile(ProfileName);
}
public List<Database.Account> GetAccounts()
{
return _dbProfile.Accounts;
}
public void DeleteAccount(int index)
{
var accountRemove = _dbProfile.Accounts[index];
_dbProfile.Accounts.Remove(accountRemove);
Database.SaveProfile(_dbProfile);
}
}