Skip to content

Commit

Permalink
added auth service tests
Browse files Browse the repository at this point in the history
  • Loading branch information
larryrider committed Feb 14, 2024
1 parent c5ff9cf commit 7f8f869
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 72 deletions.
30 changes: 6 additions & 24 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { CryptoProvider, LoginDetails } from '@internxt/sdk';
import { Keys, Password } from '@internxt/sdk/dist/auth';
import { LoginDetails } from '@internxt/sdk';
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings.js';
import { SdkManager } from './SDKManager.service';
import { KeysService } from './keys.service';
import { decryptText, decryptTextWithKey, encryptText, passToHash } from '../utils/crypto.utils';
import { CryptoService } from './crypto.service';

export class AuthService {
public static readonly instance: AuthService = new AuthService();
Expand All @@ -18,33 +17,16 @@ export class AuthService {
newToken: string;
mnemonic: string;
}> => {
const authClient = SdkManager.getInstance().auth;
const authClient = SdkManager.instance.getAuth();
const loginDetails: LoginDetails = {
email: email.toLowerCase(),
password: password,
tfaCode: twoFactorCode,
};
const cryptoProvider: CryptoProvider = {
encryptPasswordHash(password: Password, encryptedSalt: string): string {
const salt = decryptText(encryptedSalt);
const hashObj = passToHash({ password, salt });
return encryptText(hashObj.hash);
},
async generateKeys(password: Password): Promise<Keys> {
const { privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } =
await KeysService.instance.generateNewKeysWithEncrypted(password);
const keys: Keys = {
privateKeyEncrypted: privateKeyArmoredEncrypted,
publicKey: publicKeyArmored,
revocationCertificate: revocationCertificate,
};
return keys;
},
};

// eslint-disable-next-line no-useless-catch
try {
const data = await authClient.login(loginDetails, cryptoProvider);
const data = await authClient.login(loginDetails, CryptoService.cryptoProvider);
const { user, token, newToken } = data;
const { privateKey, publicKey } = user;

Expand All @@ -60,7 +42,7 @@ export class AuthService {
);
}

const clearMnemonic = decryptTextWithKey(user.mnemonic, password);
const clearMnemonic = CryptoService.instance.decryptTextWithKey(user.mnemonic, password);
const clearUser = {
...user,
mnemonic: clearMnemonic,
Expand All @@ -79,7 +61,7 @@ export class AuthService {
};

public is2FANeeded = async (email: string): Promise<boolean> => {
const authClient = SdkManager.getInstance().auth;
const authClient = SdkManager.instance.getAuth();
const securityDetails = await authClient.securityDetails(email).catch((error) => {
throw new Error(error.message ?? 'Login error');
});
Expand Down
71 changes: 71 additions & 0 deletions src/services/crypto.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CryptoProvider } from '@internxt/sdk';
import { Keys, Password } from '@internxt/sdk/dist/auth';
import { KeysService } from './keys.service';
import { ConfigService } from '../services/config.service';

interface PassObjectInterface {
salt?: string | null;
password: string;
}

export class CryptoService {
public static readonly instance: CryptoService = new CryptoService();

public static readonly cryptoProvider: CryptoProvider = {
encryptPasswordHash(password: Password, encryptedSalt: string): string {
const salt = CryptoService.instance.decryptText(encryptedSalt);
const hashObj = CryptoService.instance.passToHash({ password, salt });
return CryptoService.instance.encryptText(hashObj.hash);
},
async generateKeys(password: Password): Promise<Keys> {
const { privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } =
await KeysService.instance.generateNewKeysWithEncrypted(password);
const keys: Keys = {
privateKeyEncrypted: privateKeyArmoredEncrypted,
publicKey: publicKeyArmored,
revocationCertificate: revocationCertificate,
};
return keys;
},
};

// Method to hash password. If salt is passed, use it, in other case use crypto lib for generate salt
public passToHash = (passObject: PassObjectInterface): { salt: string; hash: string } => {
const salt = passObject.salt ? CryptoJS.enc.Hex.parse(passObject.salt) : CryptoJS.lib.WordArray.random(128 / 8);
const hash = CryptoJS.PBKDF2(passObject.password, salt, { keySize: 256 / 32, iterations: 10000 });
const hashedObjetc = {
salt: salt.toString(),
hash: hash.toString(),
};

return hashedObjetc;
};

// AES Plain text encryption method
public encryptText = (textToEncrypt: string): string => {
const APP_CRYPTO_SECRET = ConfigService.instance.get('APP_CRYPTO_SECRET');
return this.encryptTextWithKey(textToEncrypt, APP_CRYPTO_SECRET);
};

// AES Plain text decryption method
public decryptText = (encryptedText: string): string => {
const APP_CRYPTO_SECRET = ConfigService.instance.get('APP_CRYPTO_SECRET');
return this.decryptTextWithKey(encryptedText, APP_CRYPTO_SECRET);
};

// AES Plain text encryption method with enc. key
public encryptTextWithKey = (textToEncrypt: string, keyToEncrypt: string): string => {
const bytes = CryptoJS.AES.encrypt(textToEncrypt, keyToEncrypt).toString();
const text64 = CryptoJS.enc.Base64.parse(bytes);

return text64.toString(CryptoJS.enc.Hex);
};

// AES Plain text decryption method with enc. key
public decryptTextWithKey = (encryptedText: string, keyToDecrypt: string): string => {
const reb = CryptoJS.enc.Hex.parse(encryptedText);
const bytes = CryptoJS.AES.decrypt(reb.toString(CryptoJS.enc.Base64), keyToDecrypt);

return bytes.toString(CryptoJS.enc.Utf8);
};
}
47 changes: 0 additions & 47 deletions src/utils/crypto.utils.ts

This file was deleted.

27 changes: 27 additions & 0 deletions test/fixtures/auth.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import crypto from 'crypto';

export const UserFixture = {
userId: crypto.randomBytes(16).toString('hex'),
uuid: crypto.randomBytes(16).toString('hex'),
email: crypto.randomBytes(16).toString('hex'),
name: crypto.randomBytes(16).toString('hex'),
lastname: crypto.randomBytes(16).toString('hex'),
username: crypto.randomBytes(16).toString('hex'),
bridgeUser: crypto.randomBytes(16).toString('hex'),
bucket: crypto.randomBytes(16).toString('hex'),
backupsBucket: crypto.randomBytes(16).toString('hex'),
root_folder_id: crypto.randomInt(1, 9999),
sharedWorkspace: false,
credit: crypto.randomInt(1, 9999),
mnemonic: crypto.randomBytes(16).toString('hex'),
privateKey: crypto.randomBytes(16).toString('hex'),
publicKey: crypto.randomBytes(16).toString('hex'),
revocationKey: crypto.randomBytes(16).toString('hex'),
teams: false,
appSumoDetails: null,
registerCompleted: true,
hasReferralsProgram: false,
createdAt: new Date(),
avatar: crypto.randomBytes(16).toString('hex'),
emailVerified: true,
};
100 changes: 100 additions & 0 deletions test/services/auth.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { expect } from 'chai';
import sinon, { SinonSandbox } from 'sinon';
import crypto from 'crypto';
import { AuthService } from '../../src/services/auth.service';
import { KeysService } from '../../src/services/keys.service';
import { CryptoService } from '../../src/services/crypto.service';
import { SdkManager } from '../../src/services/SDKManager.service';
import { UserFixture } from '../fixtures/auth.fixture';
import { Auth, LoginDetails, SecurityDetails } from '@internxt/sdk';

describe('Auth service', () => {
let authServiceSandbox: SinonSandbox;

beforeEach(() => {
authServiceSandbox = sinon.createSandbox();
});

afterEach(function () {
authServiceSandbox.restore();
});

it('When user logs in, then login user credentials are generated', async () => {
const loginResponse = {
token: crypto.randomBytes(16).toString('hex'),
newToken: crypto.randomBytes(16).toString('hex'),
user: UserFixture,
userTeam: null,
};

authServiceSandbox.stub(Auth.prototype, 'login').returns(Promise.resolve(loginResponse));
authServiceSandbox.stub(SdkManager.instance, 'getAuth').returns(Auth.prototype);
authServiceSandbox.stub(KeysService.instance, 'decryptPrivateKey').returns(loginResponse.user.privateKey);
authServiceSandbox.stub(KeysService.instance, 'assertPrivateKeyIsValid').resolves();
authServiceSandbox.stub(KeysService.instance, 'assertValidateKeys').resolves();
authServiceSandbox.stub(CryptoService.instance, 'decryptTextWithKey').returns(loginResponse.user.mnemonic);

const responseLogin = await AuthService.instance.doLogin(
loginResponse.user.email,
crypto.randomBytes(16).toString('hex'),
'',
);
const expectedResponseLogin = {
user: { ...loginResponse.user, privateKey: Buffer.from(loginResponse.user.privateKey).toString('base64') },
token: loginResponse.token,
newToken: loginResponse.newToken,
mnemonic: loginResponse.user.mnemonic,
};
expect(responseLogin).to.eql(expectedResponseLogin);
});

it('When user logs in and credentials are not correct, then an error is thrown', async () => {
const loginDetails: LoginDetails = {
email: crypto.randomBytes(16).toString('hex'),
password: crypto.randomBytes(8).toString('hex'),
tfaCode: crypto.randomInt(1, 999999).toString().padStart(6, '0'),
};

authServiceSandbox.stub(Auth.prototype, 'login').withArgs(loginDetails, CryptoService.cryptoProvider).rejects();
authServiceSandbox.stub(SdkManager.instance, 'getAuth').returns(Auth.prototype);

try {
await AuthService.instance.doLogin(loginDetails.email, loginDetails.password, loginDetails.tfaCode || '');
expect(false).to.be.true; //should throw error
} catch {
/* no op */
}
});

it('When two factor authentication property is enabled at securityDetails endpoint, then it is returned from is2FANeeded functionality', async () => {
const email = crypto.randomBytes(16).toString('hex');
const securityDetails: SecurityDetails = {
encryptedSalt: crypto.randomBytes(16).toString('hex'),
tfaEnabled: true,
};

authServiceSandbox
.stub(Auth.prototype, 'securityDetails')
.withArgs(email)
.returns(Promise.resolve(securityDetails));
authServiceSandbox.stub(SdkManager.instance, 'getAuth').returns(Auth.prototype);

const responseLogin = await AuthService.instance.is2FANeeded(email);

expect(responseLogin).to.equal(securityDetails.tfaEnabled);
});

it('When email is not correct when checking two factor authentication property, then an error is thrown', async () => {
const email = crypto.randomBytes(16).toString('hex');

authServiceSandbox.stub(Auth.prototype, 'securityDetails').withArgs(email).rejects();
authServiceSandbox.stub(SdkManager.instance, 'getAuth').returns(Auth.prototype);

try {
await AuthService.instance.is2FANeeded(email);
expect(false).to.be.true; //should throw error
} catch {
/* no op */
}
});
});
4 changes: 3 additions & 1 deletion test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{
"extends": "../tsconfig",
"compilerOptions": {
"noEmit": true
"noEmit": true,
"rootDir": "."
},
"include": ["**/*"],
"references": [
{"path": ".."}
]
Expand Down

0 comments on commit 7f8f869

Please sign in to comment.