diff --git a/__tests__/new/hathorwallet.test.js b/__tests__/new/hathorwallet.test.js index da6ba5d8b..8bb770f5d 100644 --- a/__tests__/new/hathorwallet.test.js +++ b/__tests__/new/hathorwallet.test.js @@ -604,3 +604,79 @@ test('start', async () => { const actualAccessData = await storage.getAccessData(); expect(decryptData(actualAccessData.words, '456')).toEqual(seed); }); + +test('checkPin', async () => { + const store = new MemoryStore(); + const storage = new Storage(store); + + const checkPinSpy = jest.spyOn(storage, 'checkPin'); + + const hWallet = new FakeHathorWallet(); + hWallet.storage = storage; + + checkPinSpy.mockReturnValue(Promise.resolve(false)); + await expect(hWallet.checkPin('0000')).resolves.toEqual(false); + expect(checkPinSpy).toHaveBeenCalledTimes(1); + checkPinSpy.mockClear() + + checkPinSpy.mockReturnValue(Promise.resolve(true)); + await expect(hWallet.checkPin('0000')).resolves.toEqual(true); + expect(checkPinSpy).toHaveBeenCalledTimes(1); +}); + +test('checkPassword', async () => { + const store = new MemoryStore(); + const storage = new Storage(store); + + const checkPasswdSpy = jest.spyOn(storage, 'checkPassword'); + + const hWallet = new FakeHathorWallet(); + hWallet.storage = storage; + + checkPasswdSpy.mockReturnValue(Promise.resolve(false)); + await expect(hWallet.checkPassword('0000')).resolves.toEqual(false); + expect(checkPasswdSpy).toHaveBeenCalledTimes(1); + checkPasswdSpy.mockClear() + + checkPasswdSpy.mockReturnValue(Promise.resolve(true)); + await expect(hWallet.checkPassword('0000')).resolves.toEqual(true); + expect(checkPasswdSpy).toHaveBeenCalledTimes(1); +}); + +test('checkPinAndPassword', async () => { + const hWallet = new FakeHathorWallet(); + const checkPinSpy = jest.spyOn(hWallet, 'checkPin'); + const checkPasswdSpy = jest.spyOn(hWallet, 'checkPassword'); + + checkPinSpy.mockReturnValue(Promise.resolve(false)); + checkPasswdSpy.mockReturnValue(Promise.resolve(false)); + await expect(hWallet.checkPinAndPassword('0000', 'passwd')).resolves.toEqual(false); + expect(checkPinSpy).toHaveBeenCalledTimes(1); + expect(checkPasswdSpy).toHaveBeenCalledTimes(0); + checkPinSpy.mockClear(); + checkPasswdSpy.mockClear(); + + checkPinSpy.mockReturnValue(Promise.resolve(true)); + checkPasswdSpy.mockReturnValue(Promise.resolve(false)); + await expect(hWallet.checkPinAndPassword('0000', 'passwd')).resolves.toEqual(false); + expect(checkPinSpy).toHaveBeenCalledTimes(1); + expect(checkPasswdSpy).toHaveBeenCalledTimes(1); + checkPinSpy.mockClear(); + checkPasswdSpy.mockClear(); + + checkPinSpy.mockReturnValue(Promise.resolve(true)); + checkPasswdSpy.mockReturnValue(Promise.resolve(true)); + await expect(hWallet.checkPinAndPassword('0000', 'passwd')).resolves.toEqual(true); + expect(checkPinSpy).toHaveBeenCalledTimes(1); + expect(checkPasswdSpy).toHaveBeenCalledTimes(1); + checkPinSpy.mockClear(); + checkPasswdSpy.mockClear(); + + checkPinSpy.mockReturnValue(Promise.resolve(false)); + checkPasswdSpy.mockReturnValue(Promise.resolve(true)); + await expect(hWallet.checkPinAndPassword('0000', 'passwd')).resolves.toEqual(false); + expect(checkPinSpy).toHaveBeenCalledTimes(1); + expect(checkPasswdSpy).toHaveBeenCalledTimes(0); + checkPinSpy.mockClear(); + checkPasswdSpy.mockClear(); +}); diff --git a/__tests__/storage/storage.test.js b/__tests__/storage/storage.test.js index 8438a4541..f63e0d4c4 100644 --- a/__tests__/storage/storage.test.js +++ b/__tests__/storage/storage.test.js @@ -9,11 +9,11 @@ import walletApi from '../../src/api/wallet'; import { MemoryStore, Storage, LevelDBStore } from '../../src/storage'; import tx_history from '../__fixtures__/tx_history'; import { processHistory, loadAddresses } from '../../src/utils/storage'; +import walletUtils from '../../src/utils/wallet'; import { P2PKH_ACCT_PATH, TOKEN_DEPOSIT_PERCENTAGE, TOKEN_AUTHORITY_MASK, TOKEN_MINT_MASK, WALLET_SERVICE_AUTH_DERIVATION_PATH } from '../../src/constants'; import { HDPrivateKey, crypto } from "bitcore-lib"; import Mnemonic from 'bitcore-mnemonic'; import * as cryptoUtils from '../../src/utils/crypto'; -import walletUtils from '../../src/utils/wallet'; import { InvalidPasswdError } from '../../src/errors'; import Network from '../../src/models/network'; @@ -638,3 +638,70 @@ test('change pin and password', async () => { expect(() => cryptoUtils.decryptData(accessData.authKey, '321')).not.toThrow(); expect(() => cryptoUtils.decryptData(accessData.acctPathKey, '321')).not.toThrow(); }); + +describe('checkPin and checkPassword', () => { + const PINCODE = '1234' + const PASSWD = 'passwd' + + it('should work with memory store', async () => { + const seed = walletUtils.generateWalletWords(); + const accessData = walletUtils.generateAccessDataFromSeed( + seed, + { + pin: PINCODE, + password: PASSWD, + networkName: 'testnet', + }, + ); + const store = new MemoryStore(); + await store.saveAccessData(accessData); + await checkPinTest(store); + await checkPasswdTest(store); + }); + + it('should work with leveldb store', async () => { + const seed = walletUtils.generateWalletWords(); + const accessData = walletUtils.generateAccessDataFromSeed( + seed, + { + pin: PINCODE, + password: PASSWD, + networkName: 'testnet', + }, + ); + const store = new LevelDBStore(DATA_DIR, accessData.xpubkey); + await store.saveAccessData(accessData); + await checkPinTest(store); + await checkPasswdTest(store); + }); + + async function checkPinTest(store) { + const storage = new Storage(store); + await expect(storage.checkPin(PINCODE)).resolves.toEqual(true); + await expect(storage.checkPin('0000')).resolves.toEqual(false); + + // No access data should throw + jest.spyOn(storage, 'getAccessData') + .mockReturnValue(Promise.resolve({foo: 'bar'})); + await expect(storage.checkPin('0000')).rejects.toThrow(); + + jest.spyOn(storage, '_getValidAccessData') + .mockReturnValue(Promise.resolve({})); + await expect(storage.checkPin('0000')).rejects.toThrow(); + } + + async function checkPasswdTest(store) { + const storage = new Storage(store); + await expect(storage.checkPassword(PASSWD)).resolves.toEqual(true); + await expect(storage.checkPassword('0000')).resolves.toEqual(false); + + // No access data should throw + jest.spyOn(storage, 'getAccessData') + .mockReturnValue(Promise.resolve({foo: 'bar'})); + await expect(storage.checkPassword('0000')).rejects.toThrow(); + + jest.spyOn(storage, '_getValidAccessData') + .mockReturnValue(Promise.resolve({})); + await expect(storage.checkPassword('0000')).rejects.toThrow(); + } +}); diff --git a/__tests__/utils/crypto.test.ts b/__tests__/utils/crypto.test.ts index 06380c351..d7cedaf57 100644 --- a/__tests__/utils/crypto.test.ts +++ b/__tests__/utils/crypto.test.ts @@ -7,7 +7,7 @@ import CryptoJS from 'crypto-js'; import { DecryptionError, InvalidPasswdError } from '../../src/errors'; -import { hashData, validateHash, encryptData, decryptData } from '../../src/utils/crypto'; +import { hashData, validateHash, encryptData, decryptData, checkPassword } from '../../src/utils/crypto'; test('validateHash', () => { const data = 'a-valid-data'; @@ -63,3 +63,13 @@ test('encryption test', () => { }; expect(() => { decryptData(invalidData, passwd) }).toThrowError(DecryptionError); }); + +test('check password', () => { + const data = 'a-valid-data'; + const passwd = 'a-valid-passwd'; + const invalidPasswd = 'an-invalid-passwd'; + const encrypted = encryptData(data, passwd); + + expect(checkPassword(encrypted, passwd)).toEqual(true) + expect(checkPassword(encrypted, invalidPasswd)).toEqual(false) +}); diff --git a/__tests__/utils/wallet.test.js b/__tests__/utils/wallet.test.js index 064423d1e..e33117d12 100644 --- a/__tests__/utils/wallet.test.js +++ b/__tests__/utils/wallet.test.js @@ -6,13 +6,14 @@ */ import wallet from '../../src/utils/wallet'; -import { XPubError, InvalidWords, UncompressedPubKeyError } from '../../src/errors'; +import { XPubError, InvalidWords, UncompressedPubKeyError, InvalidPasswdError } from '../../src/errors'; import Network from '../../src/models/network'; import Mnemonic from 'bitcore-mnemonic'; import { HD_WALLET_ENTROPY, HATHOR_BIP44_CODE, P2SH_ACCT_PATH } from '../../src/constants'; -import { util, Address, HDPrivateKey, HDPublicKey } from 'bitcore-lib'; +import { util, HDPrivateKey, HDPublicKey } from 'bitcore-lib'; import { hexToBuffer } from '../../src/utils/buffer'; import { WalletType, WALLET_FLAGS } from '../../src/types'; +import { checkPassword } from '../../src/utils/crypto'; test('Words', () => { @@ -547,4 +548,36 @@ test('access data from seed', () => { pubkey: xprivRoot.deriveNonCompliantChild(P2SH_ACCT_PATH).publicKey.toString('hex'), }, }); -}); \ No newline at end of file +}); + + +test('change pin and password', async () => { + const seed = 'upon tennis increase embark dismiss diamond monitor face magnet jungle scout salute rural master shoulder cry juice jeans radar present close meat antenna mind'; + const accessData = wallet.generateAccessDataFromSeed( + seed, + { pin: '123', password: '456', networkName: 'testnet' }, + ); + + // Check the pin and password were used correctly + expect(checkPassword(accessData.words, '456')).toEqual(true); + expect(checkPassword(accessData.mainKey, '123')).toEqual(true); + expect(checkPassword(accessData.authKey, '123')).toEqual(true); + + expect(() => wallet.changeEncryptionPin(accessData, 'invalid-pin', '321')).toThrow(InvalidPasswdError); + expect(() => wallet.changeEncryptionPassword(accessData, 'invalid-passwd', '456')).toThrow(InvalidPasswdError); + + const pinChangedAccessData = wallet.changeEncryptionPin(accessData, '123', '321'); + expect(checkPassword(pinChangedAccessData.words, '456')).toEqual(true); + expect(checkPassword(pinChangedAccessData.mainKey, '321')).toEqual(true); + expect(checkPassword(pinChangedAccessData.authKey, '321')).toEqual(true); + + const passwdChangedAccessData = wallet.changeEncryptionPassword(accessData, '456', '654'); + expect(checkPassword(passwdChangedAccessData.words, '654')).toEqual(true); + expect(checkPassword(passwdChangedAccessData.mainKey, '123')).toEqual(true); + expect(checkPassword(passwdChangedAccessData.authKey, '123')).toEqual(true); + + const bothChangedAccessData = wallet.changeEncryptionPassword(pinChangedAccessData, '456', '654'); + expect(checkPassword(bothChangedAccessData.words, '654')).toEqual(true); + expect(checkPassword(bothChangedAccessData.mainKey, '321')).toEqual(true); + expect(checkPassword(bothChangedAccessData.authKey, '321')).toEqual(true); +}) diff --git a/src/new/wallet.js b/src/new/wallet.js index e80a7e1d6..2e13a256b 100644 --- a/src/new/wallet.js +++ b/src/new/wallet.js @@ -2403,6 +2403,33 @@ class HathorWallet extends EventEmitter { return { success: true, txTokens }; } + + /** + * Check if the pin used to encrypt the main key is valid. + * @param {string} pin + * @returns {Promise} + */ + async checkPin(pin) { + return this.storage.checkPin(pin); + } + + /** + * Check if the password used to encrypt the seed is valid. + * @param {string} password + * @returns {Promise} + */ + async checkPassword(password) { + return this.storage.checkPassword(password); + } + + /** + * @param {string} pin + * @param {string} password + * @returns {Promise} + */ + async checkPinAndPassword(pin, password) { + return await this.checkPin(pin) && await this.checkPassword(password); + } } // State constants. diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 67834d4ef..7802076dd 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -31,9 +31,10 @@ import { import transactionUtils from '../utils/transaction'; import { processHistory, processUtxoUnlock } from '../utils/storage'; import config, { Config } from '../config'; -import { decryptData, encryptData } from '../utils/crypto'; +import { decryptData, checkPassword } from '../utils/crypto'; import FullNodeConnection from '../new/connection'; import { getAddressType } from '../utils/address'; +import walletUtils from '../utils/wallet'; import { HATHOR_TOKEN_CONFIG, MAX_INPUTS, MAX_OUTPUTS, TOKEN_DEPOSIT_PERCENTAGE } from '../constants'; const DEFAULT_ADDRESS_META: IAddressMetadata = { @@ -779,6 +780,40 @@ export class Storage implements IStorage { return this.store.cleanStorage(cleanHistory, cleanAddresses); } + /** + * Check if the pin is correct + * + * @param {string} pinCode - Pin to check + * @returns {Promise} + * @throws {Error} if the wallet is not initialized + * @throws {Error} if the wallet does not have the private key + */ + async checkPin(pinCode: string): Promise { + const accessData = await this._getValidAccessData(); + if (!accessData.mainKey) { + throw new Error('Cannot check pin without the private key.'); + } + + return checkPassword(accessData.mainKey, pinCode); + } + + /** + * Check if the password is correct + * + * @param {string} password - Password to check + * @returns {Promise} + * @throws {Error} if the wallet is not initialized + * @throws {Error} if the wallet does not have the private key + */ + async checkPassword(password: string): Promise { + const accessData = await this._getValidAccessData(); + if (!accessData.words) { + throw new Error('Cannot check password without the words.'); + } + + return checkPassword(accessData.words, password); + } + /** * Change the wallet pin. * @param {string} oldPin Old pin to unlock data. @@ -787,30 +822,11 @@ export class Storage implements IStorage { */ async changePin(oldPin: string, newPin: string): Promise { const accessData = await this._getValidAccessData(); - if (!(accessData.mainKey || accessData.authKey || accessData.acctPathKey)) { - throw new Error('No data to change'); - } - if (accessData.mainKey) { - const mainKey = decryptData(accessData.mainKey, oldPin); - const newEncryptedMainKey = encryptData(mainKey, newPin); - accessData.mainKey = newEncryptedMainKey; - } - - if (accessData.authKey) { - const authKey = decryptData(accessData.authKey, oldPin); - const newEncryptedAuthKey = encryptData(authKey, newPin); - accessData.authKey = newEncryptedAuthKey; - } - - if (accessData.acctPathKey) { - const acctKey = decryptData(accessData.acctPathKey, oldPin); - const newEncryptedAcctKey = encryptData(acctKey, newPin); - accessData.acctPathKey = newEncryptedAcctKey; - } + const newAccessData = walletUtils.changeEncryptionPin(accessData, oldPin, newPin); // Save the changes made - await this.saveAccessData(accessData); + await this.saveAccessData(newAccessData); } /** @@ -822,16 +838,11 @@ export class Storage implements IStorage { */ async changePassword(oldPassword: string, newPassword: string): Promise { const accessData = await this._getValidAccessData(); - if (!accessData.words) { - throw new Error('No data to change.'); - } - const words = decryptData(accessData.words, oldPassword); - const newEncryptedWords = encryptData(words, newPassword); - accessData.words = newEncryptedWords; + const newAccessData = walletUtils.changeEncryptionPassword(accessData, oldPassword, newPassword); // Save the changes made - await this.saveAccessData(accessData); + await this.saveAccessData(newAccessData); } /** diff --git a/src/types.ts b/src/types.ts index f76057fd1..7def79765 100644 --- a/src/types.ts +++ b/src/types.ts @@ -392,6 +392,8 @@ export interface IStorage { cleanStorage(cleanHistory?: boolean, cleanAddresses?: boolean): Promise; handleStop(options: {connection?: FullNodeConnection, cleanStorage?: boolean, cleanAddresses?: boolean}): Promise; getTokenDepositPercentage(): number; + checkPin(pinCode: string): Promise; + checkPassword(password: string): Promise; } /** diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index cf96281e7..571855b35 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -140,3 +140,19 @@ export function validateHash( const hash = hashData(dataToValidate, { salt, iterations, pbkdf2Hasher }); return hash.hash === hashedData; } + +/** + * Check that the given password was used to encrypt the given data. + * @param {IEncryptedData} data The encrypted data. + * @param {string} password The password we want to check against the data. + * + * @returns {boolean} + */ +export function checkPassword(data: IEncryptedData, password: string): boolean { + const options = { + salt: data.salt, + iterations: data.iterations, + pbkdf2Hasher: data.pbkdf2Hasher, + }; + return validateHash(password, data.hash, options); +} diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts index 3ca1b272b..a28b35a64 100644 --- a/src/utils/wallet.ts +++ b/src/utils/wallet.ts @@ -15,8 +15,8 @@ import Network from '../models/network'; import _ from 'lodash'; import helpers from './helpers'; -import { IMultisigData, IWalletAccessData, WalletType, WALLET_FLAGS } from '../types'; -import { encryptData } from './crypto'; +import { IEncryptedData, IMultisigData, IWalletAccessData, WalletType, WALLET_FLAGS } from '../types'; +import { encryptData, decryptData } from './crypto'; const wallet = { @@ -587,6 +587,64 @@ const wallet = { walletFlags: 0, }; }, + + /** + * Change the encryption pin on the fields that are encrypted using the pin. + * Will not save the access data, only return the new access data. + * + * @param {IWalletAccessData} accessData The current access data encrypted with `oldPin`. + * @param {string} oldPin Used to decrypt the old access data. + * @param {string} newPin Encrypt the fields with this pin. + * @returns {IWalletAccessData} The access data with fields encrypted with `newPin`. + */ + changeEncryptionPin(accessData: IWalletAccessData, oldPin: string, newPin: string): IWalletAccessData { + const data = _.cloneDeep(accessData); + if (!(data.mainKey || data.authKey || data.acctPathKey)) { + throw new Error('No data to change'); + } + + if (data.mainKey) { + const mainKey = decryptData(data.mainKey, oldPin); + const newEncryptedMainKey = encryptData(mainKey, newPin); + data.mainKey = newEncryptedMainKey; + } + + if (data.authKey) { + const authKey = decryptData(data.authKey, oldPin); + const newEncryptedAuthKey = encryptData(authKey, newPin); + data.authKey = newEncryptedAuthKey; + } + + if (data.acctPathKey) { + const acctPathKey = decryptData(data.acctPathKey, oldPin); + const newEncryptedAcctPathKey = encryptData(acctPathKey, newPin); + data.acctPathKey = newEncryptedAcctPathKey; + } + + return data; + }, + + /** + * Change the encryption password on the seed. + * Will not save the access data, only return the new access data. + * + * @param {IWalletAccessData} accessData The current access data encrypted with `oldPassword`. + * @param {string} oldPassword Used to decrypt the old access data. + * @param {string} newPassword Encrypt the seed with this password. + * @returns {IWalletAccessData} The access data with fields encrypted with `newPassword`. + */ + changeEncryptionPassword(accessData: IWalletAccessData, oldPassword: string, newPassword: string): IWalletAccessData { + const data = _.cloneDeep(accessData); + if (!data.words) { + throw new Error('No data to change'); + } + + const words = decryptData(data.words, oldPassword); + const newEncryptedWords = encryptData(words, newPassword); + data.words = newEncryptedWords; + + return data; + }, } export default wallet; diff --git a/src/wallet/types.ts b/src/wallet/types.ts index 186f4f406..6a1b6bebf 100644 --- a/src/wallet/types.ts +++ b/src/wallet/types.ts @@ -308,7 +308,7 @@ export interface IHathorWallet { options: DestroyAuthorityOptions, ): Promise; destroyAuthority( - token: string, + token: string, type: string, count: number, options: DestroyAuthorityOptions, @@ -330,6 +330,9 @@ export interface IHathorWallet { graphType: string, maxLevel: number, ): Promise; + checkPin(pin: string): Promise; + checkPassword(password: string): Promise; + checkPinAndPassword(pin: string, password: string): Promise; } export interface ISendTransaction { diff --git a/src/wallet/wallet.ts b/src/wallet/wallet.ts index 9e2dbf535..8568556d8 100644 --- a/src/wallet/wallet.ts +++ b/src/wallet/wallet.ts @@ -1795,6 +1795,33 @@ class HathorWalletServiceWallet extends EventEmitter implements IHathorWallet { isWsEnabled(): boolean { return this._isWsEnabled; } + + /** + * Check if the pin used to encrypt the main key is valid. + * @param {string} pin + * @returns {Promise} + */ + async checkPin(pin: string): Promise { + return this.storage.checkPin(pin); + } + + /** + * Check if the password used to encrypt the seed is valid. + * @param {string} password + * @returns {Promise} + */ + async checkPassword(password: string): Promise { + return this.storage.checkPassword(password); + } + + /** + * @param {string} pin + * @param {string} password + * @returns {Promise} + */ + async checkPinAndPassword(pin: string, password: string): Promise { + return await this.checkPin(pin) && await this.checkPassword(password); + } } export default HathorWalletServiceWallet;