password generation service to ts

This commit is contained in:
Kyle Spearrin 2017-11-02 12:33:43 -04:00
parent c586349014
commit 8eeb5821f0
7 changed files with 312 additions and 275 deletions

View File

@ -15,7 +15,6 @@
<script type="text/javascript" src="services/syncService.js"></script>
<script type="text/javascript" src="services/autofillService.js"></script>
<script type="text/javascript" src="services/appIdService.js"></script>
<script type="text/javascript" src="services/passwordGenerationService.js"></script>
<script type="text/javascript" src="services/totpService.js"></script>
<script type="text/javascript" src="services/environmentService.js"></script>
</head>

View File

@ -4,6 +4,7 @@ import i18nService from './services/i18nService.js';
import LockService from './services/lockService.js';
import UtilsService from './services/utils.service';
import CryptoService from './services/crypto.service';
import PasswordGenerationService from './services/passwordGeneration.service';
// Model imports
import { AttachmentData } from './models/data/attachmentData';
@ -86,7 +87,7 @@ var bg_isBackground = true,
setIcon, refreshBadgeAndMenu);
window.bg_syncService = bg_syncService = new SyncService(bg_cipherService, bg_folderService, bg_userService, bg_apiService, bg_settingsService,
bg_cryptoService, logout);
window.bg_passwordGenerationService = bg_passwordGenerationService = new PasswordGenerationService(bg_constantsService, bg_utilsService, bg_cryptoService);
window.bg_passwordGenerationService = bg_passwordGenerationService = new PasswordGenerationService(bg_cryptoService);
window.bg_totpService = bg_totpService = new TotpService(bg_constantsService);
window.bg_autofillService = bg_autofillService = new AutofillService(bg_utilsService, bg_totpService, bg_tokenService, bg_cipherService,
bg_constantsService);
@ -101,7 +102,7 @@ var bg_isBackground = true,
eventAction: 'Generated Password From Command'
});
bg_passwordGenerationService.getOptions().then(function (options) {
var password = bg_passwordGenerationService.generatePassword(options);
var password = PasswordGenerationService.generatePassword(options);
bg_utilsService.copyToClipboard(password);
bg_passwordGenerationService.addHistory(password);
});
@ -236,7 +237,7 @@ var bg_isBackground = true,
eventAction: 'Generated Password From Context Menu'
});
bg_passwordGenerationService.getOptions().then(function (options) {
var password = bg_passwordGenerationService.generatePassword(options);
var password = PasswordGenerationService.generatePassword(options);
bg_utilsService.copyToClipboard(password);
bg_passwordGenerationService.addHistory(password);
});

View File

@ -0,0 +1,9 @@
export default class PasswordHistory {
password: string;
date: number;
constructor(password: string, date: number) {
this.password = password;
this.date = date;
}
}

View File

