Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@inquirer/prompts": "7.8.6",
"@internxt/inxt-js": "2.2.9",
"@internxt/lib": "1.3.1",
"@internxt/sdk": "1.11.11",
"@internxt/sdk": "1.11.12",
"@oclif/core": "4.5.4",
"@oclif/plugin-autocomplete": "3.2.35",
"axios": "1.12.2",
Expand Down
9 changes: 4 additions & 5 deletions src/commands/download-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,20 @@ import { CryptoService } from '../services/crypto.service';
import { DownloadService } from '../services/network/download.service';
import { SdkManager } from '../services/sdk-manager.service';
import { createWriteStream } from 'node:fs';
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
import { DriveFileItem } from '../types/drive.types';
import fs from 'node:fs/promises';
import path from 'node:path';
import { StreamUtils } from '../utils/stream.utils';
import { NotValidDirectoryError, NotValidFileUuidError } from '../types/command.types';
import { LoginUserDetails, NotValidDirectoryError, NotValidFileUuidError } from '../types/command.types';
import { ValidationService } from '../services/validation.service';
import { Environment } from '@internxt/inxt-js';
import { ConfigService } from '../services/config.service';

export default class DownloadFile extends Command {
static readonly args = {};
static readonly description =
// eslint-disable-next-line max-len
'Download and decrypts a file from Internxt Drive to a directory. The file name will be the same as the file name in your Drive.';
'Download and decrypts a file from Internxt Drive to a directory.' +
' The file name will be the same as the file name in your Drive.';
static readonly aliases = ['download:file'];
static readonly examples = ['<%= config.bin %> <%= command.id %>'];
static readonly flags = {
Expand Down Expand Up @@ -206,7 +205,7 @@ export default class DownloadFile extends Command {
return downloadPath;
};

private prepareNetwork = async (user: UserSettings, jsonFlag?: boolean) => {
private prepareNetwork = async (user: LoginUserDetails, jsonFlag?: boolean) => {
CLIUtils.doing('Preparing Network', jsonFlag);

const networkModule = SdkManager.instance.getNetwork({
Expand Down
21 changes: 3 additions & 18 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { LoginDetails } from '@internxt/sdk';
import { SdkManager } from './sdk-manager.service';
import { KeysService } from './keys.service';
import { CryptoService } from './crypto.service';
import { ConfigService } from './config.service';
import {
Expand Down Expand Up @@ -30,27 +29,13 @@ export class AuthService {
tfaCode: twoFactorCode,
};

const data = await authClient.login(loginDetails, CryptoService.cryptoProvider);
const data = await authClient.loginAccess(loginDetails, CryptoService.cryptoProvider);
const { user, newToken } = data;
const { privateKey, publicKey } = user;

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 = {
const clearUser: LoginCredentials['user'] = {
...user,
mnemonic: clearMnemonic,
privateKey: plainPrivateKeyInBase64,
};
return {
user: clearUser,
Expand Down Expand Up @@ -118,7 +103,7 @@ export class AuthService {
user: {
...newCreds.user,
mnemonic: oldCreds.user.mnemonic,
privateKey: oldCreds.user.privateKey,
createdAt: new Date(newCreds.user.createdAt).toISOString(),
},
token: newCreds.newToken,
lastLoggedInAt: oldCreds.lastLoggedInAt,
Expand Down
7 changes: 1 addition & 6 deletions src/services/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,7 @@ export class ConfigService {
try {
const encryptedCredentials = await fs.readFile(ConfigService.CREDENTIALS_FILE, 'utf8');
const credentialsString = CryptoService.instance.decryptText(encryptedCredentials);
const loginCredentials = JSON.parse(credentialsString, (key, value) => {
if (typeof value === 'string' && key === 'createdAt') {
return new Date(value);
}
return value;
}) as LoginCredentials;
const loginCredentials = JSON.parse(credentialsString) as LoginCredentials;
return loginCredentials;
} catch {
return;
Expand Down
92 changes: 0 additions & 92 deletions src/services/keys.service.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,10 @@
import { aes } from '@internxt/lib';
import * as openpgp from 'openpgp';
import {
BadEncodedPrivateKeyError,
CorruptedEncryptedPrivateKeyError,
KeysDoNotMatchError,
WrongIterationsToEncryptPrivateKeyError,
} from '../types/keys.types';
import { CryptoUtils } from '../utils/crypto.utils';

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

/**
* 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)
* @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<void> => {
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 {
badEncrypted = true;
}

let hasValidFormat = false;
try {
if (privateKeyDecrypted !== undefined) {
hasValidFormat = await this.isValidKey(privateKeyDecrypted);
}
} catch {
/* no op */
}

if (badEncrypted === true) throw new CorruptedEncryptedPrivateKeyError();
if (hasValidFormat === false) throw new BadEncodedPrivateKeyError();
};

/**
* Encrypts a private key using a password
* @param privateKey The plain private key
Expand All @@ -71,52 +25,6 @@ export class KeysService {
return aes.decrypt(privateKey, password);
};

/**
* 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
* @async
**/
public assertValidateKeys = async (privateKey: string, publicKey: string): Promise<void> => {
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();
}
};

/**
* 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<boolean> => {
try {
await openpgp.readKey({ armoredKey: key });
return true;
} catch {
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
Expand Down
28 changes: 26 additions & 2 deletions src/types/command.types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,31 @@
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
export interface LoginUserDetails {
userId: string;
uuid: string;
email: string;
name: string;
lastname: string;
username: string;
bridgeUser: string;
bucket: string;
rootFolderId: string;
mnemonic: string;
keys: {
ecc: {
publicKey: string;
privateKey: string;
};
kyber: {
publicKey: string;
privateKey: string;
};
};
createdAt: string;
avatar: string | null;
emailVerified: boolean;
}

export interface LoginCredentials {
user: UserSettings;
user: LoginUserDetails;
token: string;
lastLoggedInAt: string;
lastTokenRefreshAt: string;
Expand Down
32 changes: 0 additions & 32 deletions src/types/keys.types.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,3 @@
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, KeysDoNotMatchError.prototype);
}
}

export interface AesInit {
iv: string;
salt: string;
Expand Down
32 changes: 4 additions & 28 deletions test/fixtures/auth.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
import crypto from 'node:crypto';
import { LoginUserDetails } from '../../src/types/command.types';

export const UserFixture: UserSettings = {
export const UserFixture: LoginUserDetails = {
userId: crypto.randomBytes(16).toString('hex'),
uuid: crypto.randomBytes(16).toString('hex'),
email: crypto.randomBytes(16).toString('hex'),
Expand All @@ -10,21 +10,9 @@ export const UserFixture: UserSettings = {
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),
rootFolderId: crypto.randomBytes(16).toString('hex'),
rootFolderUuid: crypto.randomBytes(16).toString('hex'),
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(),
createdAt: new Date().toISOString(),
avatar: crypto.randomBytes(16).toString('hex'),
emailVerified: true,
keys: {
Expand All @@ -39,28 +27,16 @@ export const UserFixture: UserSettings = {
},
};

export const UserSettingsFixture: UserSettings = {
export const UserSettingsFixture: LoginUserDetails = {
userId: UserFixture.userId,
email: UserFixture.email,
name: UserFixture.name,
lastname: UserFixture.lastname,
username: UserFixture.username,
bridgeUser: UserFixture.bridgeUser,
bucket: UserFixture.bucket,
backupsBucket: UserFixture.backupsBucket,
root_folder_id: UserFixture.root_folder_id,
rootFolderId: UserFixture.rootFolderId,
rootFolderUuid: UserFixture.rootFolderUuid,
sharedWorkspace: UserFixture.sharedWorkspace,
credit: UserFixture.credit,
mnemonic: UserFixture.mnemonic,
privateKey: UserFixture.privateKey,
publicKey: UserFixture.publicKey,
revocationKey: UserFixture.revocationKey,
teams: UserFixture.teams,
appSumoDetails: UserFixture.appSumoDetails,
registerCompleted: UserFixture.registerCompleted,
hasReferralsProgram: UserFixture.hasReferralsProgram,
createdAt: UserFixture.createdAt,
avatar: UserFixture.avatar,
emailVerified: UserFixture.emailVerified,
Expand Down
13 changes: 5 additions & 8 deletions test/services/auth.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import crypto from 'node:crypto';
import { Auth, LoginDetails, SecurityDetails } from '@internxt/sdk';
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/sdk-manager.service';
import { ConfigService } from '../../src/services/config.service';
Expand All @@ -16,6 +15,7 @@ import {
} from '../../src/types/command.types';
import { UserCredentialsFixture } from '../fixtures/login.fixture';
import { fail } from 'node:assert';
import { paths } from '@internxt/sdk/dist/schema';

describe('Auth service', () => {
beforeEach(() => {
Expand All @@ -28,14 +28,11 @@ describe('Auth service', () => {
newToken: crypto.randomBytes(16).toString('hex'),
user: UserFixture,
userTeam: null,
};
} as unknown as paths['/auth/cli/login/access']['post']['responses']['200']['content']['application/json'];
const mockDate = new Date().toISOString();

vi.spyOn(Auth.prototype, 'login').mockResolvedValue(loginResponse);
vi.spyOn(Auth.prototype, 'loginAccess').mockResolvedValue(loginResponse);
vi.spyOn(SdkManager.instance, 'getAuth').mockReturnValue(Auth.prototype);
vi.spyOn(KeysService.instance, 'decryptPrivateKey').mockReturnValue(loginResponse.user.privateKey);
vi.spyOn(KeysService.instance, 'assertPrivateKeyIsValid').mockResolvedValue();
vi.spyOn(KeysService.instance, 'assertValidateKeys').mockResolvedValue();
vi.spyOn(CryptoService.instance, 'decryptTextWithKey').mockReturnValue(loginResponse.user.mnemonic);
vi.spyOn(Date.prototype, 'toISOString').mockReturnValue(mockDate);

Expand All @@ -46,7 +43,7 @@ describe('Auth service', () => {
);

const expectedResponseLogin: LoginCredentials = {
user: { ...loginResponse.user, privateKey: Buffer.from(loginResponse.user.privateKey).toString('base64') },
user: { ...loginResponse.user },
token: loginResponse.newToken,
lastLoggedInAt: mockDate,
lastTokenRefreshAt: mockDate,
Expand All @@ -61,7 +58,7 @@ describe('Auth service', () => {
tfaCode: crypto.randomInt(1, 999999).toString().padStart(6, '0'),
};

const loginStub = vi.spyOn(Auth.prototype, 'login').mockRejectedValue(new Error('Login failed'));
const loginStub = vi.spyOn(Auth.prototype, 'loginAccess').mockRejectedValue(new Error('Login failed'));
vi.spyOn(SdkManager.instance, 'getAuth').mockReturnValue(Auth.prototype);

try {
Expand Down
Loading