diff --git a/src/services/SDKManager.service.ts b/src/services/SDKManager.service.ts index 596e242..206f9a6 100644 --- a/src/services/SDKManager.service.ts +++ b/src/services/SDKManager.service.ts @@ -18,38 +18,38 @@ export class SdkManager { * Sets the security details needed to create SDK clients * @param apiSecurity Security properties to be setted */ - static init(apiSecurity: SdkManagerApiSecurity) { + static init = (apiSecurity: SdkManagerApiSecurity) => { SdkManager.setApiSecurity(apiSecurity); - } + }; - static setApiSecurity(apiSecurity: SdkManagerApiSecurity) { + static setApiSecurity = (apiSecurity: SdkManagerApiSecurity) => { SdkManager.apiSecurity = apiSecurity; - } + }; - static clean() { + static clean = () => { SdkManager.apiSecurity = undefined; - } + }; - static getInstance() { + static getInstance = () => { if (!SdkManager.instance) { throw new Error('No instance found, call init method first'); } return SdkManager.instance; - } + }; - public getApiSecurity(config = { throwErrorOnMissingCredentials: true }): SdkManagerApiSecurity { + public getApiSecurity = (config = { throwErrorOnMissingCredentials: true }): SdkManagerApiSecurity => { if (!SdkManager.apiSecurity && config.throwErrorOnMissingCredentials) throw new Error('Api security properties not found in SdkManager'); return SdkManager.apiSecurity as SdkManagerApiSecurity; - } + }; - private static getAppDetails(): AppDetails { + private static getAppDetails = (): AppDetails => { return { clientName: packageJson.name, clientVersion: packageJson.version, }; - } + }; /** Auth SDK */ get authV2() { diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index a2873a3..5269e39 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,78 +1,61 @@ import { CryptoProvider, LoginDetails } from '@internxt/sdk'; import { Keys, Password } from '@internxt/sdk/dist/auth'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings.js'; -import { aes } from '@internxt/lib'; import { SdkManager } from './SDKManager.service'; +import { KeysService } from './keys.service'; +import { OpenpgpService } from './pgp.service'; import { decryptText, decryptTextWithKey, encryptText, passToHash } from '../utils/crypto.utils'; -import { generateNewKeys } from './pgp.service'; -import { assertPrivateKeyIsValid, assertValidateKeys, decryptPrivateKey, getAesInitFromEnv } from './keys.service'; -const generateNewKeysWithEncrypted = async (password: string) => { - const { privateKeyArmored, publicKeyArmored, revocationCertificate } = await generateNewKeys(); +export class AuthService { + public static readonly instance: AuthService = new AuthService(); - return { - privateKeyArmored, - privateKeyArmoredEncrypted: aes.encrypt(privateKeyArmored, password, getAesInitFromEnv()), - publicKeyArmored, - revocationCertificate, - }; -}; - -export const is2FANeeded = async (email: string): Promise => { - const authClient = SdkManager.getInstance().auth; - const securityDetails = await authClient.securityDetails(email).catch((error) => { - throw new Error(error.message ?? 'Login error'); - }); - return securityDetails.tfaEnabled; -}; - -export const doLogin = async ( - email: string, - password: string, - twoFactorCode: string, -): Promise<{ - user: UserSettings; - token: string; - newToken: string; - mnemonic: string; -}> => { - const authClient = SdkManager.getInstance().auth; - 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 { - const { privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } = - await generateNewKeysWithEncrypted(password); - const keys: Keys = { - privateKeyEncrypted: privateKeyArmoredEncrypted, - publicKey: publicKeyArmored, - revocationCertificate: revocationCertificate, - }; - return keys; - }, - }; + public doLogin = async ( + email: string, + password: string, + twoFactorCode: string, + ): Promise<{ + user: UserSettings; + token: string; + newToken: string; + mnemonic: string; + }> => { + const authClient = SdkManager.getInstance().auth; + 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 { + const { privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } = + await OpenpgpService.instance.generateNewKeysWithEncrypted(password); + const keys: Keys = { + privateKeyEncrypted: privateKeyArmoredEncrypted, + publicKey: publicKeyArmored, + revocationCertificate: revocationCertificate, + }; + return keys; + }, + }; - return authClient - .login(loginDetails, cryptoProvider) - .then(async (data) => { + // eslint-disable-next-line no-useless-catch + try { + const data = await authClient.login(loginDetails, cryptoProvider); const { user, token, newToken } = data; const { privateKey, publicKey } = user; const plainPrivateKeyInBase64 = privateKey - ? Buffer.from(decryptPrivateKey(privateKey, password)).toString('base64') + ? Buffer.from(KeysService.instance.decryptPrivateKey(privateKey, password)).toString('base64') : ''; if (privateKey) { - await assertPrivateKeyIsValid(privateKey, password); - await assertValidateKeys( + await KeysService.instance.assertPrivateKeyIsValid(privateKey, password); + await KeysService.instance.assertValidateKeys( Buffer.from(plainPrivateKeyInBase64, 'base64').toString(), Buffer.from(publicKey, 'base64').toString(), ); @@ -84,20 +67,23 @@ export const doLogin = async ( mnemonic: clearMnemonic, privateKey: plainPrivateKeyInBase64, }; - - //TODO save tokens for later use - /*localStorageService.set('xToken', token); - localStorageService.set('xMnemonic', clearMnemonic); - localStorageService.set('xNewToken', newToken);*/ - return { user: clearUser, token: token, newToken: newToken, mnemonic: clearMnemonic, }; - }) - .catch((error) => { + } catch (error) { + //TODO send Sentry login errors and remove eslint-disable from this trycatch throw error; + } + }; + + public is2FANeeded = async (email: string): Promise => { + const authClient = SdkManager.getInstance().auth; + const securityDetails = await authClient.securityDetails(email).catch((error) => { + throw new Error(error.message ?? 'Login error'); }); -}; + return securityDetails.tfaEnabled; + }; +} diff --git a/src/services/config.service.ts b/src/services/config.service.ts index 91d3179..25d3444 100644 --- a/src/services/config.service.ts +++ b/src/services/config.service.ts @@ -2,9 +2,9 @@ import { ConfigKeys } from '../types/config.types'; export class ConfigService { public static readonly instance: ConfigService = new ConfigService(); - get(key: keyof ConfigKeys): string { + public get = (key: keyof ConfigKeys): string => { const value = process.env[key]; if (!value) throw new Error(`Config key ${key} was not found in process.env`); return value; - } + }; } diff --git a/src/services/keys.service.ts b/src/services/keys.service.ts index 812ee46..3251c92 100644 --- a/src/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -1,8 +1,9 @@ import { aes } from '@internxt/lib'; -import { isValid } from '../utils/pgp.utils'; -import { getOpenpgp } from './pgp.service'; +import { isValidKey } from '../utils/pgp.utils'; +import { ConfigService } from './config.service'; +import { OpenpgpService } from './pgp.service'; -export class Base64EncodedPrivateKeyError extends Error { +class Base64EncodedPrivateKeyError extends Error { constructor() { super('Key is encoded in base64'); @@ -10,7 +11,7 @@ export class Base64EncodedPrivateKeyError extends Error { } } -export class WrongIterationsToEncryptPrivateKeyError extends Error { +class WrongIterationsToEncryptPrivateKeyError extends Error { constructor() { super('Key was encrypted using the wrong iterations number'); @@ -18,7 +19,7 @@ export class WrongIterationsToEncryptPrivateKeyError extends Error { } } -export class CorruptedEncryptedPrivateKeyError extends Error { +class CorruptedEncryptedPrivateKeyError extends Error { constructor() { super('Key is corrupted'); @@ -26,7 +27,7 @@ export class CorruptedEncryptedPrivateKeyError extends Error { } } -export class KeysDoNotMatchError extends Error { +class KeysDoNotMatchError extends Error { constructor() { super('Keys do not match'); @@ -34,66 +35,61 @@ export class KeysDoNotMatchError extends Error { } } -/** - * This function validates the private key - * @param privateKey The private key to validate encrypted - * @param password The password used for encrypting the private key - * @throws {Base64EncodedPrivateKeyError} If the PLAIN private key is base64 encoded (known issue introduced in the past) - * @throws {WrongIterationsToEncryptPrivateKeyError} If the ENCRYPTED private key was encrypted using the wrong iterations number (known issue introduced in the past) - * @throws {CorruptedEncryptedPrivateKeyError} If the ENCRYPTED private key is un-decryptable (corrupted) - * @async - */ -export async function assertPrivateKeyIsValid(privateKey: string, password: string): Promise { - let privateKeyDecrypted: string; - - try { - privateKeyDecrypted = decryptPrivateKey(privateKey, password); - } catch { +export class KeysService { + public static readonly instance: KeysService = new KeysService(); + + public assertPrivateKeyIsValid = async (privateKey: string, password: string): Promise => { + let privateKeyDecrypted: string; + try { - aes.decrypt(privateKey, password, 9999); + privateKeyDecrypted = this.decryptPrivateKey(privateKey, password); } catch { - throw new CorruptedEncryptedPrivateKeyError(); + try { + aes.decrypt(privateKey, password, 9999); + } catch { + throw new CorruptedEncryptedPrivateKeyError(); + } + + throw new WrongIterationsToEncryptPrivateKeyError(); } - throw new WrongIterationsToEncryptPrivateKeyError(); - } + const hasValidFormat = await isValidKey(privateKeyDecrypted); - const hasValidFormat = await isValid(privateKeyDecrypted); + if (!hasValidFormat) throw new Base64EncodedPrivateKeyError(); + }; - if (!hasValidFormat) throw new Base64EncodedPrivateKeyError(); -} + public decryptPrivateKey = (privateKey: string, password: string): string => { + return aes.decrypt(privateKey, password); + }; -export function decryptPrivateKey(privateKey: string, password: string): string { - return aes.decrypt(privateKey, password); -} + public assertValidateKeys = async (privateKey: string, publicKey: string): Promise => { + const publicKeyArmored = await OpenpgpService.openpgp.readKey({ armoredKey: publicKey }); + const privateKeyArmored = await OpenpgpService.openpgp.readPrivateKey({ armoredKey: privateKey }); -export async function assertValidateKeys(privateKey: string, publicKey: string): Promise { - const openpgp = await getOpenpgp(); - const publicKeyArmored = await openpgp.readKey({ armoredKey: publicKey }); - const privateKeyArmored = await openpgp.readPrivateKey({ armoredKey: privateKey }); - - const plainMessage = 'validate-keys'; - const originalText = await openpgp.createMessage({ text: plainMessage }); - const encryptedMessage = await openpgp.encrypt({ - message: originalText, - encryptionKeys: publicKeyArmored, - }); - - const decryptedMessage = ( - await openpgp.decrypt({ - message: await openpgp.readMessage({ armoredMessage: encryptedMessage }), - verificationKeys: publicKeyArmored, - decryptionKeys: privateKeyArmored, - }) - ).data; - - if (decryptedMessage !== plainMessage) { - throw new KeysDoNotMatchError(); - } -} + const plainMessage = 'validate-keys'; + const originalText = await OpenpgpService.openpgp.createMessage({ text: plainMessage }); + const encryptedMessage = await OpenpgpService.openpgp.encrypt({ + message: originalText, + encryptionKeys: publicKeyArmored, + }); + + const decryptedMessage = ( + await OpenpgpService.openpgp.decrypt({ + message: await OpenpgpService.openpgp.readMessage({ armoredMessage: encryptedMessage }), + verificationKeys: publicKeyArmored, + decryptionKeys: privateKeyArmored, + }) + ).data; + + if (decryptedMessage !== plainMessage) { + throw new KeysDoNotMatchError(); + } + }; -export function getAesInitFromEnv(): { iv: string; salt: string } { - const { REACT_APP_MAGIC_IV: MAGIC_IV, REACT_APP_MAGIC_SALT: MAGIC_SALT } = process.env; + public getAesInitFromEnv = (): { iv: string; salt: string } => { + const MAGIC_IV = ConfigService.instance.get('REACT_APP_MAGIC_IV'); + const MAGIC_SALT = ConfigService.instance.get('REACT_APP_MAGIC_SALT'); - return { iv: MAGIC_IV as string, salt: MAGIC_SALT as string }; + return { iv: MAGIC_IV as string, salt: MAGIC_SALT as string }; + }; } diff --git a/src/services/pgp.service.ts b/src/services/pgp.service.ts index f896fc2..2e44878 100644 --- a/src/services/pgp.service.ts +++ b/src/services/pgp.service.ts @@ -1,22 +1,36 @@ -export async function getOpenpgp(): Promise { - return import('openpgp'); -} +import { aes } from '@internxt/lib'; +import { KeysService } from './keys.service'; +import openpgp from 'openpgp'; + +export class OpenpgpService { + public static readonly instance: OpenpgpService = new OpenpgpService(); + public static readonly openpgp: typeof import('openpgp') = openpgp; -export async function generateNewKeys(): Promise<{ - privateKeyArmored: string; - publicKeyArmored: string; - revocationCertificate: string; -}> { - const openpgp = await getOpenpgp(); + public generateNewKeysWithEncrypted = async (password: string) => { + const { privateKeyArmored, publicKeyArmored, revocationCertificate } = await this.generateNewKeys(); + + return { + privateKeyArmored, + privateKeyArmoredEncrypted: aes.encrypt(privateKeyArmored, password, KeysService.instance.getAesInitFromEnv()), + publicKeyArmored, + revocationCertificate, + }; + }; - const { privateKey, publicKey, revocationCertificate } = await openpgp.generateKey({ - userIDs: [{ email: 'inxt@inxt.com' }], - curve: 'ed25519', - }); + private generateNewKeys = async (): Promise<{ + privateKeyArmored: string; + publicKeyArmored: string; + revocationCertificate: string; + }> => { + const { privateKey, publicKey, revocationCertificate } = await OpenpgpService.openpgp.generateKey({ + userIDs: [{ email: 'inxt@inxt.com' }], + curve: 'ed25519', + }); - return { - privateKeyArmored: privateKey, - publicKeyArmored: Buffer.from(publicKey).toString('base64'), - revocationCertificate: Buffer.from(revocationCertificate).toString('base64'), + return { + privateKeyArmored: privateKey, + publicKeyArmored: Buffer.from(publicKey).toString('base64'), + revocationCertificate: Buffer.from(revocationCertificate).toString('base64'), + }; }; } diff --git a/src/utils/crypto.utils.ts b/src/utils/crypto.utils.ts index 9baa30a..cd21835 100644 --- a/src/utils/crypto.utils.ts +++ b/src/utils/crypto.utils.ts @@ -7,7 +7,7 @@ interface PassObjectInterface { } // Method to hash password. If salt is passed, use it, in other case use crypto lib for generate salt -function passToHash(passObject: PassObjectInterface): { salt: string; hash: string } { +export const 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 = { @@ -16,34 +16,32 @@ function passToHash(passObject: PassObjectInterface): { salt: string; hash: stri }; return hashedObjetc; -} +}; // AES Plain text encryption method -function encryptText(textToEncrypt: string): string { +export const encryptText = (textToEncrypt: string): string => { const REACT_APP_CRYPTO_SECRET = ConfigService.instance.get('REACT_APP_CRYPTO_SECRET'); return encryptTextWithKey(textToEncrypt, REACT_APP_CRYPTO_SECRET); -} +}; // AES Plain text decryption method -function decryptText(encryptedText: string): string { +export const decryptText = (encryptedText: string): string => { const REACT_APP_CRYPTO_SECRET = ConfigService.instance.get('REACT_APP_CRYPTO_SECRET'); return decryptTextWithKey(encryptedText, REACT_APP_CRYPTO_SECRET); -} +}; // AES Plain text encryption method with enc. key -function encryptTextWithKey(textToEncrypt: string, keyToEncrypt: string): string { +export const 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 -function decryptTextWithKey(encryptedText: string, keyToDecrypt: string): string { +export const 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); -} - -export { passToHash, encryptText, decryptText, encryptTextWithKey, decryptTextWithKey }; +}; diff --git a/src/utils/pgp.utils.ts b/src/utils/pgp.utils.ts index 18dd433..8f39414 100644 --- a/src/utils/pgp.utils.ts +++ b/src/utils/pgp.utils.ts @@ -1,11 +1,10 @@ -import { getOpenpgp } from '../services/pgp.service'; +import { OpenpgpService } from '../services/pgp.service'; -export async function isValid(key: string): Promise { +export const isValidKey = async (key: string): Promise => { try { - const openpgp = await getOpenpgp(); - await openpgp.readKey({ armoredKey: key }); + await OpenpgpService.openpgp.readKey({ armoredKey: key }); return true; } catch (error) { return false; } -} +};