testing account module

This commit is contained in:
balzack 2024-08-27 22:24:27 -07:00
parent eea9583968
commit 3d9396029a
7 changed files with 148 additions and 24 deletions

View File

@ -49,13 +49,16 @@ export class MockAccountModule implements Account {
public async confirmMFA(code: string): Promise<void> {
}
public async setAccountSeal(password: string): Promise<void> {
public async setSeal(password: string): Promise<void> {
}
public async clearAccountSeal(): Promise<void> {
public async clearSeal(): Promise<void> {
}
public async unlockAccountSeal(password: string): Promise<void> {
public async unlockSeal(password: string): Promise<void> {
}
public async forgetSeal(): Promise<void> {
}
public async setLogin(username: string, password: string): Promise<void> {

View File

@ -0,0 +1,104 @@
import { AccountModule } from '../src/account';
import { NoStore } from '../src/store';
import { Crypto } from '../src/crypto';
import { ConsoleLogging } from '../src/logging';
import { defaultAccountEntity } from '../src/entities';
import { AccountStatus } from '../src/types';
import { waitFor } from '../__mocks__/waitFor';
import axios from 'redaxios';
const testStatus = JSON.parse(JSON.stringify(defaultAccountEntity));
jest.mock('redaxios', () => {
return {
get: jest.fn().mockImplementation(() => {
testStatus.storageUsed = 2;
return Promise.resolve({ status: 200, data: testStatus });
}),
put: jest.fn().mockImplementation((url, body) => {
if (url == 'http://test_url/account/notification?agent=test_token') {
testStatus.pushEnabled = body;
}
if (url == 'http://test_url/account/searchable?agent=test_token') {
testStatus.searchable = body;
}
if (url == 'http://test_url/account/seal?agent=test_token') {
testStatus.seal = body;
}
return Promise.resolve({ status: 200 });
}),
}
})
class TestCrypto implements Crypto {
public pbkdfSalt() {
return { saltHex: 'SALT_HEX' }
}
public pbkdfKey(saltHex: string, password: string) {
return { aesKeyHex: 'AES_KEY_HEX' }
}
public aesKey() {
return { aesKeyHex: 'AES_KEY_HEX' };
}
public aesIv() {
return { ivHex: 'IV_HEX' };
}
public aesEncrypt(data: string, ivHex: string, aesKeyHex: string) {
return { encryptedDataB64: 'ENCRYPTED_DATA_B64' };
}
public aesDecrypt(encryptedDataB64: string, ivHex: string, aesKeyHex: string) {
return { data: 'DATA' }
}
public rsaKey() {
return { publicKeyB64: 'PUBLIC_KEY_B64', privateKeyB64: 'PRIVATE_KEY_B64' };
}
public rsaEncrypt(data: string, publicKeyB64: string) {
return { encryptedDataB64: 'ENCRYPTED_DATA_B64' }
}
public rsaDecrypt(encryptedDataB64: string, privateKeyB64: string) {
return { data: 'DATA' }
}
}
class TestStore extends NoStore {
public async getProfileRevision(): Promise<number> {
return 4;
}
}
test('allocates session correctly', async () => {
let status: AccountStatus | null = null;
const log = new ConsoleLogging();
const store = new TestStore();
const crypto = new TestCrypto();
const account = new AccountModule(log, store, crypto, 'test_guid', 'test_token', 'test_url', false);
account.addStatusListener((ev: AccountStatus) => { status = ev });
account.setRevision(5);
await waitFor(() => (status?.storageUsed == 2));
account.enableRegistry();
account.setRevision(6);
await waitFor(() => Boolean(status?.searchable));
account.disableRegistry();
account.setRevision(7);
await waitFor(() => !Boolean(status?.searchable));
account.enableNotifications();
account.setRevision(8);
await waitFor(() => Boolean(status?.pushEnabled));
account.disableNotifications();
account.setRevision(9);
await waitFor(() => !Boolean(status?.pushEnabled));
account.setSeal('password');
account.setRevision(10);
await waitFor(() => Boolean(status?.sealSet));
});

View File

@ -1,7 +1,9 @@
import { EventEmitter } from 'eventemitter3';
import type { Account, Logging } from './api';
import type { Account } from './api';
import type { AccountStatus } from './types';
import { Store } from './store';
import { Crypto } from './crypto';
import { Logging } from './logging';
import { defaultAccountEntity, AccountEntity } from './entities';
import { getAccountStatus } from './net/getAccountStatus';
import { addAccountMFAuth } from './net/addAccountMFAuth';
@ -12,7 +14,6 @@ import { setAccountNotifications } from './net/setAccountNotifications';
import { setAccountSearchable } from './net/setAccountSearchable';
import { setAccountSeal } from './net/setAccountSeal';
import { clearAccountSeal } from './net/clearAccountSeal';
import { Crypto } from './crypto';
const CLOSE_POLL_MS = 100;
const RETRY_POLL_MS = 2000;
@ -25,6 +26,7 @@ export class AccountModule implements Account {
private node: string;
private secure: boolean;
private log: Logging;
private store: Store;
private crypto: Crypto | null;
private syncing: boolean;
private closing: boolean;
@ -35,11 +37,13 @@ export class AccountModule implements Account {
constructor(log: Logging, store: Store, crypto: Crypto | null, guid: string, token: string, node: string, secure: boolean) {
this.log = log;
this.store = store;
this.crypto = crypto;
this.emitter = new EventEmitter();
this.guid = guid;
this.token = token;
this.node = node;
this.seal = null;
this.sealKey = null;
this.secure = secure;
this.revision = 0;
this.entity = defaultAccountEntity;
@ -52,7 +56,7 @@ export class AccountModule implements Account {
private async init() {
this.revision = await this.store.getAccountRevision(this.guid);
this.entity = await this.store.getAccountData(this.guid);
this.seal = await this.store.getSeal(this.guid);
this.sealKey = await this.store.getSeal(this.guid);
this.syncing = false;
await this.sync();
}
@ -69,7 +73,7 @@ export class AccountModule implements Account {
try {
const { guid, node, secure, token } = this;
const status = await getAccountStatus(node, secure, token);
await this.store.setAccountStatus(guid, status);
await this.store.setAccountData(guid, status);
await this.store.setAccountRevision(guid, nextRev);
this.entity = status;
this.emitter.emit('status', this.getStatus());
@ -90,9 +94,11 @@ export class AccountModule implements Account {
}
public getStatus() {
const { storageUsed, storageAvailable, forwardingAddress, searchable, allowUnseaed, pushEnabled, sealable, seal, enableIce, multiFactorAuth, webPushKey } = this.entity;
const sealSet = this.seal && seal && this.seal.publicKey == seal.publicKey && this.seal.privateKey
return { storageUsed, storageAvailable, forwardingAddress, searchable, allowUnsealed, pushEnabled, sealable, sealSet, enableIce, multiFactorAuth, webPushKey };
const { storageUsed, storageAvailable, forwardingAddress, searchable, allowUnsealed, pushEnabled, sealable, seal, enableIce, multiFactorAuth, webPushKey } = this.entity;
const { passwordSalt, privateKeyIv, privateKeyEncrypted, publicKey } = seal || {};
const sealSet = Boolean(passwordSalt && privateKeyIv && privateKeyEncrypted && publicKey);
const sealUnlocked = Boolean(sealSet && this.sealKey?.privateKey && this.sealKey?.publicKey == publicKey)
return { storageUsed, storageAvailable, forwardingAddress, searchable, allowUnsealed, pushEnabled, sealable, sealSet, sealUnlocked, enableIce, multiFactorAuth, webPushKey };
}
public addStatusListener(ev: (status: AccountStatus) => void): void {
@ -104,7 +110,7 @@ export class AccountModule implements Account {
this.emitter.off('status', ev);
}
public async close(): void {
public async close(): Promise<void> {
this.closing = true;
while(this.syncing) {
await new Promise(r => setTimeout(r, CLOSE_POLL_MS));
@ -152,7 +158,7 @@ export class AccountModule implements Account {
await setAccountMFAuth(node, secure, token, code);
}
public async setAccountSeal(password: string): Promise<void> {
public async setSeal(password: string): Promise<void> {
const { crypto, guid, node, secure, token } = this;
if (!crypto) {
throw new Error('crypto not enabled');
@ -168,19 +174,20 @@ export class AccountModule implements Account {
const seal = { publicKey: publicKeyB64, privateKey: privateKeyB64 };
this.store.setSeal(guid, seal);
this.seal = seal;
this.sealKey = seal;
this.emitter.emit('status', this.getStatus());
}
public async clearAccountSeal(): Promise<void> {
public async clearSeal(): Promise<void> {
const { guid, node, secure, token } = this;
await this.store.clearAccountSeal(guid, node, secure, token);
this.seal = null;
await clearAccountSeal(node, secure, token);
await this.store.clearSeal(guid);
this.sealKey = null;
this.emitter.emit('status', this.getStatus());
}
public async unlockAccountSeal(password: string): Promise<void> {
public async unlockSeal(password: string): Promise<void> {
const { guid, entity, crypto } = this;
const { passwordSalt, privateKeyIv, privateKeyEncrypted, publicKey } = entity.seal;
if (!passwordSalt || !privateKeyIv || !privateKeyEncrypted || !publicKey) {
@ -194,11 +201,18 @@ export class AccountModule implements Account {
const seal = { publicKey: publicKey, privateKey: data };
this.store.setSeal(guid, seal);
this.seal = seal;
this.sealKey = seal;
this.emitter.emit('status', this.getStatus());
}
public async forgetSeal(): Promise<void> {
const { guid } = this;
await this.store.clearSeal(guid);
this.sealKey = null;
this.emitter.emit('status', this.getStatus());
}
public async setLogin(username: string, password: string): Promise<void> {
const { node, secure, token } = this;
await setAccountLogin(node, secure, token, username, password);

View File

@ -91,9 +91,10 @@ export interface Account {
enableMFA(): Promise<{ secretImage: string, secretText: string }>;
disableMFA(): Promise<void>;
confirmMFA(code: string): Promise<void>;
setAccountSeal(password: string): Promise<void>;
clearAccountSeal(): Promise<void>;
unlockAccountSeal(password: string): Promise<void>;
setSeal(password: string): Promise<void>;
clearSeal(): Promise<void>;
unlockSeal(password: string): Promise<void>;
forgetSeal(): Promise<void>;
addStatusListener(ev: (status: AccountStatus) => void): void;
removeStatusListener(ev: (status: AccountStatus) => void): void;

View File

@ -1,10 +1,11 @@
import axios from 'redaxios';
export async function addAccountMFAuth(node: string, secure: boolean, token: string): { text: string, image: string } {
export async function addAccountMFAuth(node: string, secure: boolean, token: string): Promise<{ text: string, image: string }> {
const endpoint = `http${secure ? 's' : ''}://${node}/account/mfauth=${token}`;
const response = await axios.post(endpoint);
if (response.status >= 400 && response.status < 600) {
throw new Error('setAccountMFAuth failed');
}
return response.data;
}

View File

@ -1,6 +1,6 @@
import axios from 'redaxios';
export async function setAccountSeal(node: string, secure: boolean, token: string) {
export async function clearAccountSeal(node: string, secure: boolean, token: string) {
const endpoint = `http${secure ? 's' : ''}://${node}/account/seal?agent=${token}`;
const response = await axios.delete(endpoint);
if (response.status >= 400 && response.status < 600) {

View File

@ -145,6 +145,7 @@ export type AccountStatus = {
pushEnabled: boolean,
sealable: boolean,
sealSet: boolean,
sealUnlocked: boolean,
enableIce: boolean,
multiFactorAuth: boolean,
webPushKey: string,