Skip to content

Commit

Permalink
check pin method (#513)
Browse files Browse the repository at this point in the history
* feat: check pin and password methods
  • Loading branch information
r4mmer authored May 15, 2023
1 parent b132413 commit 83e64f0
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 37 deletions.
76 changes: 76 additions & 0 deletions __tests__/new/hathorwallet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
69 changes: 68 additions & 1 deletion __tests__/storage/storage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
}
});
12 changes: 11 additions & 1 deletion __tests__/utils/crypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)
});
39 changes: 36 additions & 3 deletions __tests__/utils/wallet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -547,4 +548,36 @@ test('access data from seed', () => {
pubkey: xprivRoot.deriveNonCompliantChild(P2SH_ACCT_PATH).publicKey.toString('hex'),
},
});
});
});


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);
})
27 changes: 27 additions & 0 deletions src/new/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>}
*/
async checkPin(pin) {
return this.storage.checkPin(pin);
}

/**
* Check if the password used to encrypt the seed is valid.
* @param {string} password
* @returns {Promise<boolean>}
*/
async checkPassword(password) {
return this.storage.checkPassword(password);
}

/**
* @param {string} pin
* @param {string} password
* @returns {Promise<boolean>}
*/
async checkPinAndPassword(pin, password) {
return await this.checkPin(pin) && await this.checkPassword(password);
}
}

// State constants.
Expand Down
69 changes: 40 additions & 29 deletions src/storage/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<boolean>}
* @throws {Error} if the wallet is not initialized
* @throws {Error} if the wallet does not have the private key
*/
async checkPin(pinCode: string): Promise<boolean> {
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<boolean>}
* @throws {Error} if the wallet is not initialized
* @throws {Error} if the wallet does not have the private key
*/
async checkPassword(password: string): Promise<boolean> {
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.
Expand All @@ -787,30 +822,11 @@ export class Storage implements IStorage {
*/
async changePin(oldPin: string, newPin: string): Promise<void> {
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);
}

/**
Expand All @@ -822,16 +838,11 @@ export class Storage implements IStorage {
*/
async changePassword(oldPassword: string, newPassword: string): Promise<void> {
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);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,8 @@ export interface IStorage {
cleanStorage(cleanHistory?: boolean, cleanAddresses?: boolean): Promise<void>;
handleStop(options: {connection?: FullNodeConnection, cleanStorage?: boolean, cleanAddresses?: boolean}): Promise<void>;
getTokenDepositPercentage(): number;
checkPin(pinCode: string): Promise<boolean>;
checkPassword(password: string): Promise<boolean>;
}

/**
Expand Down
Loading

0 comments on commit 83e64f0

Please sign in to comment.