Skip to content

Commit

Permalink
Merge pull request #338 from buttercup/feat/security_update
Browse files Browse the repository at this point in the history
Security update, CVE-2023-41646
  • Loading branch information
perry-mitchell authored Dec 9, 2023
2 parents 1fdd4c6 + d685b8e commit 96292d9
Show file tree
Hide file tree
Showing 23 changed files with 825 additions and 778 deletions.
1,398 changes: 708 additions & 690 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
},
"homepage": "https://github.com/buttercup/buttercup-core#readme",
"dependencies": {
"@buttercup/channel-queue": "^1.3.0",
"@buttercup/channel-queue": "^1.4.0",
"@buttercup/dropbox-client": "^2.2.0",
"@buttercup/googledrive-client": "^2.3.0",
"crypto-random-string": "^5.0.0",
Expand All @@ -93,23 +93,23 @@
"fuse.js": "^6.6.2",
"global": "^4.4.0",
"hash.js": "^1.1.7",
"iocane": "^5.1.1",
"iocane": "^5.2.0",
"is-promise": "^4.0.0",
"layerr": "^2.0.1",
"pako": "^1.0.11",
"path-posix": "^1.0.0",
"pify": "^6.1.0",
"url-join": "^5.0.0",
"uuid": "^9.0.1",
"webdav": "^5.3.0"
"webdav": "^5.3.1"
},
"devDependencies": {
"@babel/cli": "^7.23.0",
"@babel/core": "^7.23.3",
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.5",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/preset-env": "^7.23.3",
"@types/node": "^20.9.0",
"@babel/preset-env": "^7.23.5",
"@types/node": "^20.10.4",
"arraybuffer-loader": "^1.0.8",
"babel-loader": "^9.1.3",
"base64-js": "^1.5.1",
Expand All @@ -124,24 +124,24 @@
"karma-sinon": "^1.0.5",
"karma-spec-reporter": "^0.0.36",
"karma-webpack": "^5.0.0",
"lint-staged": "^15.0.2",
"lint-staged": "^15.2.0",
"mocha": "^10.2.0",
"nested-property": "^4.0.0",
"nodemon": "^3.0.1",
"nodemon": "^3.0.2",
"npm-run-all": "^4.1.1",
"null-loader": "^4.0.1",
"nyc": "^15.1.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"resolve-typescript-plugin": "^2.0.1",
"rimraf": "^5.0.5",
"sinon": "^17.0.1",
"sleep-promise": "^9.1.0",
"tmp": "^0.2.1",
"ts-loader": "^9.5.0",
"typescript": "^5.2.2",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"webdav-server": "^2.6.2",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.9.1",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-cli": "^5.1.4"
}
}
2 changes: 2 additions & 0 deletions source/core/VaultManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export class VaultManager extends EventEmitter {
/**
* Fetch all currently available Live Snapshots of vaults
* @returns An array of snapshot objects
* @deprecated Will be removed in next major - insecure
*/
getLiveSnapshots(): Array<VaultLiveSnapshot> {
return this.unlockedSources.map((source) => source.getLiveSnapshot());
Expand Down Expand Up @@ -353,6 +354,7 @@ export class VaultManager extends EventEmitter {
/**
* Restore all sources from snapshots that were taken previously
* @param snapshots An array of snapshot objects
* @deprecated Will be removed in next major - insecure
*/
async restoreLiveSnapshots(snapshots: Array<VaultLiveSnapshot>) {
await Promise.all(
Expand Down
27 changes: 16 additions & 11 deletions source/core/VaultSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { ChannelQueue } from "@buttercup/channel-queue";
import { Layerr } from "layerr";
import { Vault } from "./Vault.js";
import { Credentials } from "../credentials/Credentials.js";
import { getCredentials, setCredentials } from "../credentials/channel.js";
import { getCredentials, setCredentials } from "../credentials/memory/credentials.js";
import { getMasterPassword, setMasterPassword } from "../credentials/memory/password.js";
import { getUniqueID } from "../tools/encoding.js";
import {
getSourceOfflineArchive,
Expand All @@ -17,8 +18,8 @@ import { TextDatasource } from "../datasources/TextDatasource.js";
import { VaultManager } from "./VaultManager.js";
import { convertFormatAVault } from "../io/formatB/conversion.js";
import { VaultFormatB } from "../index.common.js";
import { VaultFormatID, VaultLiveSnapshot, VaultSourceID, VaultSourceStatus } from "../types.js";
import { getFormatForID } from "../io/formatRouter.js";
import { VaultFormatID, VaultLiveSnapshot, VaultSourceID, VaultSourceStatus } from "../types.js";

interface StateChangeEnqueuedFunction {
(): void | Promise<any>;
Expand Down Expand Up @@ -272,8 +273,8 @@ export class VaultSource extends EventEmitter {
await this.unlock(Credentials.fromPassword(oldPassword));
} else {
// Unlocked, so check password..
const credentials = getCredentials((<Credentials>this._credentials).id);
if (credentials.masterPassword !== oldPassword) {
const masterPassword = getMasterPassword((<Credentials>this._credentials).id);
if (masterPassword !== oldPassword) {
throw new Error("Old password does not match current unlocked instance value");
}
// ..and then update
Expand All @@ -296,8 +297,7 @@ export class VaultSource extends EventEmitter {
this._credentials as Credentials,
oldPassword
);
const newCreds = getCredentials(newCredentials.id);
newCreds.masterPassword = newPassword;
setMasterPassword(newCredentials.id, newPassword);
await this._updateVaultCredentials(newCredentials);
// Re-lock if it was locked earlier
if (wasLocked) {
Expand Down Expand Up @@ -385,19 +385,23 @@ export class VaultSource extends EventEmitter {
/**
* Get a live snapshot of the current unlocked state
* @returns A snapshot object
* @deprecated Will be removed in next major - insecure
*/
getLiveSnapshot(): VaultLiveSnapshot {
if (this.status !== VaultSourceStatus.Unlocked) {
throw new Layerr("Not possible to fetch live snapshot: Vault is not unlocked");
}
const credentials = getCredentials((this._credentials as Credentials).id);
const credentialsID = (this._credentials as Credentials).id;
const credentials = getCredentials(credentialsID);
if (!credentials) {
throw new Layerr("Failed fetching live snapshot: Invalid credentials data");
}
const masterPassword = getMasterPassword(credentialsID);
return {
credentials,
formatID: this.vault._format.getFormat().getFormatID(),
formatSource: this.vault._format.source,
masterPassword,
sourceID: this.id,
version: "1a"
};
Expand Down Expand Up @@ -533,6 +537,7 @@ export class VaultSource extends EventEmitter {
/**
* Restore unlocked state from a live snapshot
* @param snapshot The snapshot taken previously
* @deprecated Will be removed in next major - insecure
*/
async restoreFromLiveSnapshot(snapshot: VaultLiveSnapshot): Promise<void> {
if (this.status !== VaultSourceStatus.Locked) {
Expand All @@ -546,12 +551,12 @@ export class VaultSource extends EventEmitter {
// Setup credentials and datasource
const credentials = (this._credentials = new Credentials(
snapshot.credentials.data,
snapshot.credentials.masterPassword
snapshot.masterPassword
));
setCredentials(credentials.id, snapshot.credentials);
// Initialise datasource
const datasource = (this._datasource = credentialsToDatasource(
Credentials.fromCredentials(credentials, snapshot.credentials.masterPassword)
Credentials.fromCredentials(credentials, snapshot.masterPassword)
));
datasource.sourceID = this.id;
// Setup vault
Expand Down Expand Up @@ -650,7 +655,7 @@ export class VaultSource extends EventEmitter {
`Failed unlocking source: Source in invalid state (${this.status}): ${this.id}`
);
}
const { masterPassword } = getCredentials(vaultCredentials.id);
const masterPassword = getMasterPassword(vaultCredentials.id);
const originalCredentials = this._credentials;
this._status = VaultSource.STATUS_PENDING;
await this._enqueueStateChange(async () => {
Expand Down Expand Up @@ -813,7 +818,7 @@ export class VaultSource extends EventEmitter {
`Failed updating source credentials: Source is not unlocked: ${this.id}`
);
}
const { masterPassword } = getCredentials((<Credentials>this._credentials).id);
const masterPassword = getMasterPassword((<Credentials>this._credentials).id);
this._credentials = Credentials.fromCredentials(
this._datasource.credentials,
masterPassword
Expand Down
53 changes: 27 additions & 26 deletions source/credentials/Credentials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { generateUUID } from "../tools/uuid.js";
import { credentialsAllowsPurpose, getCredentials, setCredentials } from "./channel.js";
import { credentialsAllowsPurpose, getCredentials, setCredentials } from "./memory/credentials.js";
import { getSharedAppEnv } from "../env/appEnv.js";
import { getMasterPassword, setMasterPassword } from "./memory/password.js";
import { CredentialsData, CredentialsPayload, DatasourceConfiguration } from "../types.js";

/**
Expand Down Expand Up @@ -85,7 +86,8 @@ export class Credentials {
throw new Error("Master password is required for credentials cloning");
}
const credentialsData = getCredentials(credentials.id);
if (credentialsData.masterPassword !== masterPassword) {
const credentialsPassword = getMasterPassword(credentials.id);
if (credentialsPassword !== masterPassword) {
throw new Error("Master password does not match that of the credentials to be cloned");
}
const newData = JSON.parse(JSON.stringify(credentialsData.data));
Expand Down Expand Up @@ -134,29 +136,27 @@ export class Credentials {
* @param masterPassword The password for decryption
* @returns A promise that resolves with the new instance
*/
static fromSecureString(content: string, masterPassword: string): Promise<Credentials> {
static async fromSecureString(content: string, masterPassword: string): Promise<Credentials> {
const decrypt = getSharedAppEnv().getProperty("crypto/v1/decryptText");
return decrypt(unsignEncryptedContent(content), masterPassword)
.then((decryptedContent: string) => JSON.parse(decryptedContent))
.then((credentialsData: any) => {
// Handle compatibility updates for legacy credentials
if (credentialsData.datasource) {
if (typeof credentialsData.datasource === "string") {
credentialsData.datasource = JSON.parse(credentialsData.datasource);
}
// Move username and password INTO the datasource config, as
// they relate to the remote connection/source
if (credentialsData.username) {
credentialsData.datasource.username = credentialsData.username;
delete credentialsData.username;
}
if (credentialsData.password) {
credentialsData.datasource.password = credentialsData.password;
delete credentialsData.password;
}
}
return new Credentials(credentialsData, masterPassword);
});
const decryptedContent = await decrypt(unsignEncryptedContent(content), masterPassword);
const credentialsData = JSON.parse(decryptedContent);
// Handle compatibility updates for legacy credentials
if (credentialsData.datasource) {
if (typeof credentialsData.datasource === "string") {
credentialsData.datasource = JSON.parse(credentialsData.datasource);
}
// Move username and password INTO the datasource config, as
// they relate to the remote connection/source
if (credentialsData.username) {
credentialsData.datasource.username = credentialsData.username;
delete credentialsData.username;
}
if (credentialsData.password) {
credentialsData.datasource.password = credentialsData.password;
delete credentialsData.password;
}
}
return new Credentials(credentialsData, masterPassword);
}

/**
Expand Down Expand Up @@ -190,10 +190,10 @@ export class Credentials {
});
setCredentials(id, {
data: obj,
masterPassword,
purposes: Credentials.allPurposes(),
open: false
});
setMasterPassword(id, masterPassword);
}

/**
Expand Down Expand Up @@ -279,7 +279,8 @@ export class Credentials {
throw new Error("Credential purposes don't allow for secure exports");
}
const encrypt = getSharedAppEnv().getProperty("crypto/v1/encryptText");
const { data, masterPassword } = getCredentials(this.id);
const { data } = getCredentials(this.id);
const masterPassword = getMasterPassword(this.id);
if (typeof masterPassword !== "string") {
throw new Error(
"Cannot convert Credentials to string: master password was not set or is invalid"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CredentialsPayload } from "../types.js";
import { CredentialsPayload } from "../../types.js";

const __store = {};
const __store: Record<string, CredentialsPayload | null> = {};

export function credentialsAllowsPurpose(id: string, purpose: string): boolean {
const { purposes } = getCredentials(id);
Expand Down
14 changes: 14 additions & 0 deletions source/credentials/memory/password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const __store: Record<string, string | null> = {};

export function getMasterPassword(id: string): string | null {
return __store[id] || null;
}

export function removeMasterPassword(id: string) {
__store[id] = null;
delete __store[id];
}

export function setMasterPassword(id: string, value: string) {
__store[id] = value;
}
2 changes: 1 addition & 1 deletion source/datasources/DropboxDatasource.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TextDatasource } from "./TextDatasource.js";
import { fireInstantiationHandlers, registerDatasource } from "./register.js";
import { Credentials } from "../credentials/Credentials.js";
import { getCredentials } from "../credentials/channel.js";
import { getCredentials } from "../credentials/memory/credentials.js";
import { getSharedAppEnv } from "../env/appEnv.js";
import {
DatasourceConfigurationDropbox,
Expand Down
2 changes: 1 addition & 1 deletion source/datasources/FileDatasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import pify from "pify";
import { TextDatasource } from "./TextDatasource.js";
import { fireInstantiationHandlers, registerDatasource } from "./register.js";
import { Credentials } from "../credentials/Credentials.js";
import { getCredentials } from "../credentials/channel.js";
import { getCredentials } from "../credentials/memory/credentials.js";
import { ATTACHMENT_EXT } from "../tools/attachments.js";
import {
AttachmentDetails,
Expand Down
22 changes: 11 additions & 11 deletions source/datasources/GoogleDriveDatasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import DatasourceAuthManager from "./DatasourceAuthManager.js";
import { TextDatasource } from "./TextDatasource.js";
import { fireInstantiationHandlers, registerDatasource } from "./register.js";
import { Credentials } from "../credentials/Credentials.js";
import { getCredentials } from "../credentials/channel.js";
import { DatasourceConfigurationGoogleDrive } from "../types.js";
import { getCredentials } from "../credentials/memory/credentials.js";
import { DatasourceConfigurationGoogleDrive, DatasourceLoadedData, History } from "../types.js";

const DATASOURCE_TYPE = "googledrive";

Expand All @@ -23,7 +23,7 @@ export default class GoogleDriveDatasource extends TextDatasource {

/**
* Datasource for Google Drive connections
* @param {Credentials} credentials The credentials instance with which to
* @param credentials The credentials instance with which to
* configure the datasource with
*/
constructor(credentials: Credentials) {
Expand Down Expand Up @@ -57,11 +57,11 @@ export default class GoogleDriveDatasource extends TextDatasource {

/**
* Load an archive from the datasource
* @param {Credentials} credentials The credentials for decryption
* @returns {Promise.<LoadedVaultData>} A promise that resolves archive history
* @param credentials The credentials for decryption
* @returns A promise that resolves archive history
* @memberof GoogleDriveDatasource
*/
load(credentials, hasAuthed = false) {
load(credentials: Credentials, hasAuthed: boolean = false): Promise<DatasourceLoadedData> {
if (this.hasContent) {
return super.load(credentials);
}
Expand Down Expand Up @@ -89,12 +89,12 @@ export default class GoogleDriveDatasource extends TextDatasource {

/**
* Save an archive using the datasource
* @param {Array.<String>} history The archive history to save
* @param {Credentials} credentials The credentials to save with
* @returns {Promise} A promise that resolves when saving has completed
* @param history The archive history to save
* @param credentials The credentials to save with
* @returns A promise that resolves when saving has completed
* @memberof GoogleDriveDatasource
*/
save(history, credentials, hasAuthed = false) {
save(history: History, credentials: Credentials, hasAuthed: boolean = false) {
return super
.save(history, credentials)
.then((encryptedContent) => this.client.putFileContents(encryptedContent, this.fileID))
Expand All @@ -116,7 +116,7 @@ export default class GoogleDriveDatasource extends TextDatasource {

/**
* Whether or not the datasource supports bypassing remote fetch operations
* @returns {Boolean} True if content can be set to bypass fetch operations,
* @returns True if content can be set to bypass fetch operations,
* false otherwise
* @memberof GoogleDriveDatasource
*/
Expand Down
2 changes: 1 addition & 1 deletion source/datasources/MemoryDatasource.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TextDatasource } from "./TextDatasource.js";
import { fireInstantiationHandlers, registerDatasource } from "./register.js";
import { Credentials } from "../credentials/Credentials.js";
import { getCredentials } from "../credentials/channel.js";
import { getCredentials } from "../credentials/memory/credentials.js";
import {
AttachmentDetails,
BufferLike,
Expand Down
Loading

0 comments on commit 96292d9

Please sign in to comment.