From d85b2f846cc984b685c1a740080e3bd7056ee172 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 11:38:09 +0100 Subject: [PATCH 01/29] added internxt libs and sdk --- package.json | 6 +++++- yarn.lock | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0ea1fd2..e4f3f0f 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,19 @@ ], "dependencies": { "@internxt/eslint-config-internxt": "^1.0.9", + "@internxt/lib": "^1.2.0", "@internxt/prettier-config": "^1.0.2", - "@internxt/sdk": "^1.4.67", + "@internxt/sdk": "^1.4.68", "@oclif/core": "^3", + "crypto-js": "^4.2.0", + "openpgp": "^5.11.0", "dotenv": "^16.4.1" }, "devDependencies": { "@oclif/prettier-config": "^0.2.1", "@oclif/test": "^3", "@types/chai": "^4", + "@types/crypto-js": "^4.2.2", "@types/mocha": "^10", "@types/node": "^18", "chai": "^4", diff --git a/yarn.lock b/yarn.lock index cac97f2..a5d1b1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -775,15 +775,20 @@ "@typescript-eslint/parser" "^5.4.0" eslint-config-prettier "^8.3.0" +"@internxt/lib@^1.2.0": + version "1.2.0" + resolved "https://npm.pkg.github.com/download/@internxt/lib/1.2.0/172d7929abb3dc34fda044ad9977875791f7a471#172d7929abb3dc34fda044ad9977875791f7a471" + integrity sha512-14byNxSU0S0KSc3g+BWiJb7/fcHUfqxkHQRz9fwJhAHNmgOo+Bhx13xXk07KKUzeNX1JymSGEZkVCk0AOQiM+Q== + "@internxt/prettier-config@^1.0.2": version "1.0.2" resolved "https://npm.pkg.github.com/download/@internxt/prettier-config/1.0.2/5bd220b8de76734448db5475b3e0c01f9d22c19b#5bd220b8de76734448db5475b3e0c01f9d22c19b" integrity sha512-t4HiqvCbC7XgQepwWlIaFJe3iwW7HCf6xOSU9nKTV0tiGqOPz7xMtIgLEloQrDA34Cx4PkOYBXrvFPV6RxSFAA== -"@internxt/sdk@^1.4.67": - version "1.4.67" - resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.4.67/86fe22ae1a418a1fd29fd586e7dda5f391c490fb#86fe22ae1a418a1fd29fd586e7dda5f391c490fb" - integrity sha512-jiDue0BRNm/uAEiah/19EL5bXd3GKUdPmyFkzTTEQjpk+xGhUWAkOgTw+Bp6jN5KL9WqleOnKdWIiGKoKj24XQ== +"@internxt/sdk@^1.4.68": + version "1.4.68" + resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.4.68/20ba40fc4dfb78b9ab574c9325e0ea29ee01e161#20ba40fc4dfb78b9ab574c9325e0ea29ee01e161" + integrity sha512-pARjPLPTcWfBZvadZCxQLT4VKl4cISqt59uvU6+lYVDaLecPdCWOQDBIJB5duNlQ0ilmse6D4vZUjSXJKQ0SJA== dependencies: axios "^0.24.0" query-string "^7.1.0" @@ -1731,6 +1736,11 @@ dependencies: "@types/node" "*" +"@types/crypto-js@^4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" + integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== + "@types/expect@^1.20.4": version "1.20.4" resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" @@ -2285,6 +2295,16 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +asn1.js@^5.0.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + assertion-error@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" @@ -2365,6 +2385,11 @@ bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bn.js@^4.0.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + bowser@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" @@ -2854,6 +2879,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -4992,6 +5022,11 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + minimatch@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" @@ -5508,6 +5543,13 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +openpgp@^5.11.0: + version "5.11.0" + resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.11.0.tgz#cec5b285d188148f7b5201b9aceb53850cc286a2" + integrity sha512-hytHsxIPtRhuh6uAmoBUThHSwHSX3imLu7x4453T+xkVqIw49rl22MRD4KQIAQdCDoVdouejzYgcuLmMA/2OAA== + dependencies: + asn1.js "^5.0.0" + optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -6156,7 +6198,7 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== From 9b80ac10ff6dc2f714a325d1edccbf71a44d0ae3 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 11:38:48 +0100 Subject: [PATCH 02/29] improved tsconfig to allow ts and json imports --- tsconfig.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 21e2afb..46afc5b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,12 @@ "strict": true, "target": "es2022", "moduleResolution": "Node", - "composite": true + "composite": true, + "resolveJsonModule": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "removeComments": true, + "esModuleInterop": true, }, "include": ["./src/**/*"] From 6759e5eac2438c0e19d83eb8806d8b9cea7135c9 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 11:39:14 +0100 Subject: [PATCH 03/29] added sdkmanager --- .../app/auth/services/auth.service.ts | 100 +++++++++++ .../app/crypto/services/keys.service.ts | 99 +++++++++++ .../app/crypto/services/pgp.service.ts | 22 +++ .../app/crypto/services/utils.ts | 56 ++++++ .../app/crypto/services/utilspgp.ts | 11 ++ src/webdav-server/core/SDKExceptions.ts | 7 + src/webdav-server/core/SDKManager.ts | 166 ++++++++++++++++++ 7 files changed, 461 insertions(+) create mode 100644 src/webdav-server/app/auth/services/auth.service.ts create mode 100644 src/webdav-server/app/crypto/services/keys.service.ts create mode 100644 src/webdav-server/app/crypto/services/pgp.service.ts create mode 100644 src/webdav-server/app/crypto/services/utils.ts create mode 100644 src/webdav-server/app/crypto/services/utilspgp.ts create mode 100644 src/webdav-server/core/SDKExceptions.ts create mode 100644 src/webdav-server/core/SDKManager.ts diff --git a/src/webdav-server/app/auth/services/auth.service.ts b/src/webdav-server/app/auth/services/auth.service.ts new file mode 100644 index 0000000..76cf95f --- /dev/null +++ b/src/webdav-server/app/auth/services/auth.service.ts @@ -0,0 +1,100 @@ +import { CryptoProvider, LoginDetails } from '@internxt/sdk'; +import { Keys, Password, UserAccessError } from '@internxt/sdk/dist/auth'; +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings.js'; +import { aes } from '@internxt/lib'; +import { SdkManager } from '../../../core/SDKManager.ts'; +import { decryptText, decryptTextWithKey, encryptText, passToHash } from '../../crypto/services/utils.ts'; +import { generateNewKeys } from '../../crypto/services/pgp.service.ts'; +import { + assertPrivateKeyIsValid, + assertValidateKeys, + decryptPrivateKey, + getAesInitFromEnv, +} from '../../crypto/services/keys.service.ts'; + +const generateNewKeysWithEncrypted = async (password: string) => { + const { privateKeyArmored, publicKeyArmored, revocationCertificate } = await generateNewKeys(); + + return { + privateKeyArmored, + privateKeyArmoredEncrypted: aes.encrypt(privateKeyArmored, password, getAesInitFromEnv()), + publicKeyArmored, + revocationCertificate, + }; +}; + +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; + }, + }; + + return authClient + .login(loginDetails, cryptoProvider) + .then(async (data) => { + const { user, token, newToken } = data; + const { privateKey, publicKey } = user; + + const plainPrivateKeyInBase64 = privateKey + ? Buffer.from(decryptPrivateKey(privateKey, password)).toString('base64') + : ''; + + if (privateKey) { + await assertPrivateKeyIsValid(privateKey, password); + await assertValidateKeys( + Buffer.from(plainPrivateKeyInBase64, 'base64').toString(), + Buffer.from(publicKey, 'base64').toString(), + ); + } + + const clearMnemonic = decryptTextWithKey(user.mnemonic, password); + const clearUser = { + ...user, + 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) => { + throw error; + }); +}; diff --git a/src/webdav-server/app/crypto/services/keys.service.ts b/src/webdav-server/app/crypto/services/keys.service.ts new file mode 100644 index 0000000..ee33bdc --- /dev/null +++ b/src/webdav-server/app/crypto/services/keys.service.ts @@ -0,0 +1,99 @@ +import { aes } from '@internxt/lib'; +import { isValid } from './utilspgp.ts'; +import { getOpenpgp } from './pgp.service.ts'; + +export class Base64EncodedPrivateKeyError extends Error { + constructor() { + super('Key is encoded in base64'); + + Object.setPrototypeOf(this, Base64EncodedPrivateKeyError.prototype); + } +} + +export class WrongIterationsToEncryptPrivateKeyError extends Error { + constructor() { + super('Key was encrypted using the wrong iterations number'); + + Object.setPrototypeOf(this, WrongIterationsToEncryptPrivateKeyError.prototype); + } +} + +export class CorruptedEncryptedPrivateKeyError extends Error { + constructor() { + super('Key is corrupted'); + + Object.setPrototypeOf(this, CorruptedEncryptedPrivateKeyError.prototype); + } +} + +export class KeysDoNotMatchError extends Error { + constructor() { + super('Keys do not match'); + + Object.setPrototypeOf(this, CorruptedEncryptedPrivateKeyError.prototype); + } +} + +/** + * 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 { + try { + aes.decrypt(privateKey, password, 9999); + } catch { + throw new CorruptedEncryptedPrivateKeyError(); + } + + throw new WrongIterationsToEncryptPrivateKeyError(); + } + + const hasValidFormat = await isValid(privateKeyDecrypted); + + if (!hasValidFormat) throw new Base64EncodedPrivateKeyError(); +} + +export function decryptPrivateKey(privateKey: string, password: string): string { + return aes.decrypt(privateKey, password); +} + +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(); + } +} + +export function getAesInitFromEnv(): { iv: string; salt: string } { + const { REACT_APP_MAGIC_IV: MAGIC_IV, REACT_APP_MAGIC_SALT: MAGIC_SALT } = process.env; + + return { iv: MAGIC_IV as string, salt: MAGIC_SALT as string }; +} diff --git a/src/webdav-server/app/crypto/services/pgp.service.ts b/src/webdav-server/app/crypto/services/pgp.service.ts new file mode 100644 index 0000000..f896fc2 --- /dev/null +++ b/src/webdav-server/app/crypto/services/pgp.service.ts @@ -0,0 +1,22 @@ +export async function getOpenpgp(): Promise { + return import('openpgp'); +} + +export async function generateNewKeys(): Promise<{ + privateKeyArmored: string; + publicKeyArmored: string; + revocationCertificate: string; +}> { + const openpgp = await getOpenpgp(); + + const { privateKey, publicKey, revocationCertificate } = await openpgp.generateKey({ + userIDs: [{ email: 'inxt@inxt.com' }], + curve: 'ed25519', + }); + + return { + privateKeyArmored: privateKey, + publicKeyArmored: Buffer.from(publicKey).toString('base64'), + revocationCertificate: Buffer.from(revocationCertificate).toString('base64'), + }; +} diff --git a/src/webdav-server/app/crypto/services/utils.ts b/src/webdav-server/app/crypto/services/utils.ts new file mode 100644 index 0000000..f941a03 --- /dev/null +++ b/src/webdav-server/app/crypto/services/utils.ts @@ -0,0 +1,56 @@ +import CryptoJS from 'crypto-js'; + +interface PassObjectInterface { + salt?: string | null; + password: string; +} + +// 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 } { + 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 +function encryptText(textToEncrypt: string): string { + if (!process.env.REACT_APP_CRYPTO_SECRET) { + throw new Error('env variable REACT_APP_CRYPTO_SECRET is not defined'); + } + return encryptTextWithKey(textToEncrypt, process.env.REACT_APP_CRYPTO_SECRET); +} + +// AES Plain text decryption method +function decryptText(encryptedText: string): string { + if (!process.env.REACT_APP_CRYPTO_SECRET) { + throw new Error('env variable REACT_APP_CRYPTO_SECRET is not defined'); + } + return decryptTextWithKey(encryptedText, process.env.REACT_APP_CRYPTO_SECRET); +} + +// AES Plain text encryption method with enc. key +function 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 { + if (!keyToDecrypt) { + throw new Error('No key defined. Check .env file'); + } + + 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/webdav-server/app/crypto/services/utilspgp.ts b/src/webdav-server/app/crypto/services/utilspgp.ts new file mode 100644 index 0000000..2a4c56b --- /dev/null +++ b/src/webdav-server/app/crypto/services/utilspgp.ts @@ -0,0 +1,11 @@ +import { getOpenpgp } from './pgp.service.ts'; + +export async function isValid(key: string): Promise { + try { + const openpgp = await getOpenpgp(); + await openpgp.readKey({ armoredKey: key }); + return true; + } catch (error) { + return false; + } +} diff --git a/src/webdav-server/core/SDKExceptions.ts b/src/webdav-server/core/SDKExceptions.ts new file mode 100644 index 0000000..5018465 --- /dev/null +++ b/src/webdav-server/core/SDKExceptions.ts @@ -0,0 +1,7 @@ +export class NoEnvDefined extends Error { + constructor(envProperty: string) { + super('ENV variable ' + envProperty + ' is not defined'); + + Object.setPrototypeOf(this, NoEnvDefined.prototype); + } +} diff --git a/src/webdav-server/core/SDKManager.ts b/src/webdav-server/core/SDKManager.ts new file mode 100644 index 0000000..dc85971 --- /dev/null +++ b/src/webdav-server/core/SDKManager.ts @@ -0,0 +1,166 @@ +import { Auth, Drive, photos } from '@internxt/sdk'; +import { Trash } from '@internxt/sdk/dist/drive'; +import { ApiSecurity, AppDetails } from '@internxt/sdk/dist/shared'; +import packageJson from '../../../package.json'; +import { NoEnvDefined } from './SDKExceptions'; + +export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; +/** + * Manages all the sdk submodules initialization + * based on the current apiSecurity details + */ +export class SdkManager { + private static apiSecurity?: SdkManagerApiSecurity = undefined; + private static instance: SdkManager = new SdkManager(); + /** + * Sets the security details needed to create SDK clients + * @param apiSecurity Security properties to be setted + */ + static init(apiSecurity: SdkManagerApiSecurity) { + SdkManager.setApiSecurity(apiSecurity); + } + + static setApiSecurity(apiSecurity: SdkManagerApiSecurity) { + SdkManager.apiSecurity = apiSecurity; + } + + static clean() { + SdkManager.apiSecurity = undefined; + } + + static getInstance() { + if (!SdkManager.instance) { + throw new Error('No instance found, call init method first'); + } + return SdkManager.instance; + } + + 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 { + return { + clientName: packageJson.name, + clientVersion: packageJson.version, + }; + } + + /** Auth SDK */ + get authV2() { + if (!process.env.DRIVE_NEW_API_URL) { + throw new NoEnvDefined('DRIVE_NEW_API_URL'); + } + + const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); + const appDetails = SdkManager.getAppDetails(); + + return Auth.client(process.env.DRIVE_NEW_API_URL, appDetails, apiSecurity); + } + + /** Auth old client SDK */ + get auth() { + if (!process.env.DRIVE_API_URL) { + throw new NoEnvDefined('DRIVE_API_URL'); + } + + const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); + const appDetails = SdkManager.getAppDetails(); + + return Auth.client(process.env.DRIVE_API_URL, appDetails, apiSecurity); + } + + /** Payments SDK */ + get payments() { + if (!process.env.PAYMENTS_API_URL) { + throw new NoEnvDefined('PAYMENTS_API_URL'); + } + + const newToken = this.getApiSecurity().newToken; + const appDetails = SdkManager.getAppDetails(); + + return Drive.Payments.client(process.env.PAYMENTS_API_URL, appDetails, { + // Weird, normal accessToken doesn't work here + token: newToken, + }); + } + + /** Users SDK */ + get users() { + if (!process.env.DRIVE_API_URL) { + throw new NoEnvDefined('DRIVE_API_URL'); + } + + const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); + const appDetails = SdkManager.getAppDetails(); + + return Drive.Users.client(process.env.DRIVE_API_URL, appDetails, apiSecurity); + } + + /** Referrals SDK */ + get referrals() { + if (!process.env.DRIVE_API_URL) { + throw new NoEnvDefined('DRIVE_API_URL'); + } + + const apiSecurity = this.getApiSecurity(); + const appDetails = SdkManager.getAppDetails(); + + return Drive.Referrals.client(process.env.DRIVE_API_URL, appDetails, apiSecurity); + } + + /** Storage SDK */ + get storage() { + if (!process.env.DRIVE_API_URL) { + throw new NoEnvDefined('DRIVE_API_URL'); + } + + const apiSecurity = this.getApiSecurity(); + const appDetails = SdkManager.getAppDetails(); + + return Drive.Storage.client(process.env.DRIVE_API_URL, appDetails, apiSecurity); + } + + /** Trash SDK */ + get trash() { + if (!process.env.DRIVE_NEW_API_URL) { + throw new NoEnvDefined('DRIVE_NEW_API_URL'); + } + + const newToken = this.getApiSecurity().newToken; + const appDetails = SdkManager.getAppDetails(); + + return Trash.client(process.env.DRIVE_NEW_API_URL, appDetails, { + // Weird, normal accessToken doesn't work here + token: newToken, + }); + } + + /** Photos SDK */ + get photos() { + if (!process.env.PHOTOS_API_URL) { + throw new NoEnvDefined('PHOTOS_API_URL'); + } + + const newToken = this.getApiSecurity().newToken; + return new photos.Photos(process.env.PHOTOS_API_URL, newToken); + } + + /** Share SDK */ + get share() { + if (!process.env.DRIVE_NEW_API_URL) { + throw new NoEnvDefined('DRIVE_NEW_API_URL'); + } + + const newToken = this.getApiSecurity().newToken; + const appDetails = SdkManager.getAppDetails(); + + return Drive.Share.client(process.env.DRIVE_NEW_API_URL, appDetails, { + // Weird, normal accessToken doesn't work here + token: newToken, + }); + } +} From a830260e71e211f9c5959e945dc29c11867187dc Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 11:45:29 +0100 Subject: [PATCH 04/29] added 2fa check functionality --- src/webdav-server/app/auth/services/auth.service.ts | 12 ++++++++++-- src/webdav-server/core/SDKExceptions.ts | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/webdav-server/app/auth/services/auth.service.ts b/src/webdav-server/app/auth/services/auth.service.ts index 76cf95f..164b211 100644 --- a/src/webdav-server/app/auth/services/auth.service.ts +++ b/src/webdav-server/app/auth/services/auth.service.ts @@ -23,6 +23,14 @@ const generateNewKeysWithEncrypted = async (password: string) => { }; }; +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, @@ -84,8 +92,8 @@ export const doLogin = async ( //TODO save tokens for later use /*localStorageService.set('xToken', token); - localStorageService.set('xMnemonic', clearMnemonic); - localStorageService.set('xNewToken', newToken);*/ + localStorageService.set('xMnemonic', clearMnemonic); + localStorageService.set('xNewToken', newToken);*/ return { user: clearUser, diff --git a/src/webdav-server/core/SDKExceptions.ts b/src/webdav-server/core/SDKExceptions.ts index 5018465..3fcf1f0 100644 --- a/src/webdav-server/core/SDKExceptions.ts +++ b/src/webdav-server/core/SDKExceptions.ts @@ -1,3 +1,5 @@ +//type EnvironmentVars = process.env; + export class NoEnvDefined extends Error { constructor(envProperty: string) { super('ENV variable ' + envProperty + ' is not defined'); From 7aebda7313c645b4a0c9c32a4d0b83d2fb4b707a Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 12:00:25 +0100 Subject: [PATCH 05/29] improved env management and added new needed envs --- .env.template | 5 +- src/types/config.types.ts | 3 + .../app/auth/services/auth.service.ts | 2 +- .../app/crypto/services/utils.ts | 17 ++---- src/webdav-server/core/SDKExceptions.ts | 9 --- src/webdav-server/core/SDKManager.ts | 57 +++++++------------ 6 files changed, 33 insertions(+), 60 deletions(-) delete mode 100644 src/webdav-server/core/SDKExceptions.ts diff --git a/.env.template b/.env.template index a2e16e0..3b6631b 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,5 @@ DRIVE_API_URL= -DRIVE_NEW_API_URL= \ No newline at end of file +DRIVE_NEW_API_URL= +PAYMENTS_API_URL= +PHOTOS_API_URL= +REACT_APP_CRYPTO_SECRET= diff --git a/src/types/config.types.ts b/src/types/config.types.ts index 2964370..66b5765 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -1,4 +1,7 @@ export interface ConfigKeys { readonly DRIVE_API_URL: string; readonly DRIVE_NEW_API_URL: string; + readonly REACT_APP_CRYPTO_SECRET: string; + readonly PAYMENTS_API_URL: string; + readonly PHOTOS_API_URL: string; } diff --git a/src/webdav-server/app/auth/services/auth.service.ts b/src/webdav-server/app/auth/services/auth.service.ts index 164b211..62dec6b 100644 --- a/src/webdav-server/app/auth/services/auth.service.ts +++ b/src/webdav-server/app/auth/services/auth.service.ts @@ -1,5 +1,5 @@ import { CryptoProvider, LoginDetails } from '@internxt/sdk'; -import { Keys, Password, UserAccessError } from '@internxt/sdk/dist/auth'; +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 '../../../core/SDKManager.ts'; diff --git a/src/webdav-server/app/crypto/services/utils.ts b/src/webdav-server/app/crypto/services/utils.ts index f941a03..b3082ba 100644 --- a/src/webdav-server/app/crypto/services/utils.ts +++ b/src/webdav-server/app/crypto/services/utils.ts @@ -1,4 +1,5 @@ import CryptoJS from 'crypto-js'; +import { ConfigService } from '../../../../services/config.service'; interface PassObjectInterface { salt?: string | null; @@ -19,18 +20,14 @@ function passToHash(passObject: PassObjectInterface): { salt: string; hash: stri // AES Plain text encryption method function encryptText(textToEncrypt: string): string { - if (!process.env.REACT_APP_CRYPTO_SECRET) { - throw new Error('env variable REACT_APP_CRYPTO_SECRET is not defined'); - } - return encryptTextWithKey(textToEncrypt, process.env.REACT_APP_CRYPTO_SECRET); + 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 { - if (!process.env.REACT_APP_CRYPTO_SECRET) { - throw new Error('env variable REACT_APP_CRYPTO_SECRET is not defined'); - } - return decryptTextWithKey(encryptedText, process.env.REACT_APP_CRYPTO_SECRET); + 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 @@ -43,10 +40,6 @@ function encryptTextWithKey(textToEncrypt: string, keyToEncrypt: string): string // AES Plain text decryption method with enc. key function decryptTextWithKey(encryptedText: string, keyToDecrypt: string): string { - if (!keyToDecrypt) { - throw new Error('No key defined. Check .env file'); - } - const reb = CryptoJS.enc.Hex.parse(encryptedText); const bytes = CryptoJS.AES.decrypt(reb.toString(CryptoJS.enc.Base64), keyToDecrypt); diff --git a/src/webdav-server/core/SDKExceptions.ts b/src/webdav-server/core/SDKExceptions.ts deleted file mode 100644 index 3fcf1f0..0000000 --- a/src/webdav-server/core/SDKExceptions.ts +++ /dev/null @@ -1,9 +0,0 @@ -//type EnvironmentVars = process.env; - -export class NoEnvDefined extends Error { - constructor(envProperty: string) { - super('ENV variable ' + envProperty + ' is not defined'); - - Object.setPrototypeOf(this, NoEnvDefined.prototype); - } -} diff --git a/src/webdav-server/core/SDKManager.ts b/src/webdav-server/core/SDKManager.ts index dc85971..4359517 100644 --- a/src/webdav-server/core/SDKManager.ts +++ b/src/webdav-server/core/SDKManager.ts @@ -2,7 +2,7 @@ import { Auth, Drive, photos } from '@internxt/sdk'; import { Trash } from '@internxt/sdk/dist/drive'; import { ApiSecurity, AppDetails } from '@internxt/sdk/dist/shared'; import packageJson from '../../../package.json'; -import { NoEnvDefined } from './SDKExceptions'; +import { ConfigService } from '../../services/config.service'; export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; /** @@ -51,38 +51,32 @@ export class SdkManager { /** Auth SDK */ get authV2() { - if (!process.env.DRIVE_NEW_API_URL) { - throw new NoEnvDefined('DRIVE_NEW_API_URL'); - } + const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); const appDetails = SdkManager.getAppDetails(); - return Auth.client(process.env.DRIVE_NEW_API_URL, appDetails, apiSecurity); + return Auth.client(DRIVE_NEW_API_URL, appDetails, apiSecurity); } /** Auth old client SDK */ get auth() { - if (!process.env.DRIVE_API_URL) { - throw new NoEnvDefined('DRIVE_API_URL'); - } + const DRIVE_API_URL = ConfigService.instance.get('DRIVE_API_URL'); const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); const appDetails = SdkManager.getAppDetails(); - return Auth.client(process.env.DRIVE_API_URL, appDetails, apiSecurity); + return Auth.client(DRIVE_API_URL, appDetails, apiSecurity); } /** Payments SDK */ get payments() { - if (!process.env.PAYMENTS_API_URL) { - throw new NoEnvDefined('PAYMENTS_API_URL'); - } + const PAYMENTS_API_URL = ConfigService.instance.get('PAYMENTS_API_URL'); const newToken = this.getApiSecurity().newToken; const appDetails = SdkManager.getAppDetails(); - return Drive.Payments.client(process.env.PAYMENTS_API_URL, appDetails, { + return Drive.Payments.client(PAYMENTS_API_URL, appDetails, { // Weird, normal accessToken doesn't work here token: newToken, }); @@ -90,50 +84,42 @@ export class SdkManager { /** Users SDK */ get users() { - if (!process.env.DRIVE_API_URL) { - throw new NoEnvDefined('DRIVE_API_URL'); - } + const DRIVE_API_URL = ConfigService.instance.get('DRIVE_API_URL'); const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); const appDetails = SdkManager.getAppDetails(); - return Drive.Users.client(process.env.DRIVE_API_URL, appDetails, apiSecurity); + return Drive.Users.client(DRIVE_API_URL, appDetails, apiSecurity); } /** Referrals SDK */ get referrals() { - if (!process.env.DRIVE_API_URL) { - throw new NoEnvDefined('DRIVE_API_URL'); - } + const DRIVE_API_URL = ConfigService.instance.get('DRIVE_API_URL'); const apiSecurity = this.getApiSecurity(); const appDetails = SdkManager.getAppDetails(); - return Drive.Referrals.client(process.env.DRIVE_API_URL, appDetails, apiSecurity); + return Drive.Referrals.client(DRIVE_API_URL, appDetails, apiSecurity); } /** Storage SDK */ get storage() { - if (!process.env.DRIVE_API_URL) { - throw new NoEnvDefined('DRIVE_API_URL'); - } + const DRIVE_API_URL = ConfigService.instance.get('DRIVE_API_URL'); const apiSecurity = this.getApiSecurity(); const appDetails = SdkManager.getAppDetails(); - return Drive.Storage.client(process.env.DRIVE_API_URL, appDetails, apiSecurity); + return Drive.Storage.client(DRIVE_API_URL, appDetails, apiSecurity); } /** Trash SDK */ get trash() { - if (!process.env.DRIVE_NEW_API_URL) { - throw new NoEnvDefined('DRIVE_NEW_API_URL'); - } + const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); const newToken = this.getApiSecurity().newToken; const appDetails = SdkManager.getAppDetails(); - return Trash.client(process.env.DRIVE_NEW_API_URL, appDetails, { + return Trash.client(DRIVE_NEW_API_URL, appDetails, { // Weird, normal accessToken doesn't work here token: newToken, }); @@ -141,24 +127,21 @@ export class SdkManager { /** Photos SDK */ get photos() { - if (!process.env.PHOTOS_API_URL) { - throw new NoEnvDefined('PHOTOS_API_URL'); - } + const PHOTOS_API_URL = ConfigService.instance.get('PHOTOS_API_URL'); const newToken = this.getApiSecurity().newToken; - return new photos.Photos(process.env.PHOTOS_API_URL, newToken); + + return new photos.Photos(PHOTOS_API_URL, newToken); } /** Share SDK */ get share() { - if (!process.env.DRIVE_NEW_API_URL) { - throw new NoEnvDefined('DRIVE_NEW_API_URL'); - } + const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); const newToken = this.getApiSecurity().newToken; const appDetails = SdkManager.getAppDetails(); - return Drive.Share.client(process.env.DRIVE_NEW_API_URL, appDetails, { + return Drive.Share.client(DRIVE_NEW_API_URL, appDetails, { // Weird, normal accessToken doesn't work here token: newToken, }); From 0dfbd2f654640567a1d27377bd5338f35c2183a4 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 13:24:51 +0100 Subject: [PATCH 06/29] fixed sdk and openpgp build problems --- package.json | 7 ++++--- yarn.lock | 26 ++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index e4f3f0f..e61e86c 100644 --- a/package.json +++ b/package.json @@ -32,15 +32,16 @@ "@internxt/eslint-config-internxt": "^1.0.9", "@internxt/lib": "^1.2.0", "@internxt/prettier-config": "^1.0.2", - "@internxt/sdk": "^1.4.68", + "@internxt/sdk": "^1.4.70", "@oclif/core": "^3", "crypto-js": "^4.2.0", - "openpgp": "^5.11.0", - "dotenv": "^16.4.1" + "dotenv": "^16.4.1", + "openpgp": "^5.11.0" }, "devDependencies": { "@oclif/prettier-config": "^0.2.1", "@oclif/test": "^3", + "@openpgp/web-stream-tools": "0.0.11-patch-0", "@types/chai": "^4", "@types/crypto-js": "^4.2.2", "@types/mocha": "^10", diff --git a/yarn.lock b/yarn.lock index a5d1b1f..9985ded 100644 --- a/yarn.lock +++ b/yarn.lock @@ -785,10 +785,10 @@ resolved "https://npm.pkg.github.com/download/@internxt/prettier-config/1.0.2/5bd220b8de76734448db5475b3e0c01f9d22c19b#5bd220b8de76734448db5475b3e0c01f9d22c19b" integrity sha512-t4HiqvCbC7XgQepwWlIaFJe3iwW7HCf6xOSU9nKTV0tiGqOPz7xMtIgLEloQrDA34Cx4PkOYBXrvFPV6RxSFAA== -"@internxt/sdk@^1.4.68": - version "1.4.68" - resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.4.68/20ba40fc4dfb78b9ab574c9325e0ea29ee01e161#20ba40fc4dfb78b9ab574c9325e0ea29ee01e161" - integrity sha512-pARjPLPTcWfBZvadZCxQLT4VKl4cISqt59uvU6+lYVDaLecPdCWOQDBIJB5duNlQ0ilmse6D4vZUjSXJKQ0SJA== +"@internxt/sdk@^1.4.70": + version "1.4.70" + resolved "https://npm.pkg.github.com/download/@internxt/sdk/1.4.70/6e07263dbaa130269323d21b895ea8c0e7d1cc3c#6e07263dbaa130269323d21b895ea8c0e7d1cc3c" + integrity sha512-bqtTsYjT9kjkxFfqFVOrxlp3vlCtQdLhH9LWOF4DSIc5j/EnDh7hTKwSGo+mzMdqYYeivQB3cqrxr6QiyBMkcw== dependencies: axios "^0.24.0" query-string "^7.1.0" @@ -817,6 +817,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@mattiasbuelens/web-streams-adapter@~0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz#607b5a25682f4ae2741da7ba6df39302505336b3" + integrity sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -1159,6 +1164,14 @@ dependencies: "@octokit/openapi-types" "^12.11.0" +"@openpgp/web-stream-tools@0.0.11-patch-0": + version "0.0.11-patch-0" + resolved "https://registry.yarnpkg.com/@openpgp/web-stream-tools/-/web-stream-tools-0.0.11-patch-0.tgz#c8b2ecfa62403bb10de2b33c8548bd25b14f1828" + integrity sha512-NrIF4DkCqC3WDcMDAgz17z+0Iik1fVrKuvdbjZXCnMZgYAWHpIG8CWnbp8yQRahAdF26jqCopA/qXrp8CYI2yw== + dependencies: + "@mattiasbuelens/web-streams-adapter" "~0.1.0" + web-streams-polyfill "~3.0.3" + "@sindresorhus/is@^4.0.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" @@ -6975,6 +6988,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-streams-polyfill@~3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.0.3.tgz#f49e487eedeca47a207c1aee41ee5578f884b42f" + integrity sha512-d2H/t0eqRNM4w2WvmTdoeIvzAUSpK7JmATB8Nr2lb7nQ9BTIJVjbQ/TRFVEh2gUH1HwclPdoPtfMoFfetXaZnA== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" From eb2c7d72ffca2d0b99f03ec0a348c5101ce95943 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 13:25:13 +0100 Subject: [PATCH 07/29] fixed packageJSON import --- src/webdav-server/core/SDKManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webdav-server/core/SDKManager.ts b/src/webdav-server/core/SDKManager.ts index 4359517..6f64a40 100644 --- a/src/webdav-server/core/SDKManager.ts +++ b/src/webdav-server/core/SDKManager.ts @@ -1,8 +1,8 @@ import { Auth, Drive, photos } from '@internxt/sdk'; import { Trash } from '@internxt/sdk/dist/drive'; import { ApiSecurity, AppDetails } from '@internxt/sdk/dist/shared'; -import packageJson from '../../../package.json'; import { ConfigService } from '../../services/config.service'; +import packageJson = require('../../../package.json'); export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; /** From 81156cd707a0836fece1f3cc51af437862ecefc2 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 14:05:41 +0100 Subject: [PATCH 08/29] removed noEmit fromtsconfig --- tsconfig.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 46afc5b..666305b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,11 +9,8 @@ "moduleResolution": "Node", "composite": true, "resolveJsonModule": true, - "noEmit": true, - "allowImportingTsExtensions": true, "removeComments": true, "esModuleInterop": true, }, - "include": ["./src/**/*"] } From 815a800d2ac124113a25da8840f08360ab6e06bc Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 14:06:37 +0100 Subject: [PATCH 09/29] moved services --- .../SDKManager.service.ts} | 6 ++++-- .../app/auth => }/services/auth.service.ts | 13 ++++--------- .../app/crypto => }/services/keys.service.ts | 4 ++-- .../app/crypto => }/services/pgp.service.ts | 0 .../services/utils.ts => utils/crypto.utils.ts} | 2 +- .../services/utilspgp.ts => utils/pgp.utils.ts} | 2 +- 6 files changed, 12 insertions(+), 15 deletions(-) rename src/{webdav-server/core/SDKManager.ts => services/SDKManager.service.ts} (96%) rename src/{webdav-server/app/auth => }/services/auth.service.ts (90%) rename src/{webdav-server/app/crypto => }/services/keys.service.ts (97%) rename src/{webdav-server/app/crypto => }/services/pgp.service.ts (100%) rename src/{webdav-server/app/crypto/services/utils.ts => utils/crypto.utils.ts} (96%) rename src/{webdav-server/app/crypto/services/utilspgp.ts => utils/pgp.utils.ts} (80%) diff --git a/src/webdav-server/core/SDKManager.ts b/src/services/SDKManager.service.ts similarity index 96% rename from src/webdav-server/core/SDKManager.ts rename to src/services/SDKManager.service.ts index 6f64a40..596e242 100644 --- a/src/webdav-server/core/SDKManager.ts +++ b/src/services/SDKManager.service.ts @@ -1,8 +1,10 @@ import { Auth, Drive, photos } from '@internxt/sdk'; import { Trash } from '@internxt/sdk/dist/drive'; import { ApiSecurity, AppDetails } from '@internxt/sdk/dist/shared'; -import { ConfigService } from '../../services/config.service'; -import packageJson = require('../../../package.json'); +import { ConfigService } from './config.service'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJson = require('../../package.json'); export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; /** diff --git a/src/webdav-server/app/auth/services/auth.service.ts b/src/services/auth.service.ts similarity index 90% rename from src/webdav-server/app/auth/services/auth.service.ts rename to src/services/auth.service.ts index 62dec6b..a2873a3 100644 --- a/src/webdav-server/app/auth/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -2,15 +2,10 @@ 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 '../../../core/SDKManager.ts'; -import { decryptText, decryptTextWithKey, encryptText, passToHash } from '../../crypto/services/utils.ts'; -import { generateNewKeys } from '../../crypto/services/pgp.service.ts'; -import { - assertPrivateKeyIsValid, - assertValidateKeys, - decryptPrivateKey, - getAesInitFromEnv, -} from '../../crypto/services/keys.service.ts'; +import { SdkManager } from './SDKManager.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(); diff --git a/src/webdav-server/app/crypto/services/keys.service.ts b/src/services/keys.service.ts similarity index 97% rename from src/webdav-server/app/crypto/services/keys.service.ts rename to src/services/keys.service.ts index ee33bdc..812ee46 100644 --- a/src/webdav-server/app/crypto/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -1,6 +1,6 @@ import { aes } from '@internxt/lib'; -import { isValid } from './utilspgp.ts'; -import { getOpenpgp } from './pgp.service.ts'; +import { isValid } from '../utils/pgp.utils'; +import { getOpenpgp } from './pgp.service'; export class Base64EncodedPrivateKeyError extends Error { constructor() { diff --git a/src/webdav-server/app/crypto/services/pgp.service.ts b/src/services/pgp.service.ts similarity index 100% rename from src/webdav-server/app/crypto/services/pgp.service.ts rename to src/services/pgp.service.ts diff --git a/src/webdav-server/app/crypto/services/utils.ts b/src/utils/crypto.utils.ts similarity index 96% rename from src/webdav-server/app/crypto/services/utils.ts rename to src/utils/crypto.utils.ts index b3082ba..9baa30a 100644 --- a/src/webdav-server/app/crypto/services/utils.ts +++ b/src/utils/crypto.utils.ts @@ -1,5 +1,5 @@ import CryptoJS from 'crypto-js'; -import { ConfigService } from '../../../../services/config.service'; +import { ConfigService } from '../services/config.service'; interface PassObjectInterface { salt?: string | null; diff --git a/src/webdav-server/app/crypto/services/utilspgp.ts b/src/utils/pgp.utils.ts similarity index 80% rename from src/webdav-server/app/crypto/services/utilspgp.ts rename to src/utils/pgp.utils.ts index 2a4c56b..18dd433 100644 --- a/src/webdav-server/app/crypto/services/utilspgp.ts +++ b/src/utils/pgp.utils.ts @@ -1,4 +1,4 @@ -import { getOpenpgp } from './pgp.service.ts'; +import { getOpenpgp } from '../services/pgp.service'; export async function isValid(key: string): Promise { try { From fc17e9a9957745c9d6c4b52d1309a00c5b8b5b48 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 14:09:12 +0100 Subject: [PATCH 10/29] added missing type to whoami test --- test/commands/whoami.test.ts | 3 ++- test/types/oclif-test.types.ts | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 test/types/oclif-test.types.ts diff --git a/test/commands/whoami.test.ts b/test/commands/whoami.test.ts index ced07aa..8706511 100644 --- a/test/commands/whoami.test.ts +++ b/test/commands/whoami.test.ts @@ -1,10 +1,11 @@ import { expect, test } from '@oclif/test'; +import { OclifStdoutContext } from '../types/oclif-test.types'; describe('whoami', () => { test .stdout() .command(['whoami']) - .it('runs whoami', (ctx) => { + .it('runs whoami', (ctx: OclifStdoutContext) => { expect(ctx.stdout).to.contain('You are'); }); }); diff --git a/test/types/oclif-test.types.ts b/test/types/oclif-test.types.ts new file mode 100644 index 0000000..ec75b0c --- /dev/null +++ b/test/types/oclif-test.types.ts @@ -0,0 +1,6 @@ +import { Config } from '@oclif/core/lib/interfaces'; +import { Context } from 'fancy-test/lib/types'; + +export type OclifStdoutContext = { config: Config; expectation: string; returned: unknown } & { + readonly stdout: string; +} & Context; From 5390403b5e0090834a156bc264d5d469a4862754 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 16:57:05 +0100 Subject: [PATCH 11/29] change services to class --- src/services/SDKManager.service.ts | 24 +++--- src/services/auth.service.ts | 124 +++++++++++++---------------- src/services/config.service.ts | 4 +- src/services/keys.service.ts | 112 +++++++++++++------------- src/services/pgp.service.ts | 48 +++++++---- src/utils/crypto.utils.ts | 22 +++-- src/utils/pgp.utils.ts | 9 +-- 7 files changed, 168 insertions(+), 175 deletions(-) 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; } -} +}; From abae74a9c1aa1482b18cd4df56f505c74c69cad8 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 17:00:14 +0100 Subject: [PATCH 12/29] added missing crypto environment vars --- .env.template | 4 +++- src/services/keys.service.ts | 4 ++-- src/types/config.types.ts | 4 +++- src/utils/crypto.utils.ts | 8 ++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.env.template b/.env.template index 3b6631b..c06bbc6 100644 --- a/.env.template +++ b/.env.template @@ -2,4 +2,6 @@ DRIVE_API_URL= DRIVE_NEW_API_URL= PAYMENTS_API_URL= PHOTOS_API_URL= -REACT_APP_CRYPTO_SECRET= +APP_CRYPTO_SECRET= +APP_MAGIC_IV= +APP_MAGIC_SALT= diff --git a/src/services/keys.service.ts b/src/services/keys.service.ts index 3251c92..165ba3a 100644 --- a/src/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -87,8 +87,8 @@ export class KeysService { }; 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'); + const MAGIC_IV = ConfigService.instance.get('APP_MAGIC_IV'); + const MAGIC_SALT = ConfigService.instance.get('APP_MAGIC_SALT'); return { iv: MAGIC_IV as string, salt: MAGIC_SALT as string }; }; diff --git a/src/types/config.types.ts b/src/types/config.types.ts index 66b5765..0dc0d9a 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -1,7 +1,9 @@ export interface ConfigKeys { readonly DRIVE_API_URL: string; readonly DRIVE_NEW_API_URL: string; - readonly REACT_APP_CRYPTO_SECRET: string; readonly PAYMENTS_API_URL: string; readonly PHOTOS_API_URL: string; + readonly APP_CRYPTO_SECRET: string; + readonly APP_MAGIC_IV: string; + readonly APP_MAGIC_SALT: string; } diff --git a/src/utils/crypto.utils.ts b/src/utils/crypto.utils.ts index cd21835..54311c1 100644 --- a/src/utils/crypto.utils.ts +++ b/src/utils/crypto.utils.ts @@ -20,14 +20,14 @@ export const passToHash = (passObject: PassObjectInterface): { salt: string; has // AES Plain text encryption method 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); + const APP_CRYPTO_SECRET = ConfigService.instance.get('APP_CRYPTO_SECRET'); + return encryptTextWithKey(textToEncrypt, APP_CRYPTO_SECRET); }; // AES Plain text decryption method 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); + const APP_CRYPTO_SECRET = ConfigService.instance.get('APP_CRYPTO_SECRET'); + return decryptTextWithKey(encryptedText, APP_CRYPTO_SECRET); }; // AES Plain text encryption method with enc. key From 9068274819880c525a493cfca38b631bf3bbcbd8 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Mon, 12 Feb 2024 17:23:13 +0100 Subject: [PATCH 13/29] changed packageJSON import from require to import --- src/services/SDKManager.service.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/SDKManager.service.ts b/src/services/SDKManager.service.ts index 206f9a6..3e6eb3d 100644 --- a/src/services/SDKManager.service.ts +++ b/src/services/SDKManager.service.ts @@ -2,9 +2,7 @@ import { Auth, Drive, photos } from '@internxt/sdk'; import { Trash } from '@internxt/sdk/dist/drive'; import { ApiSecurity, AppDetails } from '@internxt/sdk/dist/shared'; import { ConfigService } from './config.service'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const packageJson = require('../../package.json'); +import packageJson from '../../package.json'; export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; /** From e9f8591fb888e786da660a8c728e23712a6083ae Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 13 Feb 2024 19:36:34 +0100 Subject: [PATCH 14/29] added keys service test and moved and fixed openpgp class --- src/services/auth.service.ts | 3 +- src/services/keys.service.ts | 164 +++++++++++++-------- src/services/pgp.service.ts | 36 ----- src/types/keys.types.ts | 36 +++++ src/utils/pgp.utils.ts | 10 -- test/services/keys.service.test.ts | 225 +++++++++++++++++++++++++++++ yarn.lock | 9 +- 7 files changed, 371 insertions(+), 112 deletions(-) delete mode 100644 src/services/pgp.service.ts create mode 100644 src/types/keys.types.ts delete mode 100644 src/utils/pgp.utils.ts create mode 100644 test/services/keys.service.test.ts diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 5269e39..2c5afa6 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -3,7 +3,6 @@ import { Keys, Password } from '@internxt/sdk/dist/auth'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings.js'; import { SdkManager } from './SDKManager.service'; import { KeysService } from './keys.service'; -import { OpenpgpService } from './pgp.service'; import { decryptText, decryptTextWithKey, encryptText, passToHash } from '../utils/crypto.utils'; export class AuthService { @@ -33,7 +32,7 @@ export class AuthService { }, async generateKeys(password: Password): Promise { const { privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } = - await OpenpgpService.instance.generateNewKeysWithEncrypted(password); + await KeysService.instance.generateNewKeysWithEncrypted(password); const keys: Keys = { privateKeyEncrypted: privateKeyArmoredEncrypted, publicKey: publicKeyArmored, diff --git a/src/services/keys.service.ts b/src/services/keys.service.ts index 165ba3a..73cd023 100644 --- a/src/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -1,92 +1,130 @@ import { aes } from '@internxt/lib'; -import { isValidKey } from '../utils/pgp.utils'; +import * as openpgp from 'openpgp'; import { ConfigService } from './config.service'; -import { OpenpgpService } from './pgp.service'; - -class Base64EncodedPrivateKeyError extends Error { - constructor() { - super('Key is encoded in base64'); - - Object.setPrototypeOf(this, Base64EncodedPrivateKeyError.prototype); - } -} - -class WrongIterationsToEncryptPrivateKeyError extends Error { - constructor() { - super('Key was encrypted using the wrong iterations number'); - - Object.setPrototypeOf(this, WrongIterationsToEncryptPrivateKeyError.prototype); - } -} - -class CorruptedEncryptedPrivateKeyError extends Error { - constructor() { - super('Key is corrupted'); - - Object.setPrototypeOf(this, CorruptedEncryptedPrivateKeyError.prototype); - } -} - -class KeysDoNotMatchError extends Error { - constructor() { - super('Keys do not match'); - - Object.setPrototypeOf(this, CorruptedEncryptedPrivateKeyError.prototype); - } -} +import { + AesInit, + BadEncodedPrivateKeyError, + CorruptedEncryptedPrivateKeyError, + KeysDoNotMatchError, + WrongIterationsToEncryptPrivateKeyError, +} from '../types/keys.types'; export class KeysService { public static readonly instance: KeysService = new KeysService(); + /** + * Validates if the private key can be decrypted with the password + * @param privateKey The private key to validate encrypted + * @param password The password used for encrypting the private key + * @throws {BadEncodedPrivateKeyError} 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 + */ public assertPrivateKeyIsValid = async (privateKey: string, password: string): Promise => { - let privateKeyDecrypted: string; + let privateKeyDecrypted: string | undefined; + let badIterations = true; + try { + aes.decrypt(privateKey, password, 9999); + } catch { + badIterations = false; + } + if (badIterations === true) throw new WrongIterationsToEncryptPrivateKeyError(); + + let badEncrypted = false; try { privateKeyDecrypted = this.decryptPrivateKey(privateKey, password); } catch { - try { - aes.decrypt(privateKey, password, 9999); - } catch { - throw new CorruptedEncryptedPrivateKeyError(); - } + badEncrypted = true; + } - throw new WrongIterationsToEncryptPrivateKeyError(); + let hasValidFormat = false; + try { + if (privateKeyDecrypted !== undefined) { + hasValidFormat = await this.isValidKey(privateKeyDecrypted); + } + } catch { + /* no op */ } - const hasValidFormat = await isValidKey(privateKeyDecrypted); + if (badEncrypted === true) throw new CorruptedEncryptedPrivateKeyError(); + if (hasValidFormat === false) throw new BadEncodedPrivateKeyError(); + }; - if (!hasValidFormat) throw new Base64EncodedPrivateKeyError(); + public encryptPrivateKey = (privateKey: string, password: string): string => { + return aes.encrypt(privateKey, password, this.getAesInitFromEnv()); }; public decryptPrivateKey = (privateKey: string, password: string): string => { return aes.decrypt(privateKey, password); }; + /** + * Validates if a message encrypted with the public key can be decrypted with the private key + * @param privateKey The plain private key + * @param publicKey The plain public key + * @throws {KeysDoNotMatchError} If the keys can not be used together to encrypt/decrypt a message + * @async + */ public assertValidateKeys = async (privateKey: string, publicKey: string): Promise => { - const publicKeyArmored = await OpenpgpService.openpgp.readKey({ armoredKey: publicKey }); - const privateKeyArmored = await OpenpgpService.openpgp.readPrivateKey({ armoredKey: privateKey }); - - 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) { + try { + 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(); + } + } catch { throw new KeysDoNotMatchError(); } }; - public getAesInitFromEnv = (): { iv: string; salt: string } => { + public isValidKey = async (key: string): Promise => { + try { + await openpgp.readKey({ armoredKey: key }); + return true; + } catch (error) { + return false; + } + }; + + /** + * Generates pgp keys adding an AES-encrypted private key property by using a password + * @param password The password for encrypting the private key + * @returns The keys { privateKeyArmored, privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } + * @async + */ + public generateNewKeysWithEncrypted = async (password: string) => { + const { privateKey, publicKey, revocationCertificate } = await openpgp.generateKey({ + userIDs: [{ email: 'inxt@inxt.com' }], + curve: 'ed25519', + }); + + return { + privateKeyArmored: privateKey, + privateKeyArmoredEncrypted: this.encryptPrivateKey(privateKey, password), + publicKeyArmored: Buffer.from(publicKey).toString('base64'), + revocationCertificate: Buffer.from(revocationCertificate).toString('base64'), + }; + }; + + public getAesInitFromEnv = (): AesInit => { const MAGIC_IV = ConfigService.instance.get('APP_MAGIC_IV'); const MAGIC_SALT = ConfigService.instance.get('APP_MAGIC_SALT'); diff --git a/src/services/pgp.service.ts b/src/services/pgp.service.ts deleted file mode 100644 index 2e44878..0000000 --- a/src/services/pgp.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -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; - - public generateNewKeysWithEncrypted = async (password: string) => { - const { privateKeyArmored, publicKeyArmored, revocationCertificate } = await this.generateNewKeys(); - - return { - privateKeyArmored, - privateKeyArmoredEncrypted: aes.encrypt(privateKeyArmored, password, KeysService.instance.getAesInitFromEnv()), - publicKeyArmored, - revocationCertificate, - }; - }; - - 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'), - }; - }; -} diff --git a/src/types/keys.types.ts b/src/types/keys.types.ts new file mode 100644 index 0000000..956a0da --- /dev/null +++ b/src/types/keys.types.ts @@ -0,0 +1,36 @@ +export class BadEncodedPrivateKeyError extends Error { + constructor() { + super('Private key is bad encoded'); + + Object.setPrototypeOf(this, BadEncodedPrivateKeyError.prototype); + } +} + +export class WrongIterationsToEncryptPrivateKeyError extends Error { + constructor() { + super('Private key was encrypted using the wrong iterations number'); + + Object.setPrototypeOf(this, WrongIterationsToEncryptPrivateKeyError.prototype); + } +} + +export class CorruptedEncryptedPrivateKeyError extends Error { + constructor() { + super('Private key is corrupted'); + + Object.setPrototypeOf(this, CorruptedEncryptedPrivateKeyError.prototype); + } +} + +export class KeysDoNotMatchError extends Error { + constructor() { + super('Keys do not match'); + + Object.setPrototypeOf(this, CorruptedEncryptedPrivateKeyError.prototype); + } +} + +export interface AesInit { + iv: string; + salt: string; +} diff --git a/src/utils/pgp.utils.ts b/src/utils/pgp.utils.ts deleted file mode 100644 index 8f39414..0000000 --- a/src/utils/pgp.utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { OpenpgpService } from '../services/pgp.service'; - -export const isValidKey = async (key: string): Promise => { - try { - await OpenpgpService.openpgp.readKey({ armoredKey: key }); - return true; - } catch (error) { - return false; - } -}; diff --git a/test/services/keys.service.test.ts b/test/services/keys.service.test.ts new file mode 100644 index 0000000..db272fc --- /dev/null +++ b/test/services/keys.service.test.ts @@ -0,0 +1,225 @@ +import { expect } from 'chai'; +import sinon, { SinonSandbox } from 'sinon'; +import crypto from 'crypto'; +import { aes } from '@internxt/lib'; +import * as openpgp from 'openpgp'; +import { KeysService } from '../../src/services/keys.service'; +import { ConfigService } from '../../src/services/config.service'; +import { AesInit, CorruptedEncryptedPrivateKeyError } from '../../src/types/keys.types'; + +import { config } from 'dotenv'; +config(); + +describe('Keys service', () => { + let keysServiceSandbox: SinonSandbox; + + const aesInit: AesInit = { + iv: crypto.randomBytes(16).toString('hex'), + salt: crypto.randomBytes(64).toString('hex'), + }; + + beforeEach(() => { + keysServiceSandbox = sinon.createSandbox(); + }); + + afterEach(function () { + keysServiceSandbox.restore(); + }); + + it('When public and private keys are validated, then there is no error thrown', async () => { + keysServiceSandbox.stub(openpgp, 'readKey').resolves(); + keysServiceSandbox.stub(openpgp, 'readPrivateKey').resolves(); + keysServiceSandbox.stub(openpgp, 'createMessage').resolves(); + keysServiceSandbox.stub(openpgp, 'encrypt').resolves(); + keysServiceSandbox.stub(openpgp, 'readMessage').resolves(); + keysServiceSandbox + .stub(openpgp, 'decrypt') + .returns( + Promise.resolve({ data: 'validate-keys' } as openpgp.DecryptMessageResult & { + data: openpgp.MaybeStream; + }), + ); + + await KeysService.instance.assertValidateKeys('dontcareprivate', 'dontcarepublic'); + expect(true).to.be.true; //checks that assertValidateKeys does not throw any error + }); + + it('When public and private keys are not valid, then the validation throws an error', async () => { + try { + await KeysService.instance.assertValidateKeys('privateKey', 'publickey'); + expect(false).to.be.true; //should throw error + } catch { + /* no op */ + } + }); + + it('When there is no error thrown at decryption but it is not working, then the validation throws an error', async () => { + keysServiceSandbox.stub(openpgp, 'readKey').resolves(); + keysServiceSandbox.stub(openpgp, 'readPrivateKey').resolves(); + keysServiceSandbox.stub(openpgp, 'createMessage').resolves(); + keysServiceSandbox.stub(openpgp, 'encrypt').resolves(); + keysServiceSandbox.stub(openpgp, 'readMessage').resolves(); + keysServiceSandbox.stub(openpgp, 'decrypt').resolves(); + //every dependency method resolves (no error thrown), but nothing should be encrypted/decrypted, so the result should not be valid + try { + await KeysService.instance.assertValidateKeys('dontcareprivate', 'dontcarepublic'); + expect(false).to.be.true; //should throw error + } catch (err) { + const error = err as Error; + expect(error.message).to.equal('Keys do not match'); + } + }); + + it('When keys do not match, then it throws a KeysDoNotMatchError', async () => { + keysServiceSandbox.stub(openpgp, 'readKey').resolves(); + keysServiceSandbox.stub(openpgp, 'readPrivateKey').resolves(); + keysServiceSandbox.stub(openpgp, 'createMessage').resolves(); + keysServiceSandbox.stub(openpgp, 'encrypt').resolves(); + keysServiceSandbox.stub(openpgp, 'readMessage').resolves(); + keysServiceSandbox.stub(openpgp, 'decrypt').rejects(); + + //decrypt method throws an exception as it can not decrypt the message (public and private keys do not match) + try { + await KeysService.instance.assertValidateKeys('dontcareprivate', 'dontcarepublic'); + expect(false).to.be.true; //should throw error + } catch (err) { + const error = err as Error; + expect(error.message).to.equal('Keys do not match'); + } + }); + + it('When private key is encrypted with a password and it is validated, then there is no error thrown', async () => { + const plainPrivateKey = crypto.randomBytes(16).toString('hex'); + const password = crypto.randomBytes(8).toString('hex'); + + const encryptedPrivateKey = aes.encrypt(plainPrivateKey, password, aesInit); + + keysServiceSandbox.stub(KeysService.instance, 'decryptPrivateKey').resolves(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + keysServiceSandbox.stub(KeysService.instance, 'isValidKey').returns(Promise.resolve(true)); + + await KeysService.instance.assertPrivateKeyIsValid(encryptedPrivateKey, password); + + expect(true).to.be.true; //checks that assertPrivateKeyIsValid does not throw any error + }); + + it('When private key is encrypted with bad iterations, then it throws a WrongIterationsToEncryptPrivateKey error', async () => { + const plainPrivateKey = crypto.randomBytes(16).toString('hex'); + const password = crypto.randomBytes(8).toString('hex'); + + const badEncryptionPrivateKey = aes.encrypt(plainPrivateKey, password, aesInit, 9999); + + keysServiceSandbox.stub(KeysService.instance, 'decryptPrivateKey').rejects(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + keysServiceSandbox.stub(KeysService.instance, 'isValidKey').returns(Promise.resolve(true)); + + try { + await KeysService.instance.assertPrivateKeyIsValid(badEncryptionPrivateKey, password); + expect(false).to.be.true; //should throw error + } catch (err) { + const error = err as Error; + expect(error.message).to.equal('Private key was encrypted using the wrong iterations number'); + } + }); + + it('When private key is badly encrypted, then it throws a CorruptedEncryptedPrivateKey error', async () => { + const plainPrivateKey = crypto.randomBytes(16).toString('hex'); + const password = crypto.randomBytes(8).toString('hex'); + + const badEncryptionPrivateKey = aes.encrypt(plainPrivateKey, password, aesInit); + + keysServiceSandbox + .stub(KeysService.instance, 'decryptPrivateKey') + .throwsException(CorruptedEncryptedPrivateKeyError); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + keysServiceSandbox.stub(KeysService.instance, 'isValidKey').returns(Promise.resolve(false)); + + try { + await KeysService.instance.assertPrivateKeyIsValid(badEncryptionPrivateKey, password); + expect(false).to.be.true; //should throw error + } catch (err) { + const error = err as Error; + expect(error.message).to.equal('Private key is corrupted'); + } + }); + + it('When private key is bad encoded, then it throws a BadEncodedPrivateKey error', async () => { + const plainPrivateKey = crypto.randomBytes(16).toString('hex'); + const password = crypto.randomBytes(8).toString('hex'); + + const badEncodedPrivateKey = aes.encrypt(plainPrivateKey, password, aesInit); + + keysServiceSandbox.stub(KeysService.instance, 'decryptPrivateKey').resolves(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + keysServiceSandbox.stub(KeysService.instance, 'isValidKey').returns(Promise.resolve(false)); + + try { + await KeysService.instance.assertPrivateKeyIsValid(badEncodedPrivateKey, password); + expect(false).to.be.true; //should throw error + } catch (err) { + const error = err as Error; + expect(error.message).to.equal('Private key is bad encoded'); + } + }); + + it('When the key is not valid, then isValid method returns false', async () => { + keysServiceSandbox.stub(openpgp, 'readKey').rejects(); + + expect(await KeysService.instance.isValidKey('key')).to.be.false; + }); + + it('When the key is valid, then isValid method returns true', async () => { + keysServiceSandbox.stub(openpgp, 'readKey').resolves(); + + expect(await KeysService.instance.isValidKey('key')).to.be.true; + }); + + it('When aes information is required, then it is read from the config service', async () => { + const configServiceInstanceStub = keysServiceSandbox.stub(ConfigService.instance, 'get'); + configServiceInstanceStub.withArgs('APP_MAGIC_IV').returns(aesInit.iv); + configServiceInstanceStub.withArgs('APP_MAGIC_SALT').returns(aesInit.salt); + + const result = KeysService.instance.getAesInitFromEnv(); + expect(result).to.eql(aesInit); + }); + + it('When message is encrypted with private key & password, then it can be decrypted using same data', async () => { + const plainPrivateKey = crypto.randomBytes(16).toString('hex'); + const password = crypto.randomBytes(8).toString('hex'); + + const configServiceInstanceStub = keysServiceSandbox.stub(ConfigService.instance, 'get'); + configServiceInstanceStub.withArgs('APP_MAGIC_IV').returns(aesInit.iv); + configServiceInstanceStub.withArgs('APP_MAGIC_SALT').returns(aesInit.salt); + + const encryptedPrivateKey = KeysService.instance.encryptPrivateKey(plainPrivateKey, password); + const decryptedPrivateKey = KeysService.instance.decryptPrivateKey(encryptedPrivateKey, password); + + expect(decryptedPrivateKey).to.equal(plainPrivateKey); + }); + + it('When new pgp keys are required, then it generates them from the openpgp library', async () => { + const pgpKeys = { + privateKey: crypto.randomBytes(16).toString('hex'), + publicKey: crypto.randomBytes(16).toString('hex'), + revocationCertificate: crypto.randomBytes(16).toString('hex'), + } as unknown as openpgp.KeyPair & { revocationCertificate: string }; + + const pgpKeysWithEncrypted = { + privateKeyArmored: pgpKeys.privateKey, + privateKeyArmoredEncrypted: crypto.randomBytes(16).toString('hex'), + publicKeyArmored: Buffer.from(pgpKeys.publicKey as unknown as string).toString('base64'), + revocationCertificate: Buffer.from(pgpKeys.revocationCertificate).toString('base64'), + }; + + const password = crypto.randomBytes(8).toString('hex'); + + keysServiceSandbox.stub(openpgp, 'generateKey').returns(Promise.resolve(pgpKeys)); + keysServiceSandbox + .stub(KeysService.instance, 'encryptPrivateKey') + .returns(pgpKeysWithEncrypted.privateKeyArmoredEncrypted); + + const newKeys = await KeysService.instance.generateNewKeysWithEncrypted(password); + + expect(newKeys).to.eql(pgpKeysWithEncrypted); + }); +}); diff --git a/yarn.lock b/yarn.lock index f7e4e07..b0b66aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1044,12 +1044,19 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.22" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" + integrity sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@mattiasbuelens/web-streams-adapter@~0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz#607b5a25682f4ae2741da7ba6df39302505336b3" integrity sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" From cda41f0f9769cf70f347b25e1650c3ee468562f3 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 13 Feb 2024 19:37:03 +0100 Subject: [PATCH 15/29] removed unused oclif type --- test/commands/whoami.test.ts | 3 +-- test/types/oclif-test.types.ts | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 test/types/oclif-test.types.ts diff --git a/test/commands/whoami.test.ts b/test/commands/whoami.test.ts index 8706511..ced07aa 100644 --- a/test/commands/whoami.test.ts +++ b/test/commands/whoami.test.ts @@ -1,11 +1,10 @@ import { expect, test } from '@oclif/test'; -import { OclifStdoutContext } from '../types/oclif-test.types'; describe('whoami', () => { test .stdout() .command(['whoami']) - .it('runs whoami', (ctx: OclifStdoutContext) => { + .it('runs whoami', (ctx) => { expect(ctx.stdout).to.contain('You are'); }); }); diff --git a/test/types/oclif-test.types.ts b/test/types/oclif-test.types.ts deleted file mode 100644 index ec75b0c..0000000 --- a/test/types/oclif-test.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Config } from '@oclif/core/lib/interfaces'; -import { Context } from 'fancy-test/lib/types'; - -export type OclifStdoutContext = { config: Config; expectation: string; returned: unknown } & { - readonly stdout: string; -} & Context; From da8907adf209a50228be79c70f2a044cc99efbaa Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Tue, 13 Feb 2024 19:37:34 +0100 Subject: [PATCH 16/29] added config service tests --- test/services/config.service.test.ts | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/services/config.service.test.ts diff --git a/test/services/config.service.test.ts b/test/services/config.service.test.ts new file mode 100644 index 0000000..55a6c7f --- /dev/null +++ b/test/services/config.service.test.ts @@ -0,0 +1,40 @@ +import { expect } from 'chai'; +import crypto from 'crypto'; +import { ConfigService } from '../../src/services/config.service'; + +import { config } from 'dotenv'; +config(); + +const env = Object.assign({}, process.env); + +describe('Config service', () => { + beforeEach(() => { + process.env = env; + }); + + after(() => { + process.env = env; + }); + + it('When an env property is requested, then the get method return its value', async () => { + const envKey = 'APP_CRYPTO_SECRET'; + const envValue = crypto.randomBytes(8).toString('hex'); + process.env[envKey] = envValue; + + const newEnvValue = ConfigService.instance.get(envKey); + expect(newEnvValue).to.equal(envValue); + }); + + it('When an env property that do not have value is requested, then an error is thrown', async () => { + const envKey = 'APP_CRYPTO_SECRET'; + process.env = {}; + + try { + ConfigService.instance.get(envKey); + expect(false).to.be.true; //should throw error + } catch (err) { + const error = err as Error; + expect(error.message).to.equal(`Config key ${envKey} was not found in process.env`); + } + }); +}); From c5ff9cf6efa1c14ce8067ce3e03dd659fb7c0bc6 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 14 Feb 2024 14:18:27 +0100 Subject: [PATCH 17/29] added sinon-chai --- package.json | 2 ++ yarn.lock | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/package.json b/package.json index 2d6dff5..ebebdd1 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@types/crypto-js": "^4.2.2", "@types/mocha": "^10", "@types/node": "^18", + "@types/sinon-chai": "^3.2.12", "chai": "^4", "eslint": "^8", "eslint-config-oclif": "^5", @@ -58,6 +59,7 @@ "oclif": "^4.4.8", "prettier": "^3.2.5", "shx": "^0.3.4", + "sinon-chai": "^3.7.0", "ts-node": "^10.9.2", "typescript": "^5.3.3" }, diff --git a/yarn.lock b/yarn.lock index b0b66aa..a88b8d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2070,6 +2070,14 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564" integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw== +"@types/sinon-chai@^3.2.12": + version "3.2.12" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.12.tgz#c7cb06bee44a534ec84f3a5534c3a3a46fd779b6" + integrity sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + "@types/sinon@*": version "10.0.6" resolved "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz" @@ -6876,6 +6884,11 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sinon-chai@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" + integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== + sinon@^16.1.3: version "16.1.3" resolved "https://registry.yarnpkg.com/sinon/-/sinon-16.1.3.tgz#b760ddafe785356e2847502657b4a0da5501fba8" From 7f8f8697dde751367e42ef339c037b8ad3b6b1be Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 14 Feb 2024 14:19:08 +0100 Subject: [PATCH 18/29] added auth service tests --- src/services/auth.service.ts | 30 ++------- src/services/crypto.service.ts | 71 ++++++++++++++++++++ src/utils/crypto.utils.ts | 47 -------------- test/fixtures/auth.fixture.ts | 27 ++++++++ test/services/auth.service.test.ts | 100 +++++++++++++++++++++++++++++ test/tsconfig.json | 4 +- 6 files changed, 207 insertions(+), 72 deletions(-) create mode 100644 src/services/crypto.service.ts delete mode 100644 src/utils/crypto.utils.ts create mode 100644 test/fixtures/auth.fixture.ts create mode 100644 test/services/auth.service.test.ts diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 2c5afa6..2acf9a4 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -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(); @@ -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 { - 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; @@ -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, @@ -79,7 +61,7 @@ export class AuthService { }; public is2FANeeded = async (email: string): Promise => { - 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'); }); diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts new file mode 100644 index 0000000..f61c764 --- /dev/null +++ b/src/services/crypto.service.ts @@ -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 { + 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); + }; +} diff --git a/src/utils/crypto.utils.ts b/src/utils/crypto.utils.ts deleted file mode 100644 index 54311c1..0000000 --- a/src/utils/crypto.utils.ts +++ /dev/null @@ -1,47 +0,0 @@ -import CryptoJS from 'crypto-js'; -import { ConfigService } from '../services/config.service'; - -interface PassObjectInterface { - salt?: string | null; - password: string; -} - -// Method to hash password. If salt is passed, use it, in other case use crypto lib for generate salt -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 = { - salt: salt.toString(), - hash: hash.toString(), - }; - - return hashedObjetc; -}; - -// AES Plain text encryption method -export const encryptText = (textToEncrypt: string): string => { - const APP_CRYPTO_SECRET = ConfigService.instance.get('APP_CRYPTO_SECRET'); - return encryptTextWithKey(textToEncrypt, APP_CRYPTO_SECRET); -}; - -// AES Plain text decryption method -export const decryptText = (encryptedText: string): string => { - const APP_CRYPTO_SECRET = ConfigService.instance.get('APP_CRYPTO_SECRET'); - return decryptTextWithKey(encryptedText, APP_CRYPTO_SECRET); -}; - -// AES Plain text encryption method with enc. key -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 -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); -}; diff --git a/test/fixtures/auth.fixture.ts b/test/fixtures/auth.fixture.ts new file mode 100644 index 0000000..e5b3c82 --- /dev/null +++ b/test/fixtures/auth.fixture.ts @@ -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, +}; diff --git a/test/services/auth.service.test.ts b/test/services/auth.service.test.ts new file mode 100644 index 0000000..d5d80cd --- /dev/null +++ b/test/services/auth.service.test.ts @@ -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 */ + } + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json index 95898fc..a4ac0c6 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,8 +1,10 @@ { "extends": "../tsconfig", "compilerOptions": { - "noEmit": true + "noEmit": true, + "rootDir": "." }, + "include": ["**/*"], "references": [ {"path": ".."} ] From 3ff871b0893c542d51acff8e72349fb2fdf99880 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 14 Feb 2024 14:20:03 +0100 Subject: [PATCH 19/29] added sdkmanager tests --- src/services/SDKManager.service.ts | 54 +++++++---------- test/services/keys.service.test.ts | 15 ++--- test/services/sdkmanager.service.test.ts | 76 ++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 42 deletions(-) create mode 100644 test/services/sdkmanager.service.test.ts diff --git a/src/services/SDKManager.service.ts b/src/services/SDKManager.service.ts index 3e6eb3d..bd09adb 100644 --- a/src/services/SDKManager.service.ts +++ b/src/services/SDKManager.service.ts @@ -10,17 +10,14 @@ export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; * based on the current apiSecurity details */ export class SdkManager { + public static readonly instance: SdkManager = new SdkManager(); private static apiSecurity?: SdkManagerApiSecurity = undefined; - private static instance: SdkManager = new SdkManager(); + /** * Sets the security details needed to create SDK clients * @param apiSecurity Security properties to be setted */ - static init = (apiSecurity: SdkManagerApiSecurity) => { - SdkManager.setApiSecurity(apiSecurity); - }; - - static setApiSecurity = (apiSecurity: SdkManagerApiSecurity) => { + public static init = (apiSecurity: SdkManagerApiSecurity) => { SdkManager.apiSecurity = apiSecurity; }; @@ -28,14 +25,7 @@ export class SdkManager { SdkManager.apiSecurity = undefined; }; - 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 static getApiSecurity = (config = { throwErrorOnMissingCredentials: true }): SdkManagerApiSecurity => { if (!SdkManager.apiSecurity && config.throwErrorOnMissingCredentials) throw new Error('Api security properties not found in SdkManager'); @@ -50,30 +40,30 @@ export class SdkManager { }; /** Auth SDK */ - get authV2() { + getAuthV2() { const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); - const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); + const apiSecurity = SdkManager.getApiSecurity({ throwErrorOnMissingCredentials: false }); const appDetails = SdkManager.getAppDetails(); return Auth.client(DRIVE_NEW_API_URL, appDetails, apiSecurity); } /** Auth old client SDK */ - get auth() { + getAuth() { const DRIVE_API_URL = ConfigService.instance.get('DRIVE_API_URL'); - const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); + const apiSecurity = SdkManager.getApiSecurity({ throwErrorOnMissingCredentials: false }); const appDetails = SdkManager.getAppDetails(); return Auth.client(DRIVE_API_URL, appDetails, apiSecurity); } /** Payments SDK */ - get payments() { + getPayments() { const PAYMENTS_API_URL = ConfigService.instance.get('PAYMENTS_API_URL'); - const newToken = this.getApiSecurity().newToken; + const newToken = SdkManager.getApiSecurity().newToken; const appDetails = SdkManager.getAppDetails(); return Drive.Payments.client(PAYMENTS_API_URL, appDetails, { @@ -83,40 +73,40 @@ export class SdkManager { } /** Users SDK */ - get users() { + getUsers() { const DRIVE_API_URL = ConfigService.instance.get('DRIVE_API_URL'); - const apiSecurity = this.getApiSecurity({ throwErrorOnMissingCredentials: false }); + const apiSecurity = SdkManager.getApiSecurity({ throwErrorOnMissingCredentials: false }); const appDetails = SdkManager.getAppDetails(); return Drive.Users.client(DRIVE_API_URL, appDetails, apiSecurity); } /** Referrals SDK */ - get referrals() { + getReferrals() { const DRIVE_API_URL = ConfigService.instance.get('DRIVE_API_URL'); - const apiSecurity = this.getApiSecurity(); + const apiSecurity = SdkManager.getApiSecurity(); const appDetails = SdkManager.getAppDetails(); return Drive.Referrals.client(DRIVE_API_URL, appDetails, apiSecurity); } /** Storage SDK */ - get storage() { + getStorage() { const DRIVE_API_URL = ConfigService.instance.get('DRIVE_API_URL'); - const apiSecurity = this.getApiSecurity(); + const apiSecurity = SdkManager.getApiSecurity(); const appDetails = SdkManager.getAppDetails(); return Drive.Storage.client(DRIVE_API_URL, appDetails, apiSecurity); } /** Trash SDK */ - get trash() { + getTrash() { const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); - const newToken = this.getApiSecurity().newToken; + const newToken = SdkManager.getApiSecurity().newToken; const appDetails = SdkManager.getAppDetails(); return Trash.client(DRIVE_NEW_API_URL, appDetails, { @@ -126,19 +116,19 @@ export class SdkManager { } /** Photos SDK */ - get photos() { + getPhotos() { const PHOTOS_API_URL = ConfigService.instance.get('PHOTOS_API_URL'); - const newToken = this.getApiSecurity().newToken; + const newToken = SdkManager.getApiSecurity().newToken; return new photos.Photos(PHOTOS_API_URL, newToken); } /** Share SDK */ - get share() { + getShare() { const DRIVE_NEW_API_URL = ConfigService.instance.get('DRIVE_NEW_API_URL'); - const newToken = this.getApiSecurity().newToken; + const newToken = SdkManager.getApiSecurity().newToken; const appDetails = SdkManager.getAppDetails(); return Drive.Share.client(DRIVE_NEW_API_URL, appDetails, { diff --git a/test/services/keys.service.test.ts b/test/services/keys.service.test.ts index db272fc..47ac3bd 100644 --- a/test/services/keys.service.test.ts +++ b/test/services/keys.service.test.ts @@ -7,9 +7,6 @@ import { KeysService } from '../../src/services/keys.service'; import { ConfigService } from '../../src/services/config.service'; import { AesInit, CorruptedEncryptedPrivateKeyError } from '../../src/types/keys.types'; -import { config } from 'dotenv'; -config(); - describe('Keys service', () => { let keysServiceSandbox: SinonSandbox; @@ -32,13 +29,11 @@ describe('Keys service', () => { keysServiceSandbox.stub(openpgp, 'createMessage').resolves(); keysServiceSandbox.stub(openpgp, 'encrypt').resolves(); keysServiceSandbox.stub(openpgp, 'readMessage').resolves(); - keysServiceSandbox - .stub(openpgp, 'decrypt') - .returns( - Promise.resolve({ data: 'validate-keys' } as openpgp.DecryptMessageResult & { - data: openpgp.MaybeStream; - }), - ); + keysServiceSandbox.stub(openpgp, 'decrypt').returns( + Promise.resolve({ data: 'validate-keys' } as openpgp.DecryptMessageResult & { + data: openpgp.MaybeStream; + }), + ); await KeysService.instance.assertValidateKeys('dontcareprivate', 'dontcarepublic'); expect(true).to.be.true; //checks that assertValidateKeys does not throw any error diff --git a/test/services/sdkmanager.service.test.ts b/test/services/sdkmanager.service.test.ts new file mode 100644 index 0000000..d6e9476 --- /dev/null +++ b/test/services/sdkmanager.service.test.ts @@ -0,0 +1,76 @@ +import chai, { expect } from 'chai'; +import sinon, { SinonSandbox } from 'sinon'; +import sinonChai from 'sinon-chai'; +import crypto from 'crypto'; +import { SdkManager, SdkManagerApiSecurity } from '../../src/services/SDKManager.service'; +import { ConfigKeys } from '../../src/types/config.types'; +import { ConfigService } from '../../src/services/config.service'; +import { AppDetails } from '@internxt/sdk/dist/shared'; + +chai.use(sinonChai); + +describe('SDKManager service', () => { + let sdkManagerServiceSandbox: SinonSandbox; + + beforeEach(() => { + sdkManagerServiceSandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sdkManagerServiceSandbox.restore(); + }); + + it('When SDKManager apiSecurity is requested, then it is returned from static property', () => { + const apiSecurity: SdkManagerApiSecurity = { + newToken: crypto.randomBytes(16).toString('hex'), + token: crypto.randomBytes(16).toString('hex'), + }; + SdkManager.init(apiSecurity); + + expect(SdkManager.getApiSecurity()).to.eql(apiSecurity); + }); + + it('When SDKManager apiSecurity is requested but it is not started, then an error is thrown', () => { + try { + SdkManager.getApiSecurity(); + expect(false).to.be.true; //should throw error + } catch { + /* no op */ + } + }); + + it('When SDKManager is cleaned, then apiSecurity property is cleaned', () => { + const apiSecurity: SdkManagerApiSecurity = { + newToken: crypto.randomBytes(16).toString('hex'), + token: crypto.randomBytes(16).toString('hex'), + }; + SdkManager.init(apiSecurity); + SdkManager.clean(); + try { + SdkManager.getApiSecurity({ throwErrorOnMissingCredentials: true }); + expect(false).to.be.true; //should throw error + } catch { + /* no op */ + } + const apiSecurityResponse = SdkManager.getApiSecurity({ throwErrorOnMissingCredentials: false }); + expect(apiSecurityResponse).to.be.undefined; + }); + + it('When AuthV2 client is requested, then it is generated using internxt sdk', () => { + const apiSecurity: SdkManagerApiSecurity = { + newToken: crypto.randomBytes(16).toString('hex'), + token: crypto.randomBytes(16).toString('hex'), + }; + const appDetails: AppDetails = { + clientName: crypto.randomBytes(16).toString('hex'), + clientVersion: crypto.randomBytes(16).toString('hex'), + }; + const envEndpoint: { key: keyof ConfigKeys; value: string } = { key: 'DRIVE_NEW_API_URL', value: 'test/api' }; + SdkManager.init(apiSecurity); + + sdkManagerServiceSandbox.stub(ConfigService.instance, 'get').withArgs(envEndpoint.key).returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + }); +}); From 2b4efff57e19958696c5d1e1b4397682890bc971 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Wed, 14 Feb 2024 17:29:18 +0100 Subject: [PATCH 20/29] added missing tests from sdkmanager --- src/services/SDKManager.service.ts | 2 +- test/services/sdkmanager.service.test.ts | 243 +++++++++++++++++++++-- 2 files changed, 232 insertions(+), 13 deletions(-) diff --git a/src/services/SDKManager.service.ts b/src/services/SDKManager.service.ts index bd09adb..5629651 100644 --- a/src/services/SDKManager.service.ts +++ b/src/services/SDKManager.service.ts @@ -32,7 +32,7 @@ export class SdkManager { return SdkManager.apiSecurity as SdkManagerApiSecurity; }; - private static getAppDetails = (): AppDetails => { + public static getAppDetails = (): AppDetails => { return { clientName: packageJson.name, clientVersion: packageJson.version, diff --git a/test/services/sdkmanager.service.test.ts b/test/services/sdkmanager.service.test.ts index d6e9476..e315a1c 100644 --- a/test/services/sdkmanager.service.test.ts +++ b/test/services/sdkmanager.service.test.ts @@ -2,16 +2,28 @@ import chai, { expect } from 'chai'; import sinon, { SinonSandbox } from 'sinon'; import sinonChai from 'sinon-chai'; import crypto from 'crypto'; +import { Auth, Drive, photos } from '@internxt/sdk'; +import { Trash } from '@internxt/sdk/dist/drive'; import { SdkManager, SdkManagerApiSecurity } from '../../src/services/SDKManager.service'; import { ConfigKeys } from '../../src/types/config.types'; import { ConfigService } from '../../src/services/config.service'; import { AppDetails } from '@internxt/sdk/dist/shared'; +import packageJson from '../../package.json'; chai.use(sinonChai); describe('SDKManager service', () => { let sdkManagerServiceSandbox: SinonSandbox; + const apiSecurity: SdkManagerApiSecurity = { + newToken: crypto.randomBytes(16).toString('hex'), + token: crypto.randomBytes(16).toString('hex'), + }; + const appDetails: AppDetails = { + clientName: crypto.randomBytes(16).toString('hex'), + clientVersion: crypto.randomBytes(16).toString('hex'), + }; + beforeEach(() => { sdkManagerServiceSandbox = sinon.createSandbox(); }); @@ -40,10 +52,6 @@ describe('SDKManager service', () => { }); it('When SDKManager is cleaned, then apiSecurity property is cleaned', () => { - const apiSecurity: SdkManagerApiSecurity = { - newToken: crypto.randomBytes(16).toString('hex'), - token: crypto.randomBytes(16).toString('hex'), - }; SdkManager.init(apiSecurity); SdkManager.clean(); try { @@ -56,21 +64,232 @@ describe('SDKManager service', () => { expect(apiSecurityResponse).to.be.undefined; }); + it('When getAppDetails is requested, then it is generated using packageJson values', () => { + const expectedAppdetails = { + clientName: packageJson.name, + clientVersion: packageJson.version, + }; + /*sdkManagerServiceSandbox.stub(packageJson, 'name').returns(appDetails.clientName); + sdkManagerServiceSandbox.stub(packageJson, 'version').returns(appDetails.clientVersion);*/ + + const appDetailsResponse = SdkManager.getAppDetails(); + expect(expectedAppdetails).to.eql(appDetailsResponse); + }); + it('When AuthV2 client is requested, then it is generated using internxt sdk', () => { - const apiSecurity: SdkManagerApiSecurity = { - newToken: crypto.randomBytes(16).toString('hex'), - token: crypto.randomBytes(16).toString('hex'), + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'DRIVE_NEW_API_URL', + value: 'test/api', + }; + SdkManager.init(apiSecurity); + + const authClientV2 = Auth.client(envEndpoint.value, appDetails, apiSecurity); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + sdkManagerServiceSandbox.stub(Auth, 'client').returns(authClientV2); + + const authV2 = SdkManager.instance.getAuthV2(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(authV2).to.eql(authClientV2); + }); + + it('When Auth client is requested, then it is generated using internxt sdk', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'DRIVE_API_URL', + value: 'test/api', + }; + SdkManager.init(apiSecurity); + + const authClient = Auth.client(envEndpoint.value, appDetails, apiSecurity); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + sdkManagerServiceSandbox.stub(Auth, 'client').returns(authClient); + + const auth = SdkManager.instance.getAuth(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(auth).to.eql(authClient); + }); + + it('When Payments client is requested, then it is generated using internxt sdk', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'PAYMENTS_API_URL', + value: 'test/api', }; - const appDetails: AppDetails = { - clientName: crypto.randomBytes(16).toString('hex'), - clientVersion: crypto.randomBytes(16).toString('hex'), + SdkManager.init(apiSecurity); + + const client = Drive.Payments.client(envEndpoint.value, appDetails, apiSecurity); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + sdkManagerServiceSandbox.stub(Drive.Payments, 'client').returns(client); + + const newClient = SdkManager.instance.getPayments(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(newClient).to.eql(client); + }); + + it('When Users client is requested, then it is generated using internxt sdk', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'DRIVE_API_URL', + value: 'test/api', }; - const envEndpoint: { key: keyof ConfigKeys; value: string } = { key: 'DRIVE_NEW_API_URL', value: 'test/api' }; SdkManager.init(apiSecurity); - sdkManagerServiceSandbox.stub(ConfigService.instance, 'get').withArgs(envEndpoint.key).returns(envEndpoint.value); + const client = Drive.Users.client(envEndpoint.value, appDetails, apiSecurity); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); // eslint-disable-next-line @typescript-eslint/no-explicit-any sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + sdkManagerServiceSandbox.stub(Drive.Users, 'client').returns(client); + + const newClient = SdkManager.instance.getUsers(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(newClient).to.eql(client); + }); + + it('When Referrals client is requested, then it is generated using internxt sdk', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'DRIVE_API_URL', + value: 'test/api', + }; + SdkManager.init(apiSecurity); + + const client = Drive.Referrals.client(envEndpoint.value, appDetails, apiSecurity); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + sdkManagerServiceSandbox.stub(Drive.Referrals, 'client').returns(client); + + const newClient = SdkManager.instance.getReferrals(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(newClient).to.eql(client); + }); + + it('When Storage client is requested, then it is generated using internxt sdk', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'DRIVE_API_URL', + value: 'test/api', + }; + SdkManager.init(apiSecurity); + + const client = Drive.Storage.client(envEndpoint.value, appDetails, apiSecurity); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + sdkManagerServiceSandbox.stub(Drive.Storage, 'client').returns(client); + + const newClient = SdkManager.instance.getStorage(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(newClient).to.eql(client); + }); + + it('When Trash client is requested, then it is generated using internxt sdk', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'DRIVE_NEW_API_URL', + value: 'test/api', + }; + SdkManager.init(apiSecurity); + + const client = Trash.client(envEndpoint.value, appDetails, apiSecurity); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + sdkManagerServiceSandbox.stub(Trash, 'client').returns(client); + + const newClient = SdkManager.instance.getTrash(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(newClient).to.eql(client); + }); + + it('When Photos client is requested, then it is generated using internxt sdk', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'PHOTOS_API_URL', + value: 'test/api', + }; + SdkManager.init(apiSecurity); + + const client = new photos.Photos(envEndpoint.value, apiSecurity.newToken); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + + sdkManagerServiceSandbox.stub(photos.Photos, 'prototype').returns(client); + + const newClient = SdkManager.instance.getPhotos(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(newClient).to.eql(client); + }); + + it('When Share client is requested, then it is generated using internxt sdk', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'DRIVE_NEW_API_URL', + value: 'test/api', + }; + SdkManager.init(apiSecurity); + + const client = Drive.Share.client(envEndpoint.value, appDetails, apiSecurity); + + const spyConfigService = sdkManagerServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + sdkManagerServiceSandbox.stub(SdkManager, 'getApiSecurity').returns(apiSecurity); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sdkManagerServiceSandbox.stub(SdkManager, 'getAppDetails').returns(appDetails); + sdkManagerServiceSandbox.stub(Drive.Share, 'client').returns(client); + + const newClient = SdkManager.instance.getShare(); + + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + expect(newClient).to.eql(client); }); }); From d8ef2d2e3eed9fd93822e81853f7122bb9e014fa Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 09:54:16 +0100 Subject: [PATCH 21/29] improved keys service tests --- src/services/keys.service.ts | 44 +++++++++++------------- test/services/auth.service.test.ts | 2 +- test/services/keys.service.test.ts | 36 +++++++++++++++---- test/services/sdkmanager.service.test.ts | 2 +- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/services/keys.service.ts b/src/services/keys.service.ts index 73cd023..c469b8f 100644 --- a/src/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -68,29 +68,25 @@ export class KeysService { * @async */ public assertValidateKeys = async (privateKey: string, publicKey: string): Promise => { - try { - 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(); - } - } catch { + 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(); } }; @@ -99,7 +95,7 @@ export class KeysService { try { await openpgp.readKey({ armoredKey: key }); return true; - } catch (error) { + } catch { return false; } }; diff --git a/test/services/auth.service.test.ts b/test/services/auth.service.test.ts index d5d80cd..43bf916 100644 --- a/test/services/auth.service.test.ts +++ b/test/services/auth.service.test.ts @@ -15,7 +15,7 @@ describe('Auth service', () => { authServiceSandbox = sinon.createSandbox(); }); - afterEach(function () { + afterEach(() => { authServiceSandbox.restore(); }); diff --git a/test/services/keys.service.test.ts b/test/services/keys.service.test.ts index 47ac3bd..b6c9c27 100644 --- a/test/services/keys.service.test.ts +++ b/test/services/keys.service.test.ts @@ -19,7 +19,7 @@ describe('Keys service', () => { keysServiceSandbox = sinon.createSandbox(); }); - afterEach(function () { + afterEach(() => { keysServiceSandbox.restore(); }); @@ -48,13 +48,17 @@ describe('Keys service', () => { } }); - it('When there is no error thrown at decryption but it is not working, then the validation throws an error', async () => { + it('When keys can be used to decrypt but they are not working good to encrypt/decrypt, then the validation throws an error', async () => { keysServiceSandbox.stub(openpgp, 'readKey').resolves(); keysServiceSandbox.stub(openpgp, 'readPrivateKey').resolves(); keysServiceSandbox.stub(openpgp, 'createMessage').resolves(); keysServiceSandbox.stub(openpgp, 'encrypt').resolves(); keysServiceSandbox.stub(openpgp, 'readMessage').resolves(); - keysServiceSandbox.stub(openpgp, 'decrypt').resolves(); + keysServiceSandbox.stub(openpgp, 'decrypt').returns( + Promise.resolve({ data: 'bad-validation' } as openpgp.DecryptMessageResult & { + data: openpgp.MaybeStream; + }), + ); //every dependency method resolves (no error thrown), but nothing should be encrypted/decrypted, so the result should not be valid try { await KeysService.instance.assertValidateKeys('dontcareprivate', 'dontcarepublic'); @@ -65,21 +69,39 @@ describe('Keys service', () => { } }); - it('When keys do not match, then it throws a KeysDoNotMatchError', async () => { + it('When encryption fails, then it throws an error', async () => { + keysServiceSandbox.stub(openpgp, 'readKey').resolves(); + keysServiceSandbox.stub(openpgp, 'readPrivateKey').resolves(); + keysServiceSandbox.stub(openpgp, 'createMessage').resolves(); + keysServiceSandbox.stub(openpgp, 'encrypt').rejects(new Error('Encryption failed')); + keysServiceSandbox.stub(openpgp, 'readMessage').resolves(); + keysServiceSandbox.stub(openpgp, 'decrypt').resolves(); + + //encrypt method throws an exception as it can not encrypt the message (something with the encryptionKeys is bad) + try { + await KeysService.instance.assertValidateKeys('dontcareprivate', 'dontcarepublic'); + expect(false).to.be.true; //should throw error + } catch (err) { + const error = err as Error; + expect(error.message).to.equal('Encryption failed'); + } + }); + + it('When decryption fails, then it throws an error', async () => { keysServiceSandbox.stub(openpgp, 'readKey').resolves(); keysServiceSandbox.stub(openpgp, 'readPrivateKey').resolves(); keysServiceSandbox.stub(openpgp, 'createMessage').resolves(); keysServiceSandbox.stub(openpgp, 'encrypt').resolves(); keysServiceSandbox.stub(openpgp, 'readMessage').resolves(); - keysServiceSandbox.stub(openpgp, 'decrypt').rejects(); + keysServiceSandbox.stub(openpgp, 'decrypt').rejects(new Error('Decryption failed')); - //decrypt method throws an exception as it can not decrypt the message (public and private keys do not match) + //decrypt method throws an exception as it can not decrypt the message (something with the decryptionKeys is bad) try { await KeysService.instance.assertValidateKeys('dontcareprivate', 'dontcarepublic'); expect(false).to.be.true; //should throw error } catch (err) { const error = err as Error; - expect(error.message).to.equal('Keys do not match'); + expect(error.message).to.equal('Decryption failed'); } }); diff --git a/test/services/sdkmanager.service.test.ts b/test/services/sdkmanager.service.test.ts index e315a1c..e34e815 100644 --- a/test/services/sdkmanager.service.test.ts +++ b/test/services/sdkmanager.service.test.ts @@ -28,7 +28,7 @@ describe('SDKManager service', () => { sdkManagerServiceSandbox = sinon.createSandbox(); }); - afterEach(function () { + afterEach(() => { sdkManagerServiceSandbox.restore(); }); From 3c1fad00fe3714c9a9496c8ab5ea32a87a19e22e Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 10:33:23 +0100 Subject: [PATCH 22/29] added crypto service tests --- src/services/crypto.service.ts | 18 ++-- test/services/crypto.service.test.ts | 130 +++++++++++++++++++++++++++ test/services/keys.service.test.ts | 2 +- 3 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 test/services/crypto.service.test.ts diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index f61c764..5bac69a 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,13 +1,10 @@ import { CryptoProvider } from '@internxt/sdk'; import { Keys, Password } from '@internxt/sdk/dist/auth'; +import crypto from 'crypto'; +import CryptoJS from 'crypto-js'; 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(); @@ -30,14 +27,13 @@ export class CryptoService { }; // 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 }); + public passToHash = (passObject: { salt?: string | null; password: string }): { salt: string; hash: string } => { + const salt = passObject.salt ? passObject.salt : crypto.randomBytes(128 / 8).toString('hex'); + const hash = crypto.pbkdf2Sync(passObject.password, salt, 10000, 256 / 8, 'sha256').toString('hex'); const hashedObjetc = { - salt: salt.toString(), - hash: hash.toString(), + salt, + hash, }; - return hashedObjetc; }; diff --git a/test/services/crypto.service.test.ts b/test/services/crypto.service.test.ts new file mode 100644 index 0000000..4fdc0da --- /dev/null +++ b/test/services/crypto.service.test.ts @@ -0,0 +1,130 @@ +import chai, { expect } from 'chai'; +import sinon, { SinonSandbox } from 'sinon'; +import sinonChai from 'sinon-chai'; +import crypto from 'crypto'; +import CryptoJS from 'crypto-js'; +import { ConfigService } from '../../src/services/config.service'; +import { CryptoService } from '../../src/services/crypto.service'; +import { ConfigKeys } from '../../src/types/config.types'; +import { AesInit } from '../../src/types/keys.types'; +import { Keys } from '@internxt/sdk'; +import { KeysService } from '../../src/services/keys.service'; + +chai.use(sinonChai); + +describe('Crypto service', () => { + let cryptoServiceSandbox: SinonSandbox; + + beforeEach(() => { + cryptoServiceSandbox = sinon.createSandbox(); + }); + + afterEach(() => { + cryptoServiceSandbox.restore(); + }); + + it('When text is encrypted using crypto secret env, then it can be decrypted back', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'APP_CRYPTO_SECRET', + value: crypto.randomBytes(16).toString('hex'), + }; + const textToEncrypt = crypto.randomBytes(16).toString('hex'); + + const spyConfigService = cryptoServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + + const textEncrypted = CryptoService.instance.encryptText(textToEncrypt); + const textDecrypted = CryptoService.instance.decryptText(textEncrypted); + expect(textDecrypted).to.be.equal(textToEncrypt); + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + }); + + it('When text is encrypted using crypto secret env, then it can be decrypted back', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'APP_CRYPTO_SECRET', + value: crypto.randomBytes(16).toString('hex'), + }; + const textToEncrypt = crypto.randomBytes(16).toString('hex'); + + const spyConfigService = cryptoServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + + const textEncrypted = CryptoService.instance.encryptText(textToEncrypt); + const textDecrypted = CryptoService.instance.decryptText(textEncrypted); + expect(textDecrypted).to.be.equal(textToEncrypt); + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + }); + + it('When a password is hashed using CryptoProvider, then it is hashed correctly', () => { + const envEndpoint: { key: keyof ConfigKeys; value: string } = { + key: 'APP_CRYPTO_SECRET', + value: crypto.randomBytes(16).toString('hex'), + }; + const spyConfigService = cryptoServiceSandbox + .stub(ConfigService.instance, 'get') + .withArgs(envEndpoint.key) + .returns(envEndpoint.value); + + const password = { + value: crypto.randomBytes(16).toString('hex'), + salt: crypto.randomBytes(16).toString('hex'), + }; + + /* + // used to migrate from discontinued CryptoJS to nodejs crypto + const pass1 = CryptoJS.PBKDF2(password.value, password.salt, { keySize: 256 / 32, iterations: 10000 }).toString(); + const pass2 = crypto.pbkdf2Sync(password.value, password.salt, 10000, 256 / 8, 'sha256').toString('hex'); + expect(pass1).to.equal(pass2); + expect(CryptoJS.lib.WordArray.random(128 / 8).toString().length).to.equal(crypto.randomBytes(128 / 8).toString('hex').length); + */ + + const encryptedSalt = CryptoService.instance.encryptText(password.salt); + const hashedAndEncryptedPassword = CryptoService.cryptoProvider.encryptPasswordHash(password.value, encryptedSalt); + const hashedPassword = CryptoService.instance.decryptText(hashedAndEncryptedPassword); + + const expectedHashedPassword = crypto + .pbkdf2Sync(password.value, password.salt, 10000, 256 / 8, 'sha256') + .toString('hex'); + + expect(hashedPassword).to.be.equal(expectedHashedPassword); + expect(spyConfigService).to.be.calledWith(envEndpoint.key); + }); + + it('When a password is hashed using passToHash without salt, then it is hashed with a new generated salt', () => { + const password = crypto.randomBytes(16).toString('hex'); + + const hashedPassword = CryptoService.instance.passToHash({ password }); + + expect(hashedPassword.hash.length).to.be.equal(64); + expect(hashedPassword.salt.length).to.be.equal(32); + }); + + it('When auth keys are generated using CryptoProvider, then they are generated using KeysService', async () => { + const password = crypto.randomBytes(8).toString('hex'); + const keysReturned = { + privateKeyArmored: crypto.randomBytes(16).toString('hex'), + privateKeyArmoredEncrypted: crypto.randomBytes(16).toString('hex'), + publicKeyArmored: crypto.randomBytes(16).toString('hex'), + revocationCertificate: crypto.randomBytes(16).toString('hex'), + }; + + const kerysServiceStub = cryptoServiceSandbox + .stub(KeysService.instance, 'generateNewKeysWithEncrypted') + .returns(Promise.resolve(keysReturned)); + + const expectedKeys: Keys = { + privateKeyEncrypted: keysReturned.privateKeyArmoredEncrypted, + publicKey: keysReturned.publicKeyArmored, + revocationCertificate: keysReturned.revocationCertificate, + }; + + const resultedKeys = await CryptoService.cryptoProvider.generateKeys(password); + + expect(expectedKeys).to.be.eql(resultedKeys); + expect(kerysServiceStub).to.be.calledWith(password); + }); +}); diff --git a/test/services/keys.service.test.ts b/test/services/keys.service.test.ts index b6c9c27..d9274d3 100644 --- a/test/services/keys.service.test.ts +++ b/test/services/keys.service.test.ts @@ -224,7 +224,7 @@ describe('Keys service', () => { const pgpKeysWithEncrypted = { privateKeyArmored: pgpKeys.privateKey, privateKeyArmoredEncrypted: crypto.randomBytes(16).toString('hex'), - publicKeyArmored: Buffer.from(pgpKeys.publicKey as unknown as string).toString('base64'), + publicKeyArmored: Buffer.from(String(pgpKeys.publicKey)).toString('base64'), revocationCertificate: Buffer.from(pgpKeys.revocationCertificate).toString('base64'), }; From b6a6d84ffac4b9fa91105dc63614cff32eabf650 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 13:28:31 +0100 Subject: [PATCH 23/29] migrated cryptojs deprecated dependency to nodejs crypto --- package.json | 2 - src/services/crypto.service.ts | 63 +++++++++++++++++++++++----- src/services/keys.service.ts | 30 +++++++++++-- test/services/crypto.service.test.ts | 10 ----- yarn.lock | 10 ----- 5 files changed, 78 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index ebebdd1..93c8cf2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "@internxt/prettier-config": "^1.0.2", "@internxt/sdk": "^1.4.70", "@oclif/core": "^3", - "crypto-js": "^4.2.0", "dotenv": "^16.4.1", "openpgp": "^5.11.0" }, @@ -43,7 +42,6 @@ "@oclif/test": "^3", "@openpgp/web-stream-tools": "0.0.11-patch-0", "@types/chai": "^4", - "@types/crypto-js": "^4.2.2", "@types/mocha": "^10", "@types/node": "^18", "@types/sinon-chai": "^3.2.12", diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index 5bac69a..b750161 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -1,7 +1,6 @@ import { CryptoProvider } from '@internxt/sdk'; import { Keys, Password } from '@internxt/sdk/dist/auth'; import crypto from 'crypto'; -import CryptoJS from 'crypto-js'; import { KeysService } from './keys.service'; import { ConfigService } from '../services/config.service'; @@ -49,19 +48,61 @@ export class CryptoService { 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); + /** + * Encrypts a plain message into an AES encrypted text + * [Crypto.JS compatible] (deprecated dependency) + * @param textToEncrypt The plain text to be encrypted + * @param secret The secret used to encrypt + * @returns The encrypted private key in 'hex' encoding + **/ + public encryptTextWithKey = (textToEncrypt: string, secret: string) => { + const TRANSFORM_ROUNDS = 3; + const openSSLstart = Buffer.from('Salted__'); + const salt = crypto.randomBytes(8); + const password = Buffer.concat([Buffer.from(secret, 'binary'), salt]); + const md5Hashes = []; - return text64.toString(CryptoJS.enc.Hex); + let digest = password; + + for (let i = 0; i < TRANSFORM_ROUNDS; i++) { + md5Hashes[i] = crypto.createHash('md5').update(digest).digest(); + digest = Buffer.concat([md5Hashes[i], password]); + } + + const key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); + const iv = md5Hashes[2]; + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + + const encrypted = Buffer.concat([cipher.update(textToEncrypt, 'utf8'), cipher.final()]); + return Buffer.concat([openSSLstart, salt, encrypted]).toString('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); + /** + * Decrypts an AES encrypted text + * [Crypto.JS compatible] (deprecated dependency) + * @param encryptedText The AES encrypted text in 'HEX' encoding + * @param secret The secret used to encrypt + * @returns The decrypted private key in 'utf8' encoding + **/ + public decryptTextWithKey = (encryptedText: string, secret: string) => { + const TRANSFORM_ROUNDS = 3; + const cypher = Buffer.from(encryptedText, 'hex'); + const salt = cypher.subarray(8, 16); + const password = Buffer.concat([Buffer.from(secret, 'binary'), salt]); + const md5Hashes = []; + + let digest = password; + + for (let i = 0; i < TRANSFORM_ROUNDS; i++) { + md5Hashes[i] = crypto.createHash('md5').update(digest).digest(); + digest = Buffer.concat([md5Hashes[i], password]); + } + + const key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); + const iv = md5Hashes[2]; + const contents = cypher.subarray(16); + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); - return bytes.toString(CryptoJS.enc.Utf8); + return Buffer.concat([decipher.update(contents), decipher.final()]).toString('utf8'); }; } diff --git a/src/services/keys.service.ts b/src/services/keys.service.ts index c469b8f..86e64f9 100644 --- a/src/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -14,8 +14,8 @@ export class KeysService { /** * Validates if the private key can be decrypted with the password - * @param privateKey The private key to validate encrypted - * @param password The password used for encrypting the private key + * @param privateKey The encrypted private key + * @param password The password used to encrypt the private key * @throws {BadEncodedPrivateKeyError} 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) @@ -52,10 +52,22 @@ export class KeysService { if (hasValidFormat === false) throw new BadEncodedPrivateKeyError(); }; + /** + * Encrypts private key using password + * @param privateKey The plain private key + * @param password The password to encrypt + * @returns The encrypted private key + **/ public encryptPrivateKey = (privateKey: string, password: string): string => { return aes.encrypt(privateKey, password, this.getAesInitFromEnv()); }; + /** + * Decrypts private key using password + * @param privateKey The encrypted private key + * @param password The password used to encrypt the private key + * @returns The decrypted private key + **/ public decryptPrivateKey = (privateKey: string, password: string): string => { return aes.decrypt(privateKey, password); }; @@ -66,7 +78,7 @@ export class KeysService { * @param publicKey The plain public key * @throws {KeysDoNotMatchError} If the keys can not be used together to encrypt/decrypt a message * @async - */ + **/ public assertValidateKeys = async (privateKey: string, publicKey: string): Promise => { const publicKeyArmored = await openpgp.readKey({ armoredKey: publicKey }); const privateKeyArmored = await openpgp.readPrivateKey({ armoredKey: privateKey }); @@ -91,6 +103,12 @@ export class KeysService { } }; + /** + * Checks if a pgp key can be read + * @param key The openpgp key to be validated + * @returns True if it can be read, false otherwise + * @async + **/ public isValidKey = async (key: string): Promise => { try { await openpgp.readKey({ armoredKey: key }); @@ -105,7 +123,7 @@ export class KeysService { * @param password The password for encrypting the private key * @returns The keys { privateKeyArmored, privateKeyArmoredEncrypted, publicKeyArmored, revocationCertificate } * @async - */ + **/ public generateNewKeysWithEncrypted = async (password: string) => { const { privateKey, publicKey, revocationCertificate } = await openpgp.generateKey({ userIDs: [{ email: 'inxt@inxt.com' }], @@ -120,6 +138,10 @@ export class KeysService { }; }; + /** + * Returns the AesInit params using ConfigService + * @returns The IV and the SALT from ConfigService + **/ public getAesInitFromEnv = (): AesInit => { const MAGIC_IV = ConfigService.instance.get('APP_MAGIC_IV'); const MAGIC_SALT = ConfigService.instance.get('APP_MAGIC_SALT'); diff --git a/test/services/crypto.service.test.ts b/test/services/crypto.service.test.ts index 4fdc0da..2755597 100644 --- a/test/services/crypto.service.test.ts +++ b/test/services/crypto.service.test.ts @@ -2,11 +2,9 @@ import chai, { expect } from 'chai'; import sinon, { SinonSandbox } from 'sinon'; import sinonChai from 'sinon-chai'; import crypto from 'crypto'; -import CryptoJS from 'crypto-js'; import { ConfigService } from '../../src/services/config.service'; import { CryptoService } from '../../src/services/crypto.service'; import { ConfigKeys } from '../../src/types/config.types'; -import { AesInit } from '../../src/types/keys.types'; import { Keys } from '@internxt/sdk'; import { KeysService } from '../../src/services/keys.service'; @@ -74,14 +72,6 @@ describe('Crypto service', () => { salt: crypto.randomBytes(16).toString('hex'), }; - /* - // used to migrate from discontinued CryptoJS to nodejs crypto - const pass1 = CryptoJS.PBKDF2(password.value, password.salt, { keySize: 256 / 32, iterations: 10000 }).toString(); - const pass2 = crypto.pbkdf2Sync(password.value, password.salt, 10000, 256 / 8, 'sha256').toString('hex'); - expect(pass1).to.equal(pass2); - expect(CryptoJS.lib.WordArray.random(128 / 8).toString().length).to.equal(crypto.randomBytes(128 / 8).toString('hex').length); - */ - const encryptedSalt = CryptoService.instance.encryptText(password.salt); const hashedAndEncryptedPassword = CryptoService.cryptoProvider.encryptPasswordHash(password.value, encryptedSalt); const hashedPassword = CryptoService.instance.decryptText(hashedAndEncryptedPassword); diff --git a/yarn.lock b/yarn.lock index a88b8d2..cd363e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1984,11 +1984,6 @@ dependencies: "@types/node" "*" -"@types/crypto-js@^4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea" - integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ== - "@types/expect@^1.20.4": version "1.20.4" resolved "https://registry.yarnpkg.com/@types/expect/-/expect-1.20.4.tgz#8288e51737bf7e3ab5d7c77bfa695883745264e5" @@ -3196,11 +3191,6 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" -crypto-js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" From ee9e65399cbf4665e4e7aa396a2035732d714af1 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 15:43:38 +0100 Subject: [PATCH 24/29] added descriptions --- src/services/SDKManager.service.ts | 19 ++++++++++++++++--- src/services/auth.service.ts | 17 ++++++++++++++++- src/services/config.service.ts | 6 ++++++ src/services/keys.service.ts | 8 ++++---- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/services/SDKManager.service.ts b/src/services/SDKManager.service.ts index 5629651..54ee8dd 100644 --- a/src/services/SDKManager.service.ts +++ b/src/services/SDKManager.service.ts @@ -11,20 +11,29 @@ export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; */ export class SdkManager { public static readonly instance: SdkManager = new SdkManager(); - private static apiSecurity?: SdkManagerApiSecurity = undefined; + private static apiSecurity?: SdkManagerApiSecurity; /** - * Sets the security details needed to create SDK clients + * Sets the security details needed to create SDK clients * @param apiSecurity Security properties to be setted - */ + **/ public static init = (apiSecurity: SdkManagerApiSecurity) => { SdkManager.apiSecurity = apiSecurity; }; + /** + * Cleans the security details + **/ static clean = () => { SdkManager.apiSecurity = undefined; }; + /** + * Returns the security details needed to create SDK clients + * @param config Config object to handle error throwing when there is not apiSecurity defined + * @throws {Error} When throwErrorOnMissingCredentials is setted to true and there is not apiSecurity defined + * @returns The SDK Manager api security details + **/ public static getApiSecurity = (config = { throwErrorOnMissingCredentials: true }): SdkManagerApiSecurity => { if (!SdkManager.apiSecurity && config.throwErrorOnMissingCredentials) throw new Error('Api security properties not found in SdkManager'); @@ -32,6 +41,10 @@ export class SdkManager { return SdkManager.apiSecurity as SdkManagerApiSecurity; }; + /** + * Returns the application details from package.json + * @returns The name and the version of the app from package.json + **/ public static getAppDetails = (): AppDetails => { return { clientName: packageJson.name, diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 2acf9a4..45186ad 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -7,10 +7,18 @@ import { CryptoService } from './crypto.service'; export class AuthService { public static readonly instance: AuthService = new AuthService(); + /** + * Login with user credentials and returns its tokens and properties + * @param email The user's email + * @param password The user's password + * @param twoFactorCode (Optional) The temporal two factor auth code + * @returns The user's properties and the tokens needed for auth + * @async + **/ public doLogin = async ( email: string, password: string, - twoFactorCode: string, + twoFactorCode?: string, ): Promise<{ user: UserSettings; token: string; @@ -60,6 +68,13 @@ export class AuthService { } }; + /** + * Checks from user's security details if it has enabled two factor auth + * @param email The user's email + * @throws {Error} If auth.securityDetails endpoint fails + * @returns True if user has enabled two factor auth + * @async + **/ public is2FANeeded = async (email: string): Promise => { const authClient = SdkManager.instance.getAuth(); const securityDetails = await authClient.securityDetails(email).catch((error) => { diff --git a/src/services/config.service.ts b/src/services/config.service.ts index 25d3444..22e2894 100644 --- a/src/services/config.service.ts +++ b/src/services/config.service.ts @@ -2,6 +2,12 @@ import { ConfigKeys } from '../types/config.types'; export class ConfigService { public static readonly instance: ConfigService = new ConfigService(); + /** + * Gets the value from an environment key + * @param key The environment key to retrieve + * @throws {Error} If key is not found in process.env + * @returns The value from the environment variable + **/ 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`); diff --git a/src/services/keys.service.ts b/src/services/keys.service.ts index 86e64f9..6cfe1aa 100644 --- a/src/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -13,7 +13,7 @@ export class KeysService { public static readonly instance: KeysService = new KeysService(); /** - * Validates if the private key can be decrypted with the password + * Checks if a private key can be decrypted with a password, otherwise it throws an error * @param privateKey The encrypted private key * @param password The password used to encrypt the private key * @throws {BadEncodedPrivateKeyError} If the PLAIN private key is base64 encoded (known issue introduced in the past) @@ -53,7 +53,7 @@ export class KeysService { }; /** - * Encrypts private key using password + * Encrypts a private key using a password * @param privateKey The plain private key * @param password The password to encrypt * @returns The encrypted private key @@ -63,7 +63,7 @@ export class KeysService { }; /** - * Decrypts private key using password + * Decrypts a private key using a password * @param privateKey The encrypted private key * @param password The password used to encrypt the private key * @returns The decrypted private key @@ -73,7 +73,7 @@ export class KeysService { }; /** - * Validates if a message encrypted with the public key can be decrypted with the private key + * Checks if a message encrypted with the public key can be decrypted with a private key, otherwise it throws an error * @param privateKey The plain private key * @param publicKey The plain public key * @throws {KeysDoNotMatchError} If the keys can not be used together to encrypt/decrypt a message From 82199c2f7750a104cc7315886526b64a2969b645 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 15:49:56 +0100 Subject: [PATCH 25/29] added getKeyAndIvFrom util for better aes management --- src/services/crypto.service.ts | 82 ++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/src/services/crypto.service.ts b/src/services/crypto.service.ts index b750161..503bc06 100644 --- a/src/services/crypto.service.ts +++ b/src/services/crypto.service.ts @@ -25,7 +25,11 @@ export class CryptoService { }, }; - // Method to hash password. If salt is passed, use it, in other case use crypto lib for generate salt + /** + * Generates the hash for a password, if salt is provided it uses it, in other case it is generated from crypto + * @param passObject The object containing the password and an optional salt + * @returns The hashed password and the salt + **/ public passToHash = (passObject: { salt?: string | null; password: string }): { salt: string; hash: string } => { const salt = passObject.salt ? passObject.salt : crypto.randomBytes(128 / 8).toString('hex'); const hash = crypto.pbkdf2Sync(passObject.password, salt, 10000, 256 / 8, 'sha256').toString('hex'); @@ -36,61 +40,84 @@ export class CryptoService { return hashedObjetc; }; - // AES Plain text encryption method + /** + * Encrypts a plain message into an AES encrypted text using APP_CRYPTO_SECRET value from env + * @param textToEncrypt The plain text to be encrypted + * @returns The encrypted string in 'hex' encoding + **/ 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 + /** + * Decrypts an AES encrypted text using APP_CRYPTO_SECRET value from env + * @param encryptedText The AES encrypted text in 'HEX' encoding + * @returns The decrypted string in 'utf8' encoding + **/ public decryptText = (encryptedText: string): string => { const APP_CRYPTO_SECRET = ConfigService.instance.get('APP_CRYPTO_SECRET'); return this.decryptTextWithKey(encryptedText, APP_CRYPTO_SECRET); }; /** - * Encrypts a plain message into an AES encrypted text - * [Crypto.JS compatible] (deprecated dependency) + * Encrypts a plain message into an AES encrypted text using a secret. + * [Crypto.JS compatible]: + * First 8 bytes are reserved for 'Salted__', next 8 bytes are the salt, and the rest is aes content * @param textToEncrypt The plain text to be encrypted * @param secret The secret used to encrypt - * @returns The encrypted private key in 'hex' encoding + * @returns The encrypted private string in 'hex' encoding **/ public encryptTextWithKey = (textToEncrypt: string, secret: string) => { - const TRANSFORM_ROUNDS = 3; - const openSSLstart = Buffer.from('Salted__'); const salt = crypto.randomBytes(8); - const password = Buffer.concat([Buffer.from(secret, 'binary'), salt]); - const md5Hashes = []; - - let digest = password; - - for (let i = 0; i < TRANSFORM_ROUNDS; i++) { - md5Hashes[i] = crypto.createHash('md5').update(digest).digest(); - digest = Buffer.concat([md5Hashes[i], password]); - } + const { key, iv } = this.getKeyAndIvFrom(secret, salt); - const key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); - const iv = md5Hashes[2]; const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); const encrypted = Buffer.concat([cipher.update(textToEncrypt, 'utf8'), cipher.final()]); + + /* CryptoJS applies the OpenSSL format for the ciphertext, i.e. the encrypted data starts with the ASCII + encoding of 'Salted__' followed by the salt and then the ciphertext. + Therefore the beginning of the Base64 encoded ciphertext starts always with U2FsdGVkX1 + */ + const openSSLstart = Buffer.from('Salted__'); + return Buffer.concat([openSSLstart, salt, encrypted]).toString('hex'); }; /** - * Decrypts an AES encrypted text - * [Crypto.JS compatible] (deprecated dependency) + * Decrypts an AES encrypted text using a secret. + * [Crypto.JS compatible]: + * First 8 bytes are reserved for 'Salted__', next 8 bytes are the salt, and the rest is aes content * @param encryptedText The AES encrypted text in 'HEX' encoding * @param secret The secret used to encrypt - * @returns The decrypted private key in 'utf8' encoding + * @returns The decrypted string in 'utf8' encoding **/ public decryptTextWithKey = (encryptedText: string, secret: string) => { + const cypherText = Buffer.from(encryptedText, 'hex'); + + const salt = cypherText.subarray(8, 16); + const { key, iv } = this.getKeyAndIvFrom(secret, salt); + + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + + const contentsToDecrypt = cypherText.subarray(16); + + return Buffer.concat([decipher.update(contentsToDecrypt), decipher.final()]).toString('utf8'); + }; + + /** + * Generates the key and the iv by transforming a secret and a salt. + * It will generate the same key and iv if the same secret and salt is used. + * This function is needed to be Crypto.JS compatible and encrypt/decrypt without errors + * @param secret The secret used to encrypt + * @param salt The salt used to encrypt + * @returns The key and the iv resulted from the secret and the salt combination + **/ + private getKeyAndIvFrom = (secret: string, salt: Buffer) => { const TRANSFORM_ROUNDS = 3; - const cypher = Buffer.from(encryptedText, 'hex'); - const salt = cypher.subarray(8, 16); const password = Buffer.concat([Buffer.from(secret, 'binary'), salt]); const md5Hashes = []; - let digest = password; for (let i = 0; i < TRANSFORM_ROUNDS; i++) { @@ -100,9 +127,6 @@ export class CryptoService { const key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); const iv = md5Hashes[2]; - const contents = cypher.subarray(16); - const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); - - return Buffer.concat([decipher.update(contents), decipher.final()]).toString('utf8'); + return { key, iv }; }; } From 1da5c1e5b8f56002cdf0b80009ae1dceb1939e6b Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 16:15:13 +0100 Subject: [PATCH 26/29] added readonly properties --- src/services/SDKManager.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/SDKManager.service.ts b/src/services/SDKManager.service.ts index 54ee8dd..e14810e 100644 --- a/src/services/SDKManager.service.ts +++ b/src/services/SDKManager.service.ts @@ -17,7 +17,7 @@ export class SdkManager { * Sets the security details needed to create SDK clients * @param apiSecurity Security properties to be setted **/ - public static init = (apiSecurity: SdkManagerApiSecurity) => { + public static readonly init = (apiSecurity: SdkManagerApiSecurity) => { SdkManager.apiSecurity = apiSecurity; }; @@ -34,7 +34,9 @@ export class SdkManager { * @throws {Error} When throwErrorOnMissingCredentials is setted to true and there is not apiSecurity defined * @returns The SDK Manager api security details **/ - public static getApiSecurity = (config = { throwErrorOnMissingCredentials: true }): SdkManagerApiSecurity => { + public static readonly getApiSecurity = ( + config = { throwErrorOnMissingCredentials: true }, + ): SdkManagerApiSecurity => { if (!SdkManager.apiSecurity && config.throwErrorOnMissingCredentials) throw new Error('Api security properties not found in SdkManager'); @@ -45,7 +47,7 @@ export class SdkManager { * Returns the application details from package.json * @returns The name and the version of the app from package.json **/ - public static getAppDetails = (): AppDetails => { + public static readonly getAppDetails = (): AppDetails => { return { clientName: packageJson.name, clientVersion: packageJson.version, From 5a2c5516ae9c0c1c6885cd36a6c9ee6b62c42460 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 17:11:39 +0100 Subject: [PATCH 27/29] some maintainability fixes --- src/services/SDKManager.service.ts | 2 +- src/services/auth.service.ts | 56 +++++++++++++----------------- src/services/keys.service.ts | 2 +- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/services/SDKManager.service.ts b/src/services/SDKManager.service.ts index e14810e..a963780 100644 --- a/src/services/SDKManager.service.ts +++ b/src/services/SDKManager.service.ts @@ -24,7 +24,7 @@ export class SdkManager { /** * Cleans the security details **/ - static clean = () => { + public static readonly clean = () => { SdkManager.apiSecurity = undefined; }; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 45186ad..15df046 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -32,40 +32,34 @@ export class AuthService { tfaCode: twoFactorCode, }; - // eslint-disable-next-line no-useless-catch - try { - const data = await authClient.login(loginDetails, CryptoService.cryptoProvider); - const { user, token, newToken } = data; - const { privateKey, publicKey } = user; + const data = await authClient.login(loginDetails, CryptoService.cryptoProvider); + const { user, token, newToken } = data; + const { privateKey, publicKey } = user; - const plainPrivateKeyInBase64 = privateKey - ? Buffer.from(KeysService.instance.decryptPrivateKey(privateKey, password)).toString('base64') - : ''; + const plainPrivateKeyInBase64 = privateKey + ? Buffer.from(KeysService.instance.decryptPrivateKey(privateKey, password)).toString('base64') + : ''; - if (privateKey) { - await KeysService.instance.assertPrivateKeyIsValid(privateKey, password); - await KeysService.instance.assertValidateKeys( - Buffer.from(plainPrivateKeyInBase64, 'base64').toString(), - Buffer.from(publicKey, 'base64').toString(), - ); - } - - const clearMnemonic = CryptoService.instance.decryptTextWithKey(user.mnemonic, password); - const clearUser = { - ...user, - mnemonic: clearMnemonic, - privateKey: plainPrivateKeyInBase64, - }; - return { - user: clearUser, - token: token, - newToken: newToken, - mnemonic: clearMnemonic, - }; - } catch (error) { - //TODO send Sentry login errors and remove eslint-disable from this trycatch - throw error; + if (privateKey) { + await KeysService.instance.assertPrivateKeyIsValid(privateKey, password); + await KeysService.instance.assertValidateKeys( + Buffer.from(plainPrivateKeyInBase64, 'base64').toString(), + Buffer.from(publicKey, 'base64').toString(), + ); } + + const clearMnemonic = CryptoService.instance.decryptTextWithKey(user.mnemonic, password); + const clearUser = { + ...user, + mnemonic: clearMnemonic, + privateKey: plainPrivateKeyInBase64, + }; + return { + user: clearUser, + token: token, + newToken: newToken, + mnemonic: clearMnemonic, + }; }; /** diff --git a/src/services/keys.service.ts b/src/services/keys.service.ts index 6cfe1aa..c7dd483 100644 --- a/src/services/keys.service.ts +++ b/src/services/keys.service.ts @@ -146,6 +146,6 @@ export class KeysService { const MAGIC_IV = ConfigService.instance.get('APP_MAGIC_IV'); const MAGIC_SALT = ConfigService.instance.get('APP_MAGIC_SALT'); - return { iv: MAGIC_IV as string, salt: MAGIC_SALT as string }; + return { iv: MAGIC_IV, salt: MAGIC_SALT }; }; } From 7014b10d990c3ae33e2c3e92ffef6206a418b1e3 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 17:12:30 +0100 Subject: [PATCH 28/29] added a disabled cryptoJS test --- test/services/crypto.service.test.ts | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/services/crypto.service.test.ts b/test/services/crypto.service.test.ts index 2755597..b7ce7ca 100644 --- a/test/services/crypto.service.test.ts +++ b/test/services/crypto.service.test.ts @@ -117,4 +117,42 @@ describe('Crypto service', () => { expect(expectedKeys).to.be.eql(resultedKeys); expect(kerysServiceStub).to.be.calledWith(password); }); + + it('The node:crypto works the same as CryptoJS library', () => { + return; + const password = { + value: crypto.randomBytes(16).toString('hex'), + salt: crypto.randomBytes(16).toString('hex'), + }; + const APP_CRYPTO_SECRET = crypto.randomBytes(16).toString('hex'); + + // test PBKDF2 - node:crypto equivalent password to hash + const actualPass = CryptoJS.PBKDF2(password.value, password.salt, { + keySize: 256 / 32, + iterations: 10000, + }).toString(); + const expectedPass = crypto.pbkdf2Sync(password.value, password.salt, 10000, 256 / 8, 'sha256').toString('hex'); + expect(actualPass).to.equal(expectedPass); + expect(CryptoJS.lib.WordArray.random(128 / 8).toString().length).to.equal( + crypto.randomBytes(128 / 8).toString('hex').length, + ); + + // test CryptoJS.AES - node:crypto equivalent encrypt/decrypt + const cryptoJSEncrypted = CryptoJS.enc.Base64.parse( + CryptoJS.AES.encrypt(password.value, APP_CRYPTO_SECRET).toString(), + ).toString(CryptoJS.enc.Hex); + const textdecrypted = CryptoService.instance.decryptTextWithKey(cryptoJSEncrypted, APP_CRYPTO_SECRET); + expect(password.value).to.equal(textdecrypted); + + const textencrypted = CryptoService.instance.encryptTextWithKey(password.value, APP_CRYPTO_SECRET); + const cryptoJSdecrypted = CryptoJS.AES.decrypt( + CryptoJS.enc.Hex.parse(textencrypted).toString(CryptoJS.enc.Base64), + APP_CRYPTO_SECRET, + ).toString(CryptoJS.enc.Utf8); + expect(password.value).to.equal(cryptoJSdecrypted); + + const expectedText1 = CryptoService.instance.encryptTextWithKey(password.value, APP_CRYPTO_SECRET); + const expectedText2 = CryptoService.instance.decryptTextWithKey(expectedText1, APP_CRYPTO_SECRET); + expect(password.value).to.equal(expectedText2); + }); }); From 7c1aaa68f0a380dbb7f4226ec783a7bb7cebdc80 Mon Sep 17 00:00:00 2001 From: larry-internxt Date: Thu, 15 Feb 2024 17:18:19 +0100 Subject: [PATCH 29/29] added comment to disabled crypto test --- test/services/crypto.service.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/services/crypto.service.test.ts b/test/services/crypto.service.test.ts index b7ce7ca..7027ba8 100644 --- a/test/services/crypto.service.test.ts +++ b/test/services/crypto.service.test.ts @@ -119,7 +119,7 @@ describe('Crypto service', () => { }); it('The node:crypto works the same as CryptoJS library', () => { - return; + /* const password = { value: crypto.randomBytes(16).toString('hex'), salt: crypto.randomBytes(16).toString('hex'), @@ -154,5 +154,6 @@ describe('Crypto service', () => { const expectedText1 = CryptoService.instance.encryptTextWithKey(password.value, APP_CRYPTO_SECRET); const expectedText2 = CryptoService.instance.decryptTextWithKey(expectedText1, APP_CRYPTO_SECRET); expect(password.value).to.equal(expectedText2); + */ }); });