@ -17,11 +17,11 @@ const Keys = {
const SigningAlgorithm = {
name: 'HMAC',
hash: { name: 'SHA-256' }
hash: { name: 'SHA-256' },
};
const AesAlgorithm = {
name: 'AES-CBC'
name: 'AES-CBC',
};
const Crypto = window.crypto;
@ -269,7 +269,7 @@ export default class CryptoService {
return this.encrypt(bytes, key, 'raw');
}
async encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey,
async encrypt(plainValue: string | Uint8Array, key?: SymmetricCryptoKey,
plainValueEncoding: string = 'utf8'): Promise<CipherString> {
if (!plainValue) {
return Promise.resolve(null);
@ -307,7 +307,7 @@ export default class CryptoService {
return encBytes.buffer;
}
async decrypt(cipherString: CipherString, key: SymmetricCryptoKey,
async decrypt(cipherString: CipherString, key?: SymmetricCryptoKey,
outputEncoding: string = 'utf8'): Promise<string> {
const ivBytes: string = forge.util.decode64(cipherString.initializationVector);
const ctBytes: string = forge.util.decode64(cipherString.cipherText);
@ -531,7 +531,7 @@ export default class CryptoService {
private async computeMacWC(dataBuf: ArrayBuffer, macKeyBuf: ArrayBuffer): Promise<ArrayBuffer> {
const key = await Subtle.importKey('raw', macKeyBuf, SigningAlgorithm, false, ['sign']);
return await Subtle.sign(SigningAlgorithm, key, dataBuf);
return await Subtle.sign(SigningAlgorithm, key, dataBuf);
}
// Safely compare two MACs in a way that protects against timing attacks (Double HMAC Verification).

View File

@ -0,0 +1,245 @@
import { CipherString } from '../models/domain/cipherString';
import PasswordHistory from '../models/domain/passwordHistory';
import ConstantsService from './constants.service';
import CryptoService from './crypto.service';
import UtilsService from './utils.service';
const DefaultOptions = {
length: 10,
ambiguous: false,
number: true,
minNumber: 1,
uppercase: true,
minUppercase: 1,
lowercase: true,
minLowercase: 1,
special: false,
minSpecial: 1,
};
const Keys = {
options: 'passwordGenerationOptions',
};
const MaxPasswordsInHistory = 100;
export default class PasswordGenerationService {
static generatePassword(options: any): string {
// overload defaults with given options
const o = UtilsService.extendObject({}, DefaultOptions, options);
// sanitize
if (o.uppercase && o.minUppercase < 0) {
o.minUppercase = 1;
}
if (o.lowercase && o.minLowercase < 0) {
o.minLowercase = 1;
}
if (o.number && o.minNumber < 0) {
o.minNumber = 1;
}
if (o.special && o.minSpecial < 0) {
o.minSpecial = 1;
}
if (!o.length || o.length < 1) {
o.length = 10;
}
const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial;
if (o.length < minLength) {
o.length = minLength;
}
const positions: string[] = [];
if (o.lowercase && o.minLowercase > 0) {
for (let i = 0; i < o.minLowercase; i++) {
positions.push('l');
}
}
if (o.uppercase && o.minUppercase > 0) {
for (let i = 0; i < o.minUppercase; i++) {
positions.push('u');
}
}
if (o.number && o.minNumber > 0) {
for (let i = 0; i < o.minNumber; i++) {
positions.push('n');
}
}
if (o.special && o.minSpecial > 0) {
for (let i = 0; i < o.minSpecial; i++) {
positions.push('s');
}
}
while (positions.length < o.length) {
positions.push('a');
}
// shuffle
positions.sort(() => {
return UtilsService.secureRandomNumber(0, 1) * 2 - 1;
});
// build out the char sets
let allCharSet = '';
let lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz';
if (o.ambiguous) {
lowercaseCharSet += 'l';
}
if (o.lowercase) {
allCharSet += lowercaseCharSet;
}
let uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ';
if (o.ambiguous) {
uppercaseCharSet += 'O';
}
if (o.uppercase) {
allCharSet += uppercaseCharSet;
}
let numberCharSet = '23456789';
if (o.ambiguous) {
numberCharSet += '01';
}
if (o.number) {
allCharSet += numberCharSet;
}
const specialCharSet = '!@#$%^&*';
if (o.special) {
allCharSet += specialCharSet;
}
let password = '';
for (let i = 0; i < o.length; i++) {
let positionChars: string;
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;
}
const randomCharIndex = UtilsService.secureRandomNumber(0, positionChars.length - 1);
password += positionChars.charAt(randomCharIndex);
}
return password;
}
optionsCache: any;
history: PasswordHistory[];
constructor(private cryptoService: CryptoService) {
const self = this;
const historyKey = ConstantsService.generatedPasswordHistoryKey;
UtilsService.getObjFromStorage<PasswordHistory[]>(historyKey).then((encrypted) => {
return self.decryptHistory(encrypted);
}).then((history) => {
self.history = history;
});
}
// TODO: remove in favor of static
generatePassword(options: any) {
return PasswordGenerationService.generatePassword(options);
}
async getOptions() {
if (this.optionsCache) {
return this.optionsCache;
}
const options = await UtilsService.getObjFromStorage(Keys.options);
this.optionsCache = options;
return options;
}
async saveOptions(options: any) {
await UtilsService.saveObjToStorage(Keys.options, options);
this.optionsCache = options;
}
getHistory() {
return this.history;
}
async addHistory(password: string) {
// Prevent duplicates
if (this.matchesPrevious(password)) {
return;
}
this.history.push(new PasswordHistory(password, Date.now()));
// Remove old items.
if (this.history.length > MaxPasswordsInHistory) {
this.history.shift();
}
await this.saveHistory();
}
clear(): Promise<any> {
this.history = [];
return UtilsService.removeFromStorage(ConstantsService.generatedPasswordHistoryKey);
}
private async saveHistory() {
const history = await this.encryptHistory();
return UtilsService.saveObjToStorage(ConstantsService.generatedPasswordHistoryKey, history);
}
private encryptHistory(): Promise<PasswordHistory[]> {
if (this.history == null) {
return Promise.resolve([]);
}
const self = this;
const promises = self.history.map(async (item) => {
const encrypted = await self.cryptoService.encrypt(item.password);
return new PasswordHistory(encrypted.encryptedString, item.date);
});
return Promise.all(promises);
}
private decryptHistory(history: PasswordHistory[]): Promise<PasswordHistory[]> {
if (history == null) {
return Promise.resolve([]);
}
const self = this;
const promises = history.map(async (item) => {
const decrypted = await self.cryptoService.decrypt(new CipherString(item.password));
return new PasswordHistory(decrypted, item.date);
});
return Promise.all(promises);
}
private matchesPrevious(password: string): boolean {
if (this.history == null) {
return false;
}
const len = this.history.length;
return len !== 0 && this.history[len - 1].password === password;
}
}

View File

@ -1,266 +0,0 @@
function PasswordGenerationService(constantsService, utilsService, cryptoService) {
this.optionsCache = null;
this.constantsService = constantsService;
this.utilsService = utilsService;
this.cryptoService = cryptoService;
this.history = [];
initPasswordGenerationService(this);
}
function initPasswordGenerationService(self) {
var optionsKey = 'passwordGenerationOptions';
var defaultOptions = {
length: 10,
ambiguous: false,
number: true,
minNumber: 1,
uppercase: true,
minUppercase: 1,
lowercase: true,
minLowercase: 1,
special: false,
minSpecial: 1
};
PasswordGenerationService.prototype.generatePassword = function (options) {
// overload defaults with given options
var o = extend({}, defaultOptions, options);
// sanitize
if (o.uppercase && o.minUppercase < 0) o.minUppercase = 1;
if (o.lowercase && o.minLowercase < 0) o.minLowercase = 1;
if (o.number && o.minNumber < 0) o.minNumber = 1;
if (o.special && o.minSpecial < 0) o.minSpecial = 1;
if (!o.length || o.length < 1) o.length = 10;
var minLength = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial;
if (o.length < minLength) o.length = minLength;
var positions = [];
if (o.lowercase && o.minLowercase > 0) {
for (var i = 0; i < o.minLowercase; i++) {
positions.push('l');
}
}
if (o.uppercase && o.minUppercase > 0) {
for (var j = 0; j < o.minUppercase; j++) {
positions.push('u');
}
}
if (o.number && o.minNumber > 0) {
for (var k = 0; k < o.minNumber; k++) {
positions.push('n');
}
}
if (o.special && o.minSpecial > 0) {
for (var l = 0; l < o.minSpecial; l++) {
positions.push('s');
}
}
while (positions.length < o.length) {
positions.push('a');
}
// shuffle
positions.sort(function () {
return randomInt(0, 1) * 2 - 1;
});
// build out the char sets
var allCharSet = '';
var lowercaseCharSet = 'abcdefghijkmnopqrstuvwxyz';
if (o.ambiguous) lowercaseCharSet += 'l';
if (o.lowercase) allCharSet += lowercaseCharSet;
var uppercaseCharSet = 'ABCDEFGHIJKLMNPQRSTUVWXYZ';
if (o.ambiguous) uppercaseCharSet += 'O';
if (o.uppercase) allCharSet += uppercaseCharSet;
var numberCharSet = '23456789';
if (o.ambiguous) numberCharSet += '01';
if (o.number) allCharSet += numberCharSet;
var specialCharSet = '!@#$%^&*';
if (o.special) allCharSet += specialCharSet;
var password = '';
for (var m = 0; m < o.length; m++) {
var positionChars;
switch (positions[m]) {
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 = randomInt(0, positionChars.length - 1);
password += positionChars.charAt(randomCharIndex);
}
return password;
};
// EFForg/OpenWireless
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
function randomInt(min, max) {
var rval = 0;
var range = max - min + 1;
var bits_needed = Math.ceil(Math.log2(range));
if (bits_needed > 53) {
throw new Exception('We cannot generate numbers larger than 53 bits.');
}
var bytes_needed = Math.ceil(bits_needed / 8);
var mask = Math.pow(2, bits_needed) - 1;
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
// Create byte array and fill with N random numbers
var byteArray = new Uint8Array(bytes_needed);
window.crypto.getRandomValues(byteArray);
var p = (bytes_needed - 1) * 8;
for (var i = 0; i < bytes_needed; i++) {
rval += byteArray[i] * Math.pow(2, p);
p -= 8;
}
// Use & to apply the mask and reduce the number of recursive lookups
rval = rval & mask;
if (rval >= range) {
// Integer out of acceptable range
return randomInt(min, max);
}
// Return an integer that falls within the range
return min + rval;
}
function extend() {
for (var i = 1; i < arguments.length; i++) {
for (var key in arguments[i]) {
if (arguments[i].hasOwnProperty(key)) {
arguments[0][key] = arguments[i][key];
}
}
}
return arguments[0];
}
PasswordGenerationService.prototype.getOptions = function () {
var deferred = Q.defer();
var self = this;
if (self.optionsCache) {
deferred.resolve(self.optionsCache);
return deferred.promise;
}
chrome.storage.local.get(optionsKey, function (obj) {
var options = obj[optionsKey];
if (!options) {
options = defaultOptions;
}
self.optionsCache = options;
deferred.resolve(self.optionsCache);
});
return deferred.promise;
};
PasswordGenerationService.prototype.saveOptions = function (options) {
var deferred = Q.defer();
var self = this;
var obj = {};
obj[optionsKey] = options;
chrome.storage.local.set(obj, function () {
self.optionsCache = options;
deferred.resolve();
});
return deferred.promise;
};
// History
var historyKey = self.constantsService.generatedPasswordHistory;
var maxPasswordsInHistory = 100;
self.utilsService.getObjFromStorage(historyKey).then(function (encrypted) {
return decrypt(encrypted);
}).then(function (history) {
history.forEach(function (item) {
self.history.push(item);
});
});
PasswordGenerationService.prototype.getHistory = function () {
return self.history;
};
PasswordGenerationService.prototype.addHistory = function (password) {
// Prevent duplicates
if (matchesPrevious(password)) {
return;
}
self.history.push({
password: password,
date: Date.now()
});
// Remove old items.
if (self.history.length > maxPasswordsInHistory) {
self.history.shift();
}
save();
};
PasswordGenerationService.prototype.clear = function () {
self.history = [];
self.utilsService.removeFromStorage(historyKey);
};
function save() {
return encryptHistory().then(function (history) {
return self.utilsService.saveObjToStorage(historyKey, history);
});
}
function encryptHistory() {
var promises = self.history.map(function (historyItem) {
return self.cryptoService.encrypt(historyItem.password).then(function (encrypted) {
return {
password: encrypted.encryptedString,
date: historyItem.date
};
});
});
return Q.all(promises);
}
function decrypt(history) {
var promises = history.map(function (item) {
return self.cryptoService.decrypt(new CipherString(item.password)).then(function (decrypted) {
return {
password: decrypted,
date: item.date
};
});
});
return Q.all(promises);
}
function matchesPrevious(password) {
var len = self.history.length;
return len !== 0 && self.history[len - 1].password === password;
}
}

View File

@ -8,6 +8,55 @@ const AnalyticsIds = {
};
export default class UtilsService {
static extendObject(...objects: any[]): any {
for (let i = 1; i < objects.length; i++) {
for (const key in objects[i]) {
if (objects[i].hasOwnProperty(key)) {
objects[0][key] = objects[i][key];
}
}
}
return objects[0];
}
// EFForg/OpenWireless
// ref https://github.com/EFForg/OpenWireless/blob/master/app/js/diceware.js
static secureRandomNumber(min: number, max: number): number {
let rval = 0;
const range = max - min + 1;
const bitsNeeded = Math.ceil(Math.log2(range));
if (bitsNeeded > 53) {
throw new Error('We cannot generate numbers larger than 53 bits.');
}
const bytesNeeded = Math.ceil(bitsNeeded / 8);
const mask = Math.pow(2, bitsNeeded) - 1;
// 7776 -> (2^13 = 8192) -1 == 8191 or 0x00001111 11111111
// Create byte array and fill with N random numbers
const byteArray = new Uint8Array(bytesNeeded);
window.crypto.getRandomValues(byteArray);
let p = (bytesNeeded - 1) * 8;
for (let i = 0; i < bytesNeeded; i++) {
rval += byteArray[i] * Math.pow(2, p);
p -= 8;
}
// Use & to apply the mask and reduce the number of recursive lookups
// tslint:disable-next-line
rval = rval & mask;
if (rval >= range) {
// Integer out of acceptable range
return UtilsService.secureRandomNumber(min, max);
}
// Return an integer that falls within the range
return min + rval;
}
static fromB64ToArray(str: string): Uint8Array {
const binaryString = window.atob(str);
const bytes = new Uint8Array(binaryString.length);