mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
password generation service
This commit is contained in:
parent
818414eb37
commit
f46151bb71
5 changed files with 8200 additions and 0 deletions
18
src/Core/Abstractions/IPasswordGenerationService.cs
Normal file
18
src/Core/Abstractions/IPasswordGenerationService.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
|
||||||
|
namespace Bit.Core.Abstractions
|
||||||
|
{
|
||||||
|
public interface IPasswordGenerationService
|
||||||
|
{
|
||||||
|
Task AddHistoryAsync(string password);
|
||||||
|
Task ClearAsync();
|
||||||
|
Task<string> GeneratePassphraseAsync(PasswordGenerationOptions options);
|
||||||
|
Task<string> GeneratePasswordAsync(PasswordGenerationOptions options);
|
||||||
|
Task<List<GeneratedPasswordHistory>> GetHistoryAsync();
|
||||||
|
Task<PasswordGenerationOptions> GetOptionsAsync();
|
||||||
|
Task<object> PasswordStrength(string password, List<string> userInputs = null);
|
||||||
|
Task SaveOptionsAsync(PasswordGenerationOptions options);
|
||||||
|
}
|
||||||
|
}
|
10
src/Core/Models/Domain/GeneratedPasswordHistory.cs
Normal file
10
src/Core/Models/Domain/GeneratedPasswordHistory.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Domain
|
||||||
|
{
|
||||||
|
public class GeneratedPasswordHistory
|
||||||
|
{
|
||||||
|
public string Password { get; set; }
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
}
|
||||||
|
}
|
58
src/Core/Models/Domain/PasswordGenerationOptions.cs
Normal file
58
src/Core/Models/Domain/PasswordGenerationOptions.cs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
namespace Bit.Core.Models.Domain
|
||||||
|
{
|
||||||
|
public class PasswordGenerationOptions
|
||||||
|
{
|
||||||
|
public PasswordGenerationOptions() { }
|
||||||
|
|
||||||
|
public PasswordGenerationOptions(bool defaultOptions)
|
||||||
|
{
|
||||||
|
if(defaultOptions)
|
||||||
|
{
|
||||||
|
Length = 14;
|
||||||
|
Ambiguous = false;
|
||||||
|
Number = true;
|
||||||
|
MinNumber = 1;
|
||||||
|
Uppercase = true;
|
||||||
|
MinUppercase = 0;
|
||||||
|
Lowercase = true;
|
||||||
|
MinLowercase = 0;
|
||||||
|
Special = false;
|
||||||
|
MinSpecial = 1;
|
||||||
|
Type = "password";
|
||||||
|
NumWords = 3;
|
||||||
|
WordSeparator = "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int? Length { get; set; }
|
||||||
|
public bool? Ambiguous { get; set; }
|
||||||
|
public bool? Number { get; set; }
|
||||||
|
public int? MinNumber { get; set; }
|
||||||
|
public bool? Uppercase { get; set; }
|
||||||
|
public int? MinUppercase { get; set; }
|
||||||
|
public bool? Lowercase { get; set; }
|
||||||
|
public int? MinLowercase { get; set; }
|
||||||
|
public bool? Special { get; set; }
|
||||||
|
public int? MinSpecial { get; set; }
|
||||||
|
public string Type { get; set; }
|
||||||
|
public int? NumWords { get; set; }
|
||||||
|
public string WordSeparator { get; set; }
|
||||||
|
|
||||||
|
public void Merge(PasswordGenerationOptions defaults)
|
||||||
|
{
|
||||||
|
Length = Length ?? defaults.Length;
|
||||||
|
Ambiguous = Ambiguous ?? defaults.Ambiguous;
|
||||||
|
Number = Number ?? defaults.Number;
|
||||||
|
MinNumber = MinNumber ?? defaults.MinNumber;
|
||||||
|
Uppercase = Uppercase ?? defaults.Uppercase;
|
||||||
|
MinUppercase = MinUppercase ?? defaults.MinUppercase;
|
||||||
|
Lowercase = Lowercase ?? defaults.Lowercase;
|
||||||
|
MinLowercase = MinLowercase ?? defaults.MinLowercase;
|
||||||
|
Special = Special ?? defaults.Special;
|
||||||
|
MinSpecial = MinSpecial ?? defaults.MinSpecial;
|
||||||
|
Type = Type ?? defaults.Type;
|
||||||
|
NumWords = NumWords ?? defaults.NumWords;
|
||||||
|
WordSeparator = WordSeparator ?? defaults.WordSeparator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
326
src/Core/Services/PasswordGenerationService.cs
Normal file
326
src/Core/Services/PasswordGenerationService.cs
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Bit.Core.Services
|
||||||
|
{
|
||||||
|
public class PasswordGenerationService : IPasswordGenerationService
|
||||||
|
{
|
||||||
|
private const string Keys_Options = "passwordGenerationOptions";
|
||||||
|
private const string Keys_History = "generatedPasswordHistory";
|
||||||
|
private const int MaxPasswordsInHistory = 100;
|
||||||
|
private const string LowercaseCharSet = "abcdefghijkmnopqrstuvwxyz";
|
||||||
|
private const string UppercaseCharSet = "ABCDEFGHIJKLMNPQRSTUVWXYZ";
|
||||||
|
private const string NumberCharSet = "23456789";
|
||||||
|
private const string SpecialCharSet = "!@#$%^&*";
|
||||||
|
|
||||||
|
private readonly ICryptoService _cryptoService;
|
||||||
|
private readonly IStorageService _storageService;
|
||||||
|
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||||
|
private PasswordGenerationOptions _defaultOptions = new PasswordGenerationOptions();
|
||||||
|
private PasswordGenerationOptions _optionsCache;
|
||||||
|
private List<GeneratedPasswordHistory> _history;
|
||||||
|
|
||||||
|
public PasswordGenerationService(
|
||||||
|
ICryptoService cryptoService,
|
||||||
|
IStorageService storageService,
|
||||||
|
ICryptoFunctionService cryptoFunctionService)
|
||||||
|
{
|
||||||
|
_cryptoService = cryptoService;
|
||||||
|
_storageService = storageService;
|
||||||
|
_cryptoFunctionService = cryptoFunctionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GeneratePasswordAsync(PasswordGenerationOptions options)
|
||||||
|
{
|
||||||
|
// Overload defaults with given options
|
||||||
|
options.Merge(_defaultOptions);
|
||||||
|
if(options.Type == "passphrase")
|
||||||
|
{
|
||||||
|
return await GeneratePassphraseAsync(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize
|
||||||
|
if(options.Uppercase.GetValueOrDefault() && options.MinUppercase.GetValueOrDefault() <= 0)
|
||||||
|
{
|
||||||
|
options.MinUppercase = 1;
|
||||||
|
}
|
||||||
|
if(options.Lowercase.GetValueOrDefault() && options.MinLowercase.GetValueOrDefault() <= 0)
|
||||||
|
{
|
||||||
|
options.MinLowercase = 1;
|
||||||
|
}
|
||||||
|
if(options.Number.GetValueOrDefault() && options.MinNumber.GetValueOrDefault() <= 0)
|
||||||
|
{
|
||||||
|
options.MinNumber = 1;
|
||||||
|
}
|
||||||
|
if(options.Special.GetValueOrDefault() && options.MinSpecial.GetValueOrDefault() <= 0)
|
||||||
|
{
|
||||||
|
options.MinSpecial = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(options.Length.GetValueOrDefault() < 1)
|
||||||
|
{
|
||||||
|
options.Length = 10;
|
||||||
|
}
|
||||||
|
var minLength = options.MinSpecial.GetValueOrDefault() + options.MinLowercase.GetValueOrDefault() +
|
||||||
|
options.MinNumber.GetValueOrDefault() + options.MinSpecial.GetValueOrDefault();
|
||||||
|
if(options.Length < minLength)
|
||||||
|
{
|
||||||
|
options.Length = minLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
var positionsBuilder = new StringBuilder();
|
||||||
|
if(options.Lowercase.GetValueOrDefault() && options.MinLowercase.GetValueOrDefault() > 0)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < options.MinLowercase.GetValueOrDefault(); i++)
|
||||||
|
{
|
||||||
|
positionsBuilder.Append("l");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(options.Uppercase.GetValueOrDefault() && options.MinUppercase.GetValueOrDefault() > 0)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < options.MinUppercase.GetValueOrDefault(); i++)
|
||||||
|
{
|
||||||
|
positionsBuilder.Append("u");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(options.Number.GetValueOrDefault() && options.MinNumber.GetValueOrDefault() > 0)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < options.MinNumber.GetValueOrDefault(); i++)
|
||||||
|
{
|
||||||
|
positionsBuilder.Append("n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(options.Special.GetValueOrDefault() && options.MinSpecial.GetValueOrDefault() > 0)
|
||||||
|
{
|
||||||
|
for(int i = 0; i < options.MinSpecial.GetValueOrDefault(); i++)
|
||||||
|
{
|
||||||
|
positionsBuilder.Append("s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while(positionsBuilder.Length < options.Length.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
positionsBuilder.Append("a");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle
|
||||||
|
var positions = positionsBuilder.ToString().ToCharArray()
|
||||||
|
.OrderBy(async a => await _cryptoFunctionService.RandomNumberAsync()).ToArray();
|
||||||
|
|
||||||
|
// Build out other character sets
|
||||||
|
var allCharSet = string.Empty;
|
||||||
|
var lowercaseCharSet = LowercaseCharSet;
|
||||||
|
if(options.Ambiguous.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
lowercaseCharSet = string.Concat(lowercaseCharSet, "l");
|
||||||
|
}
|
||||||
|
if(options.Lowercase.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
allCharSet = string.Concat(allCharSet, lowercaseCharSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
var uppercaseCharSet = UppercaseCharSet;
|
||||||
|
if(options.Ambiguous.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
uppercaseCharSet = string.Concat(uppercaseCharSet, "O");
|
||||||
|
}
|
||||||
|
if(options.Uppercase.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
allCharSet = string.Concat(allCharSet, uppercaseCharSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
var numberCharSet = NumberCharSet;
|
||||||
|
if(options.Ambiguous.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
numberCharSet = string.Concat(numberCharSet, "01");
|
||||||
|
}
|
||||||
|
if(options.Number.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
allCharSet = string.Concat(allCharSet, numberCharSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
var specialCharSet = SpecialCharSet;
|
||||||
|
if(options.Special.GetValueOrDefault())
|
||||||
|
{
|
||||||
|
allCharSet = string.Concat(allCharSet, specialCharSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
var password = new StringBuilder();
|
||||||
|
for(var i = 0; i < options.Length.GetValueOrDefault(); i++)
|
||||||
|
{
|
||||||
|
var positionChars = string.Empty;
|
||||||
|
switch(positions[i])
|
||||||
|
{
|
||||||
|
case 'l':
|
||||||
|
positionChars = lowercaseCharSet;
|
||||||
|
break;
|
||||||
|
case 'u':
|
||||||
|
positionChars = uppercaseCharSet;
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
positionChars = numberCharSet;
|
||||||
|
break;
|
||||||
|
case 's':
|
||||||
|
positionChars = specialCharSet;
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
positionChars = allCharSet;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var randomCharIndex = await _cryptoService.RandomNumberAsync(0, positionChars.Length - 1);
|
||||||
|
password.Append(positionChars[randomCharIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> GeneratePassphraseAsync(PasswordGenerationOptions options)
|
||||||
|
{
|
||||||
|
options.Merge(_defaultOptions);
|
||||||
|
if(options.NumWords.GetValueOrDefault() <= 2)
|
||||||
|
{
|
||||||
|
options.NumWords = _defaultOptions.NumWords;
|
||||||
|
}
|
||||||
|
if(options.WordSeparator == null || options.WordSeparator.Length == 0 || options.WordSeparator.Length > 1)
|
||||||
|
{
|
||||||
|
options.WordSeparator = " ";
|
||||||
|
}
|
||||||
|
var listLength = WordList.EEFLongWordList.Count - 1;
|
||||||
|
var wordList = new List<string>();
|
||||||
|
for(int i = 0; i < options.NumWords.GetValueOrDefault(); i++)
|
||||||
|
{
|
||||||
|
var wordIndex = await _cryptoService.RandomNumberAsync(0, listLength);
|
||||||
|
wordList.Add(WordList.EEFLongWordList[wordIndex]);
|
||||||
|
}
|
||||||
|
return string.Join(options.WordSeparator, wordList);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PasswordGenerationOptions> GetOptionsAsync()
|
||||||
|
{
|
||||||
|
if(_optionsCache == null)
|
||||||
|
{
|
||||||
|
var options = await _storageService.GetAsync<PasswordGenerationOptions>(Keys_Options);
|
||||||
|
if(options == null)
|
||||||
|
{
|
||||||
|
_optionsCache = _defaultOptions;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
options.Merge(_defaultOptions);
|
||||||
|
_optionsCache = options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _optionsCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveOptionsAsync(PasswordGenerationOptions options)
|
||||||
|
{
|
||||||
|
await _storageService.SaveAsync(Keys_Options, options);
|
||||||
|
_optionsCache = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<GeneratedPasswordHistory>> GetHistoryAsync()
|
||||||
|
{
|
||||||
|
var hasKey = await _cryptoService.HasKeyAsync();
|
||||||
|
if(!hasKey)
|
||||||
|
{
|
||||||
|
return new List<GeneratedPasswordHistory>();
|
||||||
|
}
|
||||||
|
if(_history == null)
|
||||||
|
{
|
||||||
|
var encrypted = await _storageService.GetAsync<List<GeneratedPasswordHistory>>(Keys_History);
|
||||||
|
_history = await DecryptHistoryAsync(encrypted);
|
||||||
|
}
|
||||||
|
return _history ?? new List<GeneratedPasswordHistory>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddHistoryAsync(string password)
|
||||||
|
{
|
||||||
|
var hasKey = await _cryptoService.HasKeyAsync();
|
||||||
|
if(!hasKey)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var currentHistory = await GetHistoryAsync();
|
||||||
|
|
||||||
|
// Prevent duplicates
|
||||||
|
if(MatchesPrevious(password, currentHistory))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Remove old items.
|
||||||
|
if(currentHistory.Count > MaxPasswordsInHistory)
|
||||||
|
{
|
||||||
|
currentHistory.RemoveAt(currentHistory.Count - 1);
|
||||||
|
}
|
||||||
|
var newHistory = await EncryptHistoryAsync(currentHistory);
|
||||||
|
await _storageService.SaveAsync(Keys_History, newHistory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearAsync()
|
||||||
|
{
|
||||||
|
_history = new List<GeneratedPasswordHistory>();
|
||||||
|
await _storageService.RemoveAsync(Keys_History);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<object> PasswordStrength(string password, List<string> userInputs = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
private async Task<List<GeneratedPasswordHistory>> EncryptHistoryAsync(List<GeneratedPasswordHistory> history)
|
||||||
|
{
|
||||||
|
if(!history?.Any() ?? true)
|
||||||
|
{
|
||||||
|
return new List<GeneratedPasswordHistory>();
|
||||||
|
}
|
||||||
|
var tasks = history.Select(async item =>
|
||||||
|
{
|
||||||
|
var encrypted = await _cryptoService.EncryptAsync(item.Password);
|
||||||
|
return new GeneratedPasswordHistory
|
||||||
|
{
|
||||||
|
Password = encrypted.EncryptedString,
|
||||||
|
Date = item.Date
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var h = await Task.WhenAll(tasks);
|
||||||
|
return h.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<GeneratedPasswordHistory>> DecryptHistoryAsync(List<GeneratedPasswordHistory> history)
|
||||||
|
{
|
||||||
|
if(!history?.Any() ?? true)
|
||||||
|
{
|
||||||
|
return new List<GeneratedPasswordHistory>();
|
||||||
|
}
|
||||||
|
var tasks = history.Select(async item =>
|
||||||
|
{
|
||||||
|
var decrypted = await _cryptoService.DecryptToUtf8Async(new CipherString(item.Password));
|
||||||
|
return new GeneratedPasswordHistory
|
||||||
|
{
|
||||||
|
Password = decrypted,
|
||||||
|
Date = item.Date
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var h = await Task.WhenAll(tasks);
|
||||||
|
return h.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MatchesPrevious(string password, List<GeneratedPasswordHistory> history)
|
||||||
|
{
|
||||||
|
if(!history?.Any() ?? true)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return history.Last().Password == password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7788
src/Core/Utilities/WordList.cs
Normal file
7788
src/Core/Utilities/WordList.cs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue