├── .prettierignore ├── .vscode └── settings.json ├── source ├── tools │ ├── tag.ts │ ├── polyfill.ts │ ├── hash.ts │ ├── buffer.ts │ ├── uuid.ts │ ├── date.ts │ ├── vaultManagement.ts │ ├── attachments.ts │ ├── sharing.ts │ ├── vaultSearch.ts │ ├── entry.ts │ └── encoding.ts ├── env │ ├── web │ │ ├── environment.ts │ │ ├── rng.ts │ │ ├── net.ts │ │ ├── compression.v1.ts │ │ ├── index.ts │ │ ├── compression.v2.ts │ │ ├── encoding.ts │ │ └── crypto.ts │ ├── native │ │ ├── environment.ts │ │ ├── rng.ts │ │ ├── net.ts │ │ ├── compression.v1.ts │ │ ├── index.ts │ │ ├── encoding.ts │ │ ├── compression.v2.ts │ │ └── crypto.ts │ ├── appEnv.ts │ └── core │ │ ├── constants.ts │ │ ├── prop.ts │ │ ├── global.ts │ │ └── singleton.ts ├── credentials │ └── memory │ │ ├── password.ts │ │ └── credentials.ts ├── index.node.ts ├── search │ ├── searcher.ts │ ├── tags.ts │ ├── groups.ts │ ├── entries.ts │ ├── VaultEntrySearch.ts │ ├── VaultFacadeEntrySearch.ts │ └── VaultSourceEntrySearch.ts ├── io │ ├── common.ts │ ├── formatRouter.ts │ ├── formatB │ │ ├── signing.ts │ │ ├── history.ts │ │ └── compare.ts │ └── formatA │ │ ├── signing.ts │ │ ├── merge.ts │ │ └── Flattener.ts ├── facades │ ├── detection.ts │ └── symbols.ts ├── index.web.ts ├── storage │ ├── StorageInterface.ts │ └── MemoryStorageInterface.ts ├── web │ ├── LocalStorageInterface.ts │ └── LocalStorageDatasource.ts └── core │ └── VaultItem.ts ├── test ├── resources │ ├── attachments │ │ └── image.png │ ├── vaults │ │ ├── format-a │ │ │ ├── test-archive-0.18.0.bcup │ │ │ ├── test-archive-0.14.0.bcup │ │ │ ├── test-archive-0.21.0.bcup │ │ │ ├── test-archive-0.22.0.bcup │ │ │ ├── test-archive-0.23.0.bcup │ │ │ ├── test-archive-0.24.0.bcup │ │ │ ├── test-archive-0.25.0.bcup │ │ │ ├── test-archive-0.14.1.bcup │ │ │ ├── test-archive-0.17.0.bcup │ │ │ ├── test-archive-0.20.0.bcup │ │ │ ├── test-archive-0.26.0.bcup │ │ │ ├── test-archive-0.27.0.bcup │ │ │ ├── test-archive-0.29.0.bcup │ │ │ ├── test-archive-0.16.0.bcup │ │ │ ├── test-archive-0.34.0.bcup │ │ │ ├── test-archive-0.19.0.bcup │ │ │ ├── test-archive-0.32.0.bcup │ │ │ ├── test-archive-0.15.0.bcup │ │ │ ├── test-archive-0.28.0.bcup │ │ │ ├── test-archive-0.30.0.bcup │ │ │ ├── test-archive-0.33.0.bcup │ │ │ ├── test-archive-0.31.0.bcup │ │ │ ├── test-archive-0.41.0.bcup │ │ │ ├── test-archive-0.42.0.bcup │ │ │ ├── test-archive-0.45.0.bcup │ │ │ ├── test-archive-0.49.0.bcup │ │ │ ├── test-archive-2.0.4.bcup │ │ │ ├── test-archive-2.12.0.bcup │ │ │ ├── test-archive-2.16.1.bcup │ │ │ ├── test-archive-0.36.0.bcup │ │ │ ├── test-archive-0.38.1.bcup │ │ │ ├── test-archive-0.39.0.bcup │ │ │ ├── test-archive-1.1.0.bcup │ │ │ ├── test-archive-1.3.0.bcup │ │ │ ├── test-archive-0.35.0.bcup │ │ │ ├── test-archive-0.37.0.bcup │ │ │ ├── test-archive-0.37.1.bcup │ │ │ ├── test-archive-0.38.0.bcup │ │ │ ├── test-archive-0.40.0.bcup │ │ │ ├── test-archive-0.48.0.bcup │ │ │ ├── test-archive-1.1.1.bcup │ │ │ ├── test-archive-1.6.2.bcup │ │ │ ├── test-archive-2.0.0-0.bcup │ │ │ ├── test-archive-2.0.0.bcup │ │ │ ├── test-archive-2.0.3.bcup │ │ │ ├── test-archive-2.14.0.bcup │ │ │ ├── test-archive-3.0.0-rc2.1.bcup │ │ │ ├── test-archive-3.0.0-rc3.0.bcup │ │ │ ├── test-archive-2.3.0.bcup │ │ │ ├── test-archive-5.0.0-1.bcup │ │ │ ├── test-archive-5.6.1.bcup │ │ │ ├── test-archive-6.15.1.bcup │ │ │ ├── test-archive-4.0.0.bcup │ │ │ ├── test-archive-4.8.0.bcup │ │ │ ├── test-archive-5.2.0.bcup │ │ │ ├── test-archive-5.9.0.bcup │ │ │ ├── test-archive-6.0.0.bcup │ │ │ └── test-archive-5.0.0-0.bcup │ │ └── format-b │ │ │ ├── test-vault-5.0.0-0.bcup │ │ │ ├── test-vault-5.0.0-1.bcup │ │ │ ├── test-vault-5.9.0.bcup │ │ │ ├── test-vault-6.0.0.bcup │ │ │ ├── test-vault-6.15.1.bcup │ │ │ ├── test-vault-5.2.0.bcup │ │ │ └── test-vault-5.6.1.bcup │ └── webdavServer.js ├── index.js ├── web │ ├── index.js │ ├── Vault.spec.js │ ├── LocalStorageDatasource.spec.js │ ├── env │ │ └── appEnv.spec.js │ └── MemoryDatasource.spec.js ├── unit │ ├── tools │ │ ├── entry.spec.js │ │ └── encoding.spec.js │ ├── facades │ │ ├── detection.spec.js │ │ ├── tools.spec.js │ │ └── vault.spec.js │ ├── credentials │ │ └── Credentials.spec.js │ └── datasources │ │ └── GoogleDriveDatasource.spec.js └── integration │ ├── datasources │ ├── WebDAVDatasource.spec.js │ ├── MemoryDatasource.spec.js │ └── FileDatasource.spec.js │ ├── facades │ └── entry.spec.js │ ├── env │ └── appEnv.spec.js │ ├── formatAToBConversion.spec.js │ ├── formatA.spec.js │ ├── vaultRefs.spec.js │ ├── fileVaultManagement.spec.js │ ├── saving.spec.js │ └── search │ └── helpers.js ├── .prettierrc ├── .editorconfig ├── .gitignore ├── tsconfig.web.test.json ├── scripts ├── generate_dts.sh ├── gen_vault_format_a.js └── gen_vault_format_b.js ├── tsconfig.web.json ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ └── test.yml ├── webpack.config.cjs ├── karma.conf.cjs ├── ENTRY_FACADES.md └── VAULT_FORMAT.md /.prettierignore: -------------------------------------------------------------------------------- 1 | test.js 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifierEnding": "js" 3 | } 4 | -------------------------------------------------------------------------------- /source/tools/tag.ts: -------------------------------------------------------------------------------- 1 | export function isValidTag(tag: string): boolean { 2 | return /^[a-zA-Z0-9_]+$/.test(tag); 3 | } 4 | -------------------------------------------------------------------------------- /test/resources/attachments/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buttercup/buttercup-core/HEAD/test/resources/attachments/image.png -------------------------------------------------------------------------------- /source/tools/polyfill.ts: -------------------------------------------------------------------------------- 1 | export function objectValues(obj: Object): Array { 2 | return Object.keys(obj).map((key) => obj[key]); 3 | } 4 | -------------------------------------------------------------------------------- /source/env/web/environment.ts: -------------------------------------------------------------------------------- 1 | export function getEnvironmentResources() { 2 | return { 3 | "env/v1/isClosedEnv": () => false 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /source/env/native/environment.ts: -------------------------------------------------------------------------------- 1 | export function getEnvironmentResources() { 2 | return { 3 | "env/v1/isClosedEnv": () => false 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /source/env/appEnv.ts: -------------------------------------------------------------------------------- 1 | export { getSharedAppEnv } from "./core/singleton.js"; 2 | 3 | export type AppEnvMapper = (template: T) => T; 4 | -------------------------------------------------------------------------------- /source/tools/hash.ts: -------------------------------------------------------------------------------- 1 | import hashJS from "hash.js"; 2 | 3 | export function hashHistory(history) { 4 | return hashJS.sha256().update(history.join("\n")).digest("hex"); 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "printWidth": 100, 5 | "quoteProps": "consistent", 6 | "tabWidth": 4, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /source/tools/buffer.ts: -------------------------------------------------------------------------------- 1 | export function isTypedArray(arr: any): boolean { 2 | return /^\[object (Ui|I)nt(8|16|32)(Clamped)?Array\]$/.test( 3 | Object.prototype.toString.call(arr) 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /source/env/core/constants.ts: -------------------------------------------------------------------------------- 1 | export const CRYPTO_PBKDF2_ROUNDS = 125000; 2 | export const CRYPTO_RANDOM_STRING_CHARS = 3 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~-_=+[]{}|;:,.<>?!@#$%^&*()"; 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [{.prettierrc,package.json,package-lock.json,.eslintrc,.babelrc}] 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /source/tools/uuid.ts: -------------------------------------------------------------------------------- 1 | import { getSharedAppEnv } from "../env/appEnv.js"; 2 | 3 | /** 4 | * Generate a UUID (v4) 5 | * @returns The new UUID 6 | */ 7 | export function generateUUID(): string { 8 | return getSharedAppEnv().getProperty("rng/v1/uuid")(); 9 | } 10 | -------------------------------------------------------------------------------- /source/env/core/prop.ts: -------------------------------------------------------------------------------- 1 | export function assignObjImmutableProp(obj: Object, name: string, value: any) { 2 | Object.defineProperty(obj, name, { 3 | value, 4 | writable: false, 5 | configurable: false, 6 | enumerable: false 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | *.log 3 | .DS_Store 4 | .history 5 | .idea 6 | 7 | # Testing 8 | .nyc_output 9 | coverage 10 | node_modules 11 | 12 | # Debugging files 13 | /test.js 14 | /test.bcup 15 | *.test.bcup 16 | *.test.js 17 | /stats.json 18 | 19 | # Artifacts 20 | /dist 21 | /web 22 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { init } from "../dist/node/index.js"; 2 | 3 | let closed = false; 4 | 5 | init((env) => ({ 6 | ...env, 7 | "env/v1/isClosedEnv": () => closed 8 | })); 9 | 10 | Object.assign(global, { 11 | setClosedEnv: (isClosed) => { 12 | closed = isClosed; 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /test/web/index.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { init } from "../../source/index.web.js"; 3 | import { getSharedAppEnv } from "../../source/env/appEnv.js"; 4 | 5 | init(); 6 | 7 | Object.assign(window, { 8 | expect 9 | }); 10 | 11 | getSharedAppEnv().getProperty("crypto/v1/setDerivationRounds")(10); 12 | -------------------------------------------------------------------------------- /tsconfig.web.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./", 4 | "esModuleInterop": true, 5 | "outDir": "./web", 6 | "module": "CommonJS", 7 | "declaration": false, 8 | "target": "es6" 9 | }, 10 | "files": [ 11 | "./source/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /source/env/web/rng.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a UUID 3 | * @returns A random UUID v4 string 4 | */ 5 | function generateUUID(): string { 6 | const { v4: uuid } = require("uuid"); 7 | return uuid(); 8 | } 9 | 10 | export function getRNGResources() { 11 | return { 12 | "rng/v1/uuid": generateUUID 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /source/env/native/rng.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | 3 | /** 4 | * Generate a UUID 5 | * @returns A random UUID v4 string 6 | */ 7 | function generateUUID(): string { 8 | return uuid(); 9 | } 10 | 11 | export function getRNGResources() { 12 | return { 13 | "rng/v1/uuid": generateUUID 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /source/env/native/net.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "webdav"; 2 | import { DropboxClient } from "@buttercup/dropbox-client"; 3 | 4 | export function getNetResources() { 5 | return { 6 | "net/dropbox/v1/newClient": (token: string) => new DropboxClient(token), 7 | "net/webdav/v1/newClient": createClient 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /scripts/generate_dts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./node_modules/.bin/tsc 4 | sed -i 's/.js"/"/g' dist/index.d.ts 5 | sed -i 's/module "index.common"/module "buttercup"/' dist/index.d.ts 6 | sed -i 's/module "index.web"/module "buttercup\/web"/' dist/index.d.ts 7 | sed -i 's/module "index.node"/module "buttercup\/node"/' dist/index.d.ts 8 | cp dist/index.d.ts web/index.d.ts 9 | -------------------------------------------------------------------------------- /source/env/core/global.ts: -------------------------------------------------------------------------------- 1 | export function getGlobal(): typeof globalThis | Window { 2 | if (typeof window !== "undefined") { 3 | return window; 4 | } else if (typeof global !== "undefined") { 5 | return global; 6 | } else if (typeof self !== "undefined") { 7 | return self; 8 | } 9 | throw new Error("Unable to detect any global variable/context"); 10 | } 11 | -------------------------------------------------------------------------------- /source/credentials/memory/password.ts: -------------------------------------------------------------------------------- 1 | const __store: Record = {}; 2 | 3 | export function getMasterPassword(id: string): string | null { 4 | return __store[id] || null; 5 | } 6 | 7 | export function removeMasterPassword(id: string) { 8 | __store[id] = null; 9 | delete __store[id]; 10 | } 11 | 12 | export function setMasterPassword(id: string, value: string) { 13 | __store[id] = value; 14 | } 15 | -------------------------------------------------------------------------------- /source/tools/date.ts: -------------------------------------------------------------------------------- 1 | import { DateString, UTCTimestamp } from "../types.js"; 2 | 3 | export function getDateFromDateString(dateString: DateString): Date { 4 | return new Date(dateString); 5 | } 6 | 7 | export function getDateString(date: Date = new Date()): DateString { 8 | return date.toISOString(); 9 | } 10 | 11 | export function getTimestamp(date: Date = new Date()): UTCTimestamp { 12 | return date.getTime(); 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "esModuleInterop": true, 6 | "outDir": "./dist/web", 7 | "module": "es2020", 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "target": "es2020" 11 | }, 12 | "files": [ 13 | "./source/**/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /source/env/web/net.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "webdav"; 2 | import { DropboxClient } from "@buttercup/dropbox-client"; 3 | 4 | export function getNetResources() { 5 | return { 6 | "net/dropbox/v1/newClient": (token: string) => 7 | new DropboxClient(token, { 8 | compat: true, 9 | compatCorsHack: false 10 | }), 11 | "net/webdav/v1/newClient": createClient 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /test/web/Vault.spec.js: -------------------------------------------------------------------------------- 1 | import { Group, Vault } from "../../source/index.web.js"; 2 | 3 | describe("Vault", function () { 4 | it("can be instantiated", function () { 5 | const v1 = new Vault(); 6 | expect(v1).to.be.an.instanceof(Vault); 7 | }); 8 | 9 | it("can create groups", function () { 10 | const v1 = new Vault(); 11 | const g1 = v1.createGroup("Custom"); 12 | expect(g1).to.be.an.instanceof(Group); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "esModuleInterop": true, 6 | "outDir": "./dist/node", 7 | "module": "es2020", 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "target": "es2020" 11 | }, 12 | "include": [ 13 | "./source/**/*" 14 | ], 15 | "exclude":[ 16 | "./source/index.web.ts", 17 | "./source/web/**/*" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /source/index.node.ts: -------------------------------------------------------------------------------- 1 | import { getSharedAppEnv } from "./env/core/singleton.js"; 2 | import { AppEnvMapper } from "./env/appEnv.js"; 3 | import { applyNativeConfiguration } from "./env/native/index.js"; 4 | 5 | let __hasInitialised = false; 6 | 7 | /** 8 | * Initialise the node/native environment 9 | * @memberof module:Buttercup 10 | */ 11 | export function init(appEnvMapper: AppEnvMapper = (x) => x) { 12 | if (__hasInitialised) return; 13 | __hasInitialised = true; 14 | const appEnv = getSharedAppEnv(); 15 | applyNativeConfiguration(appEnv, appEnvMapper); 16 | } 17 | 18 | export * from "./index.common.js"; 19 | -------------------------------------------------------------------------------- /source/credentials/memory/credentials.ts: -------------------------------------------------------------------------------- 1 | import { CredentialsPayload } from "../../types.js"; 2 | 3 | const __store: Record = {}; 4 | 5 | export function credentialsAllowsPurpose(id: string, purpose: string): boolean { 6 | const { purposes } = getCredentials(id); 7 | return purposes.includes(purpose); 8 | } 9 | 10 | export function getCredentials(id: string): CredentialsPayload | null { 11 | return __store[id] || null; 12 | } 13 | 14 | export function removeCredentials(id: string) { 15 | __store[id] = null; 16 | delete __store[id]; 17 | } 18 | 19 | export function setCredentials(id: string, value: CredentialsPayload) { 20 | __store[id] = value; 21 | } 22 | -------------------------------------------------------------------------------- /source/search/searcher.ts: -------------------------------------------------------------------------------- 1 | import Fuse from "fuse.js"; 2 | 3 | export interface SearchKey { 4 | name: string; 5 | weight: number; 6 | } 7 | 8 | const SEARCH_PARAMS_DEFAULT: Array = [ 9 | { 10 | name: "properties.title", 11 | weight: 0.6 12 | }, 13 | { 14 | name: "properties.username", 15 | weight: 0.25 16 | }, 17 | { 18 | name: "urls", 19 | weight: 0.15 20 | } 21 | ]; 22 | 23 | export function buildSearcher(items: Array, keys: Array = SEARCH_PARAMS_DEFAULT) { 24 | return new Fuse(items, { 25 | includeScore: true, 26 | keys, 27 | shouldSort: true, 28 | threshold: 0.5 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /source/env/web/compression.v1.ts: -------------------------------------------------------------------------------- 1 | import { gzip, ungzip } from "pako"; 2 | 3 | /** 4 | * Compress text using GZIP 5 | * @param text The text to compress 6 | * @returns Compressed text 7 | */ 8 | function compress(text: string): string { 9 | return gzip(text, { 10 | level: 9, 11 | to: "string" 12 | }); 13 | } 14 | 15 | /** 16 | * Decompress a compressed string (GZIP) 17 | * @param text The compressed text 18 | * @returns Decompressed text 19 | */ 20 | function decompress(text: string): string { 21 | return ungzip(text, { to: "string" }); 22 | } 23 | 24 | export function getCompressionResources() { 25 | return { 26 | "compression/v1/compressText": compress, 27 | "compression/v1/decompressText": decompress 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /source/env/core/singleton.ts: -------------------------------------------------------------------------------- 1 | import { AppEnv, createAppEnv } from "./appEnv.js"; 2 | import { assignObjImmutableProp } from "./prop.js"; 3 | import { getGlobal } from "./global.js"; 4 | 5 | const GLOBAL_INSTANCE_REF = "@@__ButtercupAppEnv"; 6 | 7 | /** 8 | * Get the shared app-environment configurator 9 | * (provides a controller for handling the substitution of 10 | * functions that need to work differently on different 11 | * environments) 12 | * @memberof module:Buttercup 13 | */ 14 | export function getSharedAppEnv(): AppEnv { 15 | const globalRef = getGlobal(); 16 | if (!globalRef[GLOBAL_INSTANCE_REF]) { 17 | const appEnv = createAppEnv(); 18 | assignObjImmutableProp(globalRef, GLOBAL_INSTANCE_REF, appEnv); 19 | } 20 | return globalRef[GLOBAL_INSTANCE_REF]; 21 | } 22 | -------------------------------------------------------------------------------- /source/io/common.ts: -------------------------------------------------------------------------------- 1 | import { History, VaultFormatID } from "../types.js"; 2 | 3 | /** 4 | * Convert array of history lines to a string 5 | * @param historyArray An array of history items 6 | * @returns The string representation 7 | * @private 8 | */ 9 | export function historyArrayToString(historyArray: History): string { 10 | return historyArray.join("\n"); 11 | } 12 | 13 | /** 14 | * Convert a history string to an array 15 | * @param historyString The history string 16 | * @returns An array of history items 17 | * @private 18 | */ 19 | export function historyStringToArray(historyString: string, formatID?: VaultFormatID): History { 20 | const hist = historyString.split("\n"); 21 | if (formatID) { 22 | hist.format = formatID; 23 | } 24 | return hist; 25 | } 26 | -------------------------------------------------------------------------------- /source/env/native/compression.v1.ts: -------------------------------------------------------------------------------- 1 | import pako from "pako"; 2 | 3 | const { gzip, ungzip } = pako; 4 | 5 | /** 6 | * Compress text using GZIP 7 | * @param text The text to compress 8 | * @returns Compressed text 9 | */ 10 | function compress(text: string): string { 11 | return gzip(text, { 12 | level: 9, 13 | to: "string" 14 | }); 15 | } 16 | 17 | /** 18 | * Decompress a compressed string (GZIP) 19 | * @param text The compressed text 20 | * @returns Decompressed text 21 | */ 22 | function decompress(text: string): string { 23 | return ungzip(text, { to: "string" }); 24 | } 25 | 26 | export function getCompressionResources() { 27 | return { 28 | "compression/v1/compressText": compress, 29 | "compression/v1/decompressText": decompress 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /source/facades/detection.ts: -------------------------------------------------------------------------------- 1 | const OTPURI_PATTERN = /^otpauth:\/\/[ht]otp\/[^\s]+$/i; 2 | 3 | /** 4 | * Check if a string is an OTP URI 5 | * @param str The string to check 6 | * @memberof module:Buttercup 7 | */ 8 | export function isOTPURI(str: string): boolean { 9 | return OTPURI_PATTERN.test(str); 10 | } 11 | 12 | /** 13 | * Check if an object is a vault facade 14 | * @param obj The item to check 15 | * @returns True if a vault facade 16 | * @memberof module:Buttercup 17 | */ 18 | export function isVaultFacade(obj: any): boolean { 19 | if (!obj || typeof obj !== "object") { 20 | return false; 21 | } 22 | return ( 23 | obj.type === "vault" && 24 | typeof obj.id === "string" && 25 | Array.isArray(obj.entries) && 26 | Array.isArray(obj.groups) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.18.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aEXitErfO7Re0Hzi2egKAJLjxbC0GdoOY8y+OvUfsVFyc+Kbxr3a9gcWc8fTdlv6oflML9h3sFWYAphpeJSHzDv0MLJTGbZZ90a2CRdjjv8vRBsY/SF0lIKCqKenwZicWgNO9eyvAzvRV3JmLjY6x7JA7gO6Z8v+cwpfwWtx/nbRdLMvWUB2tY7XHpi7VEcDw+H53gyj0qPmkFN2wXMOdctTJINbfwJ7PFqJ5fpZewx55YwzZG1YQJj9wGB9DbI4ht9owIwiB8BQUIbZF0DNO0TSA/CXSPc4fYbaseE66b2Ec8C7rrm9y2vaai1WsouPGGLj7PF5SeLXFowbtU/3ZpCWIKQRYEym7Cg6hwXEmAz0urObsQgRo5kjrGe7QAnptreY+KjtCr/52Smv+Sy3JpENlYupvKvrdqNSyA0hCnJaIWaxW4MwJQkCws6qSDF6amz6Tvoy2VxggooZEWhXf7Nj/oHTYMrOWxo2kRYB3GwFBdtrv4lHvlZ2VmYkM48AmEh71s4wveI9EEJ8pjQtKi7LYx+OP4ie0y5HjflzMofi33Ohy0pXchPHFkREkFxml+f983GAzuWwbjE1mF+QiXP4h+HRApRV7VJFzATQ1CNydU4tF/dyJ53mvt3DeufpQ2EHRpy6X0wpV2WeqfbaNgohaYByyqPDW2HLpwtPsozrc7pifqa6oopFgevz+MaUx67DHwFUqXXrWZrQTnPj1jQ==$0b4a4be1e42e7bdbb48300caf54ffe1e$54a9f1d1988a$4f42ad72c76894c62d59814e7e6a28eb03c67a24dd9e80167dd02a0a77f33426$6007 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.14.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aLonhUCibtVn7UG+1TngjTnzWFVvlEMwapyREHJt9mGE+6M9bfaG+gxhAH8Y+FSCzW346qcsYA6hOaXZYQRwE0G8/cQ4IWXJlikqGgQHxxrJYMsl4Yanc0LH0/83V3/oOriKYtssV26hafmJgqmgMiKngiVXZFPfNGovH1+MuNMbSRmg7t2Gb8psLhx0yPkmKQrOkO2q8Od4UuejKm+NyRu6EvyQggagSdq8Veuq0X9niYZvsPUm5UttMuetljCrAMjyDVRs2ndIuNTClbrISXNEXzBMtvXIh8/y9Jz9dcuqBRzafpueUsAvpxSqHxXkMAfzI46QLAsGgoBXAQfEu+N+DiQ3F1sy/syYr+FXG23TlfIL9f41jlCnrPAkWyW0S4lRYrmMHpTPPVI0Q60ejg530BLuhYPVaARkUmR/BpKt6Bw5gqqRcjX49/cLc+ZDUPQaNDH+oeVIyiAY5Mwb+tdMHMR1M7s0iuVWrFcp/R3F/V38UwehQUY74MUdVtv+XKlMrsykjU7Y/ELXHPDwN2BhVJwTzG5iImXLK5FY4PjIcoQWtgap/HjoU/jAAhJxqFvNfXIc/+/gIWR2SJb5c3JI2EhtIvzZbd6oJMX+nawiupXpcJxSDrSeN3E/RC8gaMyZVMCl7cjS2Y4FWVEONS+qUac/4GLZfmIKR/LrOJJXiXTevMQQzny3A8Py8WuJn8qLSQbFufsfYs/P9xLTaRMh6hUdDOrm8kP1ZOsbt6jU=$536af555a68f60c246d86b3e21a1a6ee$bae061d6c3eb$8e37fa93a669ba0f270ca95f21daaaf80467b88e023cefc722bb3527a7e8da24 -------------------------------------------------------------------------------- /source/search/tags.ts: -------------------------------------------------------------------------------- 1 | export function extractTagsFromSearchTerm(term: string): { 2 | tags: Array; 3 | term: string; 4 | } { 5 | const searchItems: Array = []; 6 | const tags = new Set(); 7 | const parts = term.split(/\s+/g); 8 | for (const part of parts) { 9 | if (/^#.+/.test(part)) { 10 | const raw = part.replace(/^#/, ""); 11 | tags.add(raw.toLowerCase()); 12 | } else if (part.length > 0) { 13 | searchItems.push(part); 14 | } 15 | } 16 | return { 17 | tags: [...tags], 18 | term: searchItems.join(" ").trim() 19 | }; 20 | } 21 | 22 | export function tagsMatchSearch(searchTags: Array, entryTags: Array): boolean { 23 | return searchTags.every((searchTag) => 24 | entryTags.some((entryTag) => entryTag.indexOf(searchTag) === 0) 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.21.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aihVx87SYWYkU2ZlXcDkwrAAiq0lhhfH+cF2SZqjdhtlmIoqgAL9DcnHX934UQUCu8Aat+MfDT3DND13KAoKfor9buC1RQu7WX11bH5xXI7xQIFwBs303aBJImeCplcQ6VG+hcBu9W7KmhryjtuhopJPrfGegtkC23qnYWAPszvOuZcvA8UA0mumooJdRvUN1hPybpDJ6ov3GPII6D6MmfkY8xqjdApR9OvCUQqbxJac83GhekuugFq4qsh9Se9t1vH9dN6wIf9xtTroRMzB9ue/k3GxtlqwG5OfhhOQicY+YMweuLUOuACRRFVh1ZqAt5DMcyuR+tvqPxXOZyAEWIrNK3Aiqzv/POYkV/u01W7d08cEbTHlh5+4/63rCs/rMIVRlL0tazV5c2YDPOnPS1PKSDizZ4XfIMJ3n7u3SQaYCQct+xucnsHBoAljTH/mR/WujtI1pPi1GnxQ01amxsWF7Q7NysLUyFq9G7eh6IXta0fLNI0myOhoI2FJZonFf8ALQ0ahXfj3YNu381FpwP2MxN0vrNeqzP+FPo6fI3p+FXeLM9jgMcgDPP13MCiJy1L0+4ezV1E86DkN2PvryngK7Mqcd1cSlFgDl8R5ow97ENzfxMRq7Ya8AYd+3O+9SZdlbci4zCRB53x+XTs1JusXXK2nsOIxu/IGR1tyCk5grj552OLPCsWJ4ziPowe4Z56wBa7kSJdE9ywkUHdOrJ526yNjTo/oeHfHw8+HRpF8=$2f79c13a4a4046a8166929bef793b442$64a216369274$b9c74153da8b3dbebc79ce397c176f769c4912d9ba45b907bdb93a3f2c85b00a$6886 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.22.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/atS54+7XHCQAzgxLOdMJ1+sVgMHkX4/6YR7V+mnb0Gv70OMDI2ivlpZUwYSzWrqfr+2ukHcdH1zWUTUjBac81CgNFNImU6tz3TOIFYVuL9DcMNK0pIm1cEqBWFU2dq9ifj6lOZ5QUUpJbcoCbtCEAnKWbDehrDLLDrvaIyZ7jCfDRk8hVoszWy7Uplz1O/QbGm1z0xn72RCYbzlunjE+NXRK0/VJpkaBy/NvE/E4f6IHhA5Ri8VjzYbDFP3iV/FHNZ5x99+fxrncs4Tjbsuny/e06oG+UEj9PAaAGC579oRqhYbi39KV7m1Myjdb7J2xTwYWvEsk/ThOUKNFPbnqU368nC7t/Br8pQ3QvicSnqJAu9gFzrf7GV8DVcVEmJg2haI9g+bAz4pOKfsJLuZU6PMTwlxSk3kj9Jt2Pvqh/JoNiIrUWDVDzpfPglW0f8NC16SeNdZTdMsbHN+TMCH7kSshe1ftPj1T0pNGGCB6Ke+I40+Ft76tcU04+uL8n0bVwJuCbltK1Tv0C/Dq+cTXUXVcWczHWD8WpNgdPUh/htaohbED5IXUauxvfZ6YUMBzXG/AvuLy5MD8t0HKbNm5ONxxmGtEQVSs9904cJNXk5YNt3AI+XJ5CLrvA9VwCeYvu0wTis1CCPLAfHA3OTwMIRKGSCg3b6q5VA8VjU5jgsh9+lQavqU4ayC5aUr2clQgd46WaFq80vqlbQSIolYmYtQ1ZGgykZ7zEp4+qbQYl8/g=$4b7c4f18b653ee9e098525aab8cc2948$6c233df9c79a$48e833ddf072cb8f0e4d5d3135b471abf03053626aaa74353d48ffb734b9ca74$6621 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.23.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/ahT7oVS5YqjQYqC9I4ZKY2AsT0QXSxWUJp8VG2XeVXrS2YRNoSJaV5CYEPHpv65cY8kA5S8NDJxblGdQyPeSIjYhDpsGW2yzXbIVnqfaGkztokOU23TsgH1Uv6blmI+rWU5ZF2CzOaErnQxFnMtE46EvfU+RwJG9/tow4CNAsli4+XCaewzL8d+lWBVc2/BPhlDRWY267RWAyRuAL6Lb+YNtUjwiSx25P5IbtywUK8d2hkZbmBvQI+l/CLy/T1yEk7Q9Yu3JEQqdAaEyRHnRPapTCwZSBh27xQZSxx1wPljm61lRvCQlH80ikH2CpDAw5LzpRDViYJpKK4kLUf/HTUYFUVq5vmv6ZdEsFRjuPcxaBOGwVFqbx6IvQzHI4ENFNEE0Y60jNl72bT00Usq6aKuw3HI0NXuEjYHGOr95jphiJobzXNmmzBFchgkA89rh/wlXtBE1qWKSa/P2oKc2pV/FQsvqsEoj+QVkoN/9/vzfM6pBB37O+pfG6BbYwBKJYbzYAOouxhlCcmPq3tbAVGFE3EnLhHSliTYCQhB9jMSJLYZx3cACuEYdPXleKPZE3MoLVawV4WgqcLmEiK+eoqcFpOzaHUVWJt/ZHUy6I3VNa6ODYUbPrHaHmY6a5HKo3ITp+VpbOIoCJ4xrUVmvspVcJ8LDeNl2TFhnKr6axyUmuHxgBTDvxPz4z1BWG/nW9TTm9e1bk54F8A1ttFzv6zQFELQjRZYuubYxT3mACQW4=$1ea0c1fbc209aada08ae7496f9e66ae0$b9530b8eb36a$7262fdb7bdaad9091a5bf935b115809428129e921e556d30c0b0f7f68d40225b$7656 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.24.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aynG4P2Eo5vA4rH0fqH/Vk7BPCiJh5U64y2PQv4vllTwam5qQHRg3sdg4g7MNhHAO1XOe38MHoPoDHL2Ml4yTV/wJBCRBR5r+GEjOBasRXeRCTgY2ZQFryP5rSkZCn5vYCWHB2SiJcpeLlGBamSOOKzi14uuaA35T/ncVUG60KDJBalTP7cJfCKYC3bIU+xKlL9TuCwucD4rNoPfTwTa8jHyS0oB529qzqG9QUdPVamKylEvWAoo/qlIs6Qm0AhuNSn4xiF2rjoF5I5/NwG+ePTsAd+gmYvK1mVqFCPd8LaLOw/S0UHkbmQUVNcMS9h2xgsjmK1nSh1RkcYk1fH1FvypzLXPcK7hVhiseM4O+2eKB99tbR9Kl9uSrhTu0dPNHX/4ilTgo2DCxtBP/rqbUXQuKivLDkvbZzJgUx/1Ii7mXMKHN2VVTV4SJNBEhmj5ifhcy7r/RP9ni+6ZXG9kT1CWQhSLWrPJwz+oWOT6KR4DhqqzcdSvlcILOmEK1CXT5stVj4hJew/LOeJbYIzy2LbUHo+mSfHUs5MlkSD3l7zLSSTot2gOjD6zF4OwqCUk58M78xmK+mQdrsz/KuyxWUqoSAM58GUrcdkquDSm4rEoNK28IjYzIjSs6QKX5mkn4q9UrHOkkzfj9lPL+BZsetUaXEVcZidQdVtlWYvEKf45/NmpyK35cR3AOJi64Y3Q+PyddAY29CW7RIpNSJTR5FWKznsV9nZqE1w3nDUWJtDI=$9d2bd58c5337aa8e150c8d1abdcd73b9$d2832b95bbf2$01ba3b0a1a16a5a2f8b4dbc93118a22f641b5e6a57294a58ca5b18a29a67c2f3$7603 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.25.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a1oc2XKGEbHh5jcSxOuXFYDnS1gKe8Otqh/lofBER4YR/6KfIAtr0jNui6NvpT4GnpRrKuVneiwc2BQWDVxl5etl/bVaJmn0ccjW2b0EVVFSzjucDOET2MLe8NLnz7zX5TWPBk9FNtmMsCxkklenAr1t9oqBwokkkKZB/15Sp/E21jLsI4fS68JWzaa/AeX0DJn5/tSSW3eQ3n5a21uW/x9pQ/ZNY+IF/bIROaZr7SgDDFR7tjmhayUNKDNBbnwPkdUSAups9JJ7q5Fm3sPd9SznKrmmk8CnjbTvco4k8yR7rl6rd4oYIJrZYeRIJXLa1w/hKOH4I0npYcq2W7tDhZJMtd/kaDupZ/ftzkppVHLOFUMvAj1JIvHCrCDZyNaYi4ZJnx5fT0+dETnIeWrxuLMtXDcv797C6SvWLHmVyhQmHE5ToGCbh9ZDF0fis/XZ6eChV/t6gub8TYDziLcvlmaNqiAa4oZatuOwTR5x5ju/kjgcVyl4rmxbhFquh8BSvLlRGta+7FC49HNVnCn63VxSSvyqfa+1w63Vsy15odYjn5HDLyd+tBIHiJyqy1pelOEGt1+3BPwxpN3R5tI0xJcQoKZxmWaVHEUUupujFwnXEbXuMvHuKJ0kPPeeIwvmbfvARS+95AA2myy+hCtF6vXPbdG9RPkIXaqnDSVuwWC/LH9M2aQURcr0ZlUfLVmhrPb40HldPQFmyF2XM1QPJVuOkY7m9o++7AHUZ2FXIH2s=$10fbc71dada4b1ca9cf6739dc1574979$b86d091cf23c$9aef22327595ae0295c4495f147a6810c27d6ca1a84ede604ee045682190d532$6019 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.14.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aAhTl9f2nVJ/Ckf2n9/2FKL/9zIPUSx71vDoNDOFoDDF4V1TKf46JKgredCFRlu+GEJAUMu6Xj0+Atkw+IWKk/D1eWw2Pv30ytJc/hqy3z744kNggrzj80uyJEZqhsLRRVAFrKkYXaabJhP2FtCv44mcCgNrOKel7m/d4CZHKmaTVV3ExzLDktxJy+Lj8ikO4w046aF99aSCysmCYyect+pksQ18bmtr4KBw1cml+iOYzos6S6WleczM8nUshVKsSfJ1lV/AQ79luR8YlkLfCJvD1P2Lnlmy2IhDcAsNX7LS51SNm1mKxtKKymOlveISUj2JxEhGbWkW9OIqCv1GpaopoA258P1r4HHuWASBD9q/TJsXZdTjd41vKQ6GjEUDWhCpi3AzqN8CIrTx4ixBuSTjEoWOfNIaYIr+M0+jvyMsdb+XDVyBOVOdMUdCfvqamtXXhHN7K686T8q8UrK3LCy0E5jhaItWZ75Rx1vKUjZrmO6RuYKAfeXf39LOdh1wROQm6q2XaRTmIay3URcrYM5wBWdRVovvrA8Fom95AcCyiBce7S/Rw4sbppwuKTaqIZq6DTNV2BKEk2UIib59yg4jhMyObDCtIjeu/QrE5cRi9DCO9KdLaNUrUHFkDScvAxpNR1B5KeA0knWNOpUCjDrxHtdeXSZeuGBhM79MQiF8XCqT7GimkO/UIdJpMqJnoLrArdN2jfgWO163Ut4pr/WeWkn19BRGDJajCWwRIlR7QzK82nQjrQP7EwGAVSj5T$9eaa39861c68f9370367424fc124a58d$cc77e6d5e3b5$09b10b3d04ed638af62ccfaae747d20d46d9a81125b454d967fbc6e47fa28b66 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.17.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aG/mMmQU+Dha1sjjFhw5vdY5sflOMIuffaHO0h9sMXqOvtpwX+d/ZBMtsG24m5SO7adErevU+5zI1EIOwJpuhuBPy/C4UEkVoaVYduObASj/9gEmWQqKIV8DPf+XpjsEcVsKOffy3DdRbtCOpz7br0Rb0W6vWffizr88X9kS4CPzdyyO9p4AKvVJLlJc/0iuCwjyShMpbT5uCpadmUs560tB3RkwvyHlv/RcrmtSegNJfOzCFiJK2SNDBZPcWf36eZVUaNpihe7fISMhHC1thsUZeYPmL79cGCb+KH9Qd81zzPdVVCEcUkLmiDAXIW7AMKFYxRATloUgZY37cFp7YkrKqs17H2ucSqtPXtymc9vPmkTaf9ub+QGpWmb8BE6AFKR/3vLPsCNgc4HTrYDWXtlGq7LK7WUpy9GQbmzJM/TXJzVbzs3OgpN/j4M+kmFMI5DvNBHS1HF02xMShwWO696yLn9aVtbJvhC0LbwCkxiEtRh4hxRZ12OKPjPHTOWzSupLmH1RjZ9ni3u6amoi+o+/l+d3Yzids4tWLp8qywv7lXLt0DD+ZY4U4ng60X2vZ7B4rrpK+AB7iZ9DRJJtXfY7TVYphkxYfnX0qMOzGA9TRLIx/5E4CgdkzZLTNeuvUbnQVkao3F3SdcSf9fn70DVjp+v4bIvSEigATUnF0Tw1z5S5Q/NVdAYLtmSfp26rSZ9wQKjrp84uHG7Xm7e9XN7msj8DVQlQjqSfd5TXC2AFHnxoQV3BzBDXWIoe9BAyW$8df09f20de1fdd617f22589d09439b0f$78d0f06b3b4a$94291589c81a4734fbf239c4fdb8b82fe8bd8f6f4e7d263f2c7a31e8118b4c9a$7710 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.20.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aIK1ALn0wZ9GKwAvZh+IP6/PVLgtpZxxEIs35lGHj/RF/lIV8ohSjvFVO3mfMvTrXHUTl/D/sz/jVfZMBP7ZHOsfukPcfKtue9A4MtxB5XNX+SRKmiPE2qPeaImIdMiijbkVlzxR2hV2ytt9lPeU5ZuTYw/PSOwotnsAc4TkLJT9SWJcZ1FlOLJFtIuYQS8SBTAFNfk3v/IOedTe2wa+wDHmTTFP1w3zJqaepVYhZco6oup9MbO6rxmT8D5uqLTRObEqaui3xcI5s3PNE+OyfE/fPjJxO0EhZj8olJmEKu7kWXT18hoyCxCC1tQwt9j1tZrDshmZjAf8YMbNatXeVR4Yqahf+it7eoT1R7WHhY43olomk1goMVlvDo0c+zpvrgoYHdltkoYaxB3csqFKQ7aMPMr6dzwXl1EmWXp7VqPWGYRtVk01DaoGLX88q0lZalPYff0elSDvBZIQi+A1s9FQ8/UAWRRbpxJX1K6v9DS/UXtKQZJ5rMUfGTQprvEg3D0qIgJ5g/wJpdCDPVsyFs8KMbsY6bHZNWuisiXRFWEA3DK0FaCkWcPBtalMXk9T0lm7JCYUVxPDMgL3QPyCcur6O4rfdclFMAq0r56KvUiaN9+xGf+pLJGj5POhBVJHmWIB5WRbyXOVcWINIFdu0MDBXHEIn4tNVpEW/7E5vYq6BilfMjPNMn1bDAW2aRA4yr5Z/leTGaETPP/cU4+v5wsMX9Bg/E5mBrkBdp1L/ZSn9bviQJ7KMMIVTocN65BlL$37265b4a492abd4c15c1e746b18454b6$e9d247309dfc$b4f4a7e2d0511f17035934346330a47e2f8ba0a68e82f6fae70a4b1287b6bdec$7325 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.26.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aMuFjb+aSi8pERfZCMYdvxaCPcWCoprEByPH7TD5r9W+XuO/gfybmp8LdOPYmF6AsDxWjkhVi8Poy/uwnfRMlhiY+auYMOVHlM7e3312gZDVM7MkLXVAj3QFJxsSx6vhVeAw61NX1bhkfOlZp/hNlC6UnnyUFBux2SY1RdyJFHVbNOPwJNigG8vEvlvHk4Gz9Z39LQMyA/63JEfrwdihoKfjYcGr0TnZbtLZ7C+TeGSVLiuW9aXKfqZqn5KvjqjGq8rYUuBOKslNSS2hrcI7hfTe2FQ9ljV3L3/mFz+5hsioVSY4nyND5t0n/0UL6BOSTfctSTIjfalA4k93YtCpwYdwdTuE6IFbEopzDO70mGVJ+nOWH3IukqH/kIK5I3MP0o5IxnWDHrGZzqj1wj4YdcGhcMWsOdTE0HLPZWTjqtgN4Cen124jT1in4Srdh7cqYwwLMORr4883uj+TCt8WnmYzE1mB4pJsFNnBfO9oJo+Omo5MRoSCsFU2ilMYUvM3HavyAEOcdWM5hh5U64p9orQCJydeGSNlVCHOdLASkBg/NsqH5BWSkUpdUi1LETswDIY3oIKIVoW01M4c6waFUTIy0sbo+cjkLKDKC5ovnso6//V9bvSeESXmZtbsA95Gt/7LiUCYvF5taLKt+ZczunsW/MZ9+AJhWHtCm0ejIMONKgZgDNCJgHdzS052C6fW1/VPFDeZ0JN1GVe5WShm3G0ts0afjbKzhvDBphdmcL7BVImAcmcW2iGc4+Mbl7cAn$3b1b14a79e053aada0bfbf03cdc3c6cb$5ba5c0f94cdd$706fc43cb16f2036b7c9839c6bcfc43c827c6e53aa267a63e33d61418c49f764$6947 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.27.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/asuFtUxPYaVjE/t7Vdeo2R1KyQaWTeNDwhUCaiyDUJGPJN4AuHl8Qfs2CPfpEh4Q0aZuLPXoEFxklxuIFhgFkbEeNr0PA1oBG6VGv1dVQ4ZpKSdHjn3+DlLKKXhKZFqZZwfTp8P2tqB+VR6K3an7uzey0CG1XDJnR8KTEasS2SoJhggMUqxHqXpYNs3H4zA6Ub/yTdYDyzNyoV5t5hk5cMAMjqUfVbgfVBL/ce8Pa89HBntrBKCzc3DIJf9abOyM1Vk+7Bg1V6GUpMUS34+dBB/oUmfudXKfj114WtL18Su+aZZ94pVbmyPCBH2k00JKC0Hln8la2/6okfg5FRV+eWBpTe6eBIZF+BHZ4L32RZkuBZetIbUmVcbKMQj1DjSWPWeWvkJGD+Gvp0N7q6BdXA3Zwj+vWFRXuL+EvPyaHpO/vgDWRCRc4EoaV6wlRA9I33/3yNWVg5oftpR/DRt4vjAYoe5E0XtbDHU17wrgqXiOjC24t4U7WMfwN4YOvtvYFVcLP+9s165pxYEtB2kFrjeHIGHPBL7GtCctm6dkyg5v7+rF9UqpWgtkezrRsnClvBJuo3IEewNN+9d81NR3+Nwx2U31ON8euK0LAegdWwFUYgJupzGZ3xgSDoAkEWkuI4nVHKC8mtTGtTT1fp0U6hq9GsHz72hldCiQJyDMp6A3Ax5cnkkfX9Wy01uOMW8y7yExuCNTzgetMyRn42+/7hhdxeXS6u0/7waxTIkJXBqk8k2cDhTNwqGmgjXlnlPj4$0a7b7f572d3d59031cf3fe8082cf74e7$d5e2945b005a$9c16377300a527e201ff431149b7048716159c7f1de4e27c4a530d3498c3dcea$6400 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.29.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/av4n3t7+b9khMHrJAjvQcSZdZ/0GnLSlNQNBhpnc/5cS3GaSTnSSlaTSgdhmZaAEv4SoHIYuTE+sS3cBOfv2WXK96BYnn4K0exoblas8tXYkzvqAgo10U+rEsBGhquUyz1JT7s68MSuri/lBBgTYfE0Ai+uSn8CzP5OH2w9ePeSi6SqRPramhdUVy3ZMLTyfrk7NoeQbrhDWOxnnHgop15ALwj4/DSpPTbY2Oc80IbGoNnc4++1vAr8XOmyVt89nUxmnIA3Q1HWYcUON0HuTtFze+LtLKns8MNMyXir5K3QvcNFSp0U+SCgFxTPGfaAEzEuMJAflTA4RK2bqUGiLakymUFrz4ImBEODWUrgUr0h35RMhT2qPYLgCJVz20vH7oY9UblYZjdmrzADAWLGg+Zz7XoUR57VeXjaZNUWt14EimtRJQgUw9m7v+PXS2kX+zhDLdS5m0nvDR8IWoI53s29ei0FvOvZfAha92wA2HW+97Ub9kuvzcvp5f831gbSxVDl6cHTxTuAA+ywwDRmYyzf8r/NsIxysUfqZUj5npHL25qU0opttg9boLIYxWYoSemFyw79+cQLFvh04ixF11SwkQgI6gpGi+4LE/tTc9Ay3oMabeOa/ag/bzzOXvdFeogPmumv9fPFDrLYDD9B2HHz0QUjmqeA6/4lLPxUehAxfDhEoBESJL0pZ+NIHSLd3uvN1J+8mYzP5Aa5v2Z8ROn59TqsJbK3hUyQrQtFFpnx9omKsnRznvq9JDjfwaG0/P$54e0dc8c64f154d7555af46f907eccb9$4cbee0f25ae9$ecbb4e5cc1d0dafa4f9ebbb18e0ce6ec902a4053b4bb02465c620f39387c2f41$7327 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.16.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aQZecV5BS7Emo1vFUGMZZzQqV/JaFvXiccmXgEtheeEPyCNYPogeVtXxhF1MbWx4PjCrL6Bl5/pKm/mdfw1xRVgb+yjxhhQJ5C3RoD++n/2lM83uRDzwpTWMCzIzc3b1VwVzA5MGgGeDxWEi9hE1PuMFmpNFedOinA+aYGxk5cuc+b4ZcVMNNRzkhxb2l2SCCiVhhsA+mDvQBJV3R52VgTedSndT1du8y8xeeyUw500U0pfKUwgWx56HtjRRc94sluBf7LjaYqwHam06bOpl9OoIolHOh8xxQr57KJphHq0VKfSKd14Ci5tMcE3SkVWCAEVRJU9iiRcA+GG+KaB36+CBipK8CXhZ3WoUe4f7Gqjomcx+9NNkDzNxOIerk2CvwrY0bGRysNUgtn8NYRzXGarcTOuPE6so6jm3SfMJr4jJijgFmdDCYwFa4GwnuiWO0HaTDnEdiOp+1FVfX5rjtIJxg/WHlV1zhr/M6TYXZOTlD8v10j6m0kvG+HEY8cVLRt674ej5UW8Hf4ifBSryoCSr0Qc8OsNTspBCijuo8qYJgq1AFgdVvXCDr8ryL27gfwhXBT1Bp6rEhAR1IDdt+zRyaLt4jEKpe6Hic7BlJsb/l8CJDxdLwpVtXXA795ThWZ0a3EJVb2kgQAMpluBsTXiUkyJ+EAlLlsglQ3o5XaLyMT56HyJNuB59zdcOuOV2o5xU/3omnHeeApIrq6FMmsBQeusuzjE3WyGAykz8GoNUs+5Ik1m4AHHX4kUCkaM/W$05092b0bcb28cc6fee8d7b3fe7333f80$036f60eac4ea$ee67489e680ebaa7195f24f2064a11a5dad11a92a558b89b578aa1232c685318$201791 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.34.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aRl8FUlUoygXqyGnibqqaj8ZFmp85uRRZTxz4kDuGBulMfkxxCIfRi6uddSLiZ0GbJpq2ATnJBFP1c5BcgPMM6ZLdXuOQ88QBJ0k6xpr+T0E2ZZI/nbpvYmQvRbzMFY4u7aN0DpeY2iwIsaP9DFdJZl7dZBJA56JhjGd/GXWVUjWtWBdyFB5u4Xqvis0vrCinLtrbR8ph/Y6EOUqGzuxrs3bvhlkhOSrjKj4ZDhY0T9uK8rZQdp9MGHJS41OFmQqm3AkQGA21ud6FGFqG63qG63vjnfZPpv7SYUWf2UPeMQrHQjlZ5KlsdVEM4h5hU7IQquSp94b8wyd9pm3Xh4wJdrekTS27mGgOW0Yp+nPmCct6qen4buNqWsEOo/WeTcWzZzOADq6lb1L2SHUGiN6B2EA1jY96B0m6yqx4uezSw4lAdrd25JKEdTOUmke1ihw2yl8a2PsShstjIIvYafwTmWjS/kTe/s1FFBxmP5rzRgA7eRqpN5LmCLyZqwyQoUnp0GjpcqTRe3ETO8QGq9DyJ6QLHPcZ/z/Py3oYVVxmCife5VysDO/svCrdtvbcmYgWyKhCIzA+ZNGUcIX5gbLWC6LD7aCsQ8E2iBmZ8uY8VAbUNa7VPp/m4vs9JAzDaKVaiDh4U6qxvEtjk58tHRuTt1qWqwkFNqcWULDISNG6YMNp//syPNRjcxs7nEAZpKR5Gm+hnz07MioLfj2n6TWL5EieNbBAYcK/OMktwHYHkNYOQlhdp+e0lNIvun1g8aTf$b46b316ede697e474b186477cf245842$fe99569973a7$efa407fa2d4ae10013d09636fa45fe96b24b91fee1eceb286ad37845ae667f01$214841 -------------------------------------------------------------------------------- /source/io/formatRouter.ts: -------------------------------------------------------------------------------- 1 | import { VaultFormatA } from "./VaultFormatA.js"; 2 | import { VaultFormatB } from "./VaultFormatB.js"; 3 | import { hasValidSignature as isFormatBSigned } from "./formatB/signing.js"; 4 | import { VaultFormatID } from "../types.js"; 5 | 6 | const DEFAULT_FORMAT = VaultFormatB; 7 | 8 | let __defaultFormat = DEFAULT_FORMAT; 9 | 10 | export function detectFormat(encryptedContent: string): any { 11 | if (isFormatBSigned(encryptedContent)) { 12 | return VaultFormatB; 13 | } 14 | return VaultFormatA; 15 | } 16 | 17 | export function getDefaultFormat(): any { 18 | return __defaultFormat; 19 | } 20 | 21 | export function getFormatForID(id: VaultFormatID): any { 22 | if (id === VaultFormatID.B) { 23 | return VaultFormatB; 24 | } 25 | return VaultFormatA; 26 | } 27 | 28 | export function setDefaultFormat(Format: any = null) { 29 | __defaultFormat = Format || DEFAULT_FORMAT; 30 | } 31 | -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.19.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aF9wrTEhp8s/Db3bf9WqVIkZxp/qXCEB1obBNEeJU4me4j4Bia1sEsk6mmi6kug/wz/PZm2LDoraR1dvDWbbA62GAhRPRCcYnKPyYo4Srm3BEru43W39jWrYrWJsBeFjfP2R6OQQ46PxM3qBzAFgm1z1epHsJqP2XPwSd0cuS424vSip6TBjWvypOZqI2snt/rjlYJmkwg+WFoZkfy9dcMhQ1Q4FJdBnnIxPQAN5CNViaRQdFa8vUIZDzVqfEbBMapyQHMg/+EsKGDtvOc4jVE7JXAwtzj8w6F3gMbPGyGjBNKDQXl2uhQTbwb2Prt4nNSE7CRWoWPteoVMDSwWPi6uXVCHuzP8f+iD6j+zF01jvsVJ2+/HmeT3tfyMBGjaRn5X1N87VVfH8IkW3HS1KJwNVMiF7wDtvs8hnDB+u/bfzRXRM7S/nXZNmwytb5VpgdPW/zR7jE+HWH90r9GyOw+Fya28w2TAWmuovFpGasbDzLb3ee57eDUzkQZbQ3xpE/wNXkqE5AqcLHjpxwzQgu30k47LqJP/J4LDGaPcOxsmUgJIdMznwiPTTS1Y8LOWnwrdvIxGdQ57dHHO7OyjoIUfKwb8+08oShZFm4SCCPCMGlITbBZ85esIotsoK+u92qb81IvZYCqBWv0ngWwToWADb6SzPQhXYN2LKUSVrPC20mGM8kbFfMAtDGhS7wtIhkh5S5SM3UtGAaY9l4opIospSaw0DEr+Wy1AwEAug0Q6WTSlmQMTKHAWs08OIOU4w2rzaJwDAhttSBgDCgVc9s+A==$94ee5648cef0511ec716c24e1a0d70a7$0d642635a2e7$c9ebdc29e7c08a1f4833e06c094eed1c61fcad1045ca4f3c8055eae65be62404$6391 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.32.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aar9yal5PG2hsRD3NPYQHAY8uEBp/41U53dkl/xZVOkY3KS8VGHSkX/mk6h7PE0EaAKyCzskjEtjIWWNL4BR25xTj5wAFFiun38lSdLgy0WyxAsu0GXluKHx62n8ZQLAZikEagNG0DravpkskQHl6hQjCIlVWoL9+gcREGQuIuzsBIPfwaHhhZGcJIv2FSe5CbOzpbko7+hYzSJrFxqNAr9HSVHQYYpnM75xWJupTGyPvHlUmJrOWcftwZvf7+5NmpQB7Oo/sBoaVmrY2VugpmbEO8tTrHicyoK2VHWyuCyJVorH4dttfErRkIvkEs6xlDMiybFB19QYk1GS/flZi6rRo/QJwboU/dhX36bhIVxM61Xhaf0yNa+GCREboomzC0ETMplDdrEjglDsrr26uXysE86Q/r27/eLFU65AOR7DF4QS+V5Hze8m0itpQnTenTVXort3w2s3i0FD0v6it3Xcu2P50UFq4XMTuxynCBne2XH3V/wenZu6UsQefnNqwW9W2pKUuoDaIhNk/OwsQyx2u2q0mr4Gmph52yD5oatBvC8x8vyMxFdFY482Vxjovms47XfP3yjgidtSOcIP0xKk5aVdH8VZtYtlexQFDQvgXLJW4YZK+dJ/m3lpBr0ggW0ExZTCj7a/hN95DmOVT6Et60pFi2pW0xUN615AxrtCxX8PgAN2H67Y5Io4sRR37hs5+BBcLX0ZcJkpvg/Q1TGzhJ+WN0YZh9sLHvnVb2OmAL0VSHRyQQrrdaRCaofegbI1KC8wRdqtiFK27SG8oeg==$78ec09d90d71c240f323ef97ed732165$cc5817e1f606$4a94cf0fae86adb1afb10d0edf0e166f8dd6db683e89c347adee2e76d8585a63$6167 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.15.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/asmRNaODdIKQVEiSmHVbYsVkD1FW0FpLnWd1J9vW4ccpO6Zca0Cg9G49Z7aZmfNUaSKMLpRddLqOxM3isLRWxfY+l9Gz0clDVYdxH+KOsEJcg2khDDoYCdu9brskeYwd9y9cGY39gjOKfqorbqexyM2hky/3ONQX2OhW4cxjnTeHSoloxXR4SlCG/hAzcycGMcEAgtPkAMALj6wg4rlS7jYiVXiV8A7Avzf8mguVXZqgwaDxyx2dql5PNcHgy4/bytr5dRU6Uyuw/2M93E4mYgVoUcM37Sb94UjJ7+gS0VHlowPmDZBVmBq7M9f1zj1UhpeteH582u42/WTS4Kt+36y52joP/l7Buh0EtUhZCc+D9WkTqN//YeHudrUAES2fEiUSZC4Yu2R++9Z6bmQZ1rgHb9Ij/BT4n4zh3qCYR9kRCIqSWdMm0kEO6kJUQca5k09QlyKXoNw3EOQdzQJiF974ZOS+X28b3ElPNnTJb8nWoYr+4x/AULd7DBG71yoOuxEjIy3Xl86O//LCpTBwF+VibN4gqGvk8zo3cxprKfhI2S3FO1Z3CfKLNcQZMOdec9COTdmGtaNyyfv2aRV0eGHZr3tzrrNrMi+YmFl8t9ZMm8hYY2ijVJVlTYJeLpBdTJMv203KHgrR9/tEk074FBGVbTtM7m0kAgJxyYJ62K5sO0VdAXMSLW45BmRQ9VJCj2QOBWLiD3Ns8SggJEY4ELq/hZEukknD+xs12s5ylZIlCQPZ5U33PpMPYOomjTzykZ/zKsPlo5MNKvBqcuDSjZg==$979b0d351a70c220f6eecf32db9d6036$2b40084e4e03$d37ef077df2adcf66816e853789fedf69a0970b38a5a6c11c401d32cf3c739ee$228654 -------------------------------------------------------------------------------- /test/unit/tools/entry.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Vault, getEntryPath } from "../../../dist/node/index.js"; 3 | 4 | describe("tools/entry", function () { 5 | describe("getEntryPath", function () { 6 | beforeEach(function () { 7 | const vault = new Vault(); 8 | this.group1 = vault.createGroup("test"); 9 | this.group2 = this.group1.createGroup("child"); 10 | this.entry = this.group2.createEntry("Bank"); 11 | this.entry 12 | .setProperty("username", "u12345") 13 | .setProperty("password", "passw0rd") 14 | .setProperty("URL", "https://bank.com") 15 | .setAttribute("BC_TEST", "test"); 16 | }); 17 | 18 | it("returns the correct path", function () { 19 | expect(getEntryPath(this.entry)).to.deep.equal([this.group1.id, this.group2.id]); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /source/index.web.ts: -------------------------------------------------------------------------------- 1 | import { getSharedAppEnv } from "./env/core/singleton.js"; 2 | import { AppEnvMapper } from "./env/appEnv.js"; 3 | import { applyWebConfiguration } from "./env/web/index.js"; 4 | import * as localFileClient from "./web/localFileClient.js"; 5 | 6 | let __hasInitialised = false; 7 | 8 | /** 9 | * Initialise the web environment 10 | * @memberof module:Buttercup 11 | */ 12 | export function init(appEnvMapper: AppEnvMapper = (x) => x) { 13 | if (__hasInitialised) return; 14 | __hasInitialised = true; 15 | const appEnv = getSharedAppEnv(); 16 | applyWebConfiguration(appEnv, appEnvMapper); 17 | } 18 | 19 | export { default as LocalFileDatasource } from "./web/LocalFileDatasource.js"; 20 | export { default as LocalStorageInterface } from "./web/LocalStorageInterface.js"; 21 | export { default as LocalStorageDatasource } from "./web/LocalStorageDatasource.js"; 22 | export { localFileClient }; 23 | 24 | export * from "./index.common.js"; 25 | -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.28.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a65nBVYvZ0q77hq4aiKvqKTBkjPzehSodxPtT1RI3QQfaf4Z6ofmDQW4SeaJ01/yQW8HHdJo1kM8V71TinAmZrlaENhcV5QfaLD0PQ3Y6xLMbvwSjmGujxbKev4Fb5lJSQ7YtOOJCsGSCXKPMDCoibgedgv4zE2ZsWzfOPDrgM0b4xwnxzofy5Qjr6Ckgqd8GLS35C7k4eclF7VAD4Qf1Gnb9MCNqDbMzn3qBwuFxkF/ncTnwXl0zo3KyloaHRDvCQXChbfZxonHnTjVoResE4n+Wo68I6ASkVw8FpHwXsJJo6uag4scamEVNqnO9h+6ktTgnQ6pLyGrUjzJAG8YWY9drxjnaLrRXXrILAE24+7oy0mydo7imAVQv06VtEHO/TXkzRcE2Aq0bHiZdnXRg0VzHx5MoSQa7hxj2/rMLMVC3K0lRA1AhN5TnppIHiHJxcwQpj4Y/icPDyoAmPnHSMeCgeV4vZYXUWAmdE2d7ZTXEgJfRNoISxnANXCBBZMPH1rneMokacMfK2qxpxbUBohWr1jj+JrwsZrwfy691Mn40LsxeMHNvtWA76ke3CUeDR9/U9v3FSoApxqqjWxuU66nCiRDveLxsTWJpJpV/u/LQw4qAv004bGXe4w2GCCahUiXMHoo0fR6HstV2sVp3ZwVvwWTnJZvhEA4FHT6CzkvPniApGxSt3jvY3kFLiOfFdBHu7Df32bKMD97ngfA37NaezMqCxAsonn6Qw6QoPQzwyyvJ6PHhzlxURhlQBWsaclfZrFCT2295x0osfYPU4T/Dea7xuqy9Onxkap2zB00=$37164c87203ce09563cbf91704f7c8b0$236f0e2d53b5$f0e18c0453eaf2a08dd59e19ba795914b79d8aba02dd38a4d0286787fc072826$6833 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.30.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/axsBPDAMo10wftIAb7cBS3k8PBK8xBW/NXlFaDGH2t9xklXln3wTXsDV6DF9yxb5vmhfdaOduf5bAdNntW0hy9osYPp/qtuyrGR/Ns35lM5pgTTSxNqTV+lmVKsgjX+1Rrt5CQE0vjNzf6p+pOHWYv3YrrPSQ014gqxkzci6iBCY/AoqjhsqK5ktkNRjZlK3EVxdeTx5mBcxe85RoCeyT83jE8G1opEnBDFtbhCkDnbjJRfatCXt2fhzZCnegQs7Vbk9/8CSgGijaC5HARt9CszqBxp9nBd3+EVowwCroLPdOMr9kHGi6+vkLwNEUoxFAxNL87526XUNx3U+hXq+RAeTJmVJzj4giFAL5fpkeqZzy6XREg4b/ljxugitEc7suXr5CwPv/kOYfgfK50jtVaTS1ZKwT5ayGIUTIgYWqMOM51gLX9jxpr9B/Z3XZntmyI2j37tAgTUtygTyUu7oC8S9/oYYnsqNXM9bWSwYj8aHCkZ1IRL5t72c/Feu0POTQJWvuJTqgo8GNpV3k0+vMg+Q2w/ay9mpfcE5OtdkHSYGN1QXELT5VuiLFP4Vij3AMbkOyEmWdMdB2ODkpe840QJqdzMmptiBR5qvoUctMboLiJXdA0311octWdr0axhfFOmK8yzxPz+NSaoLR2TY6DzJ3T2mlvWIxe2bqVABjAdVhwcojJfWDhg1PuxQBZDGWFLDE1JvI9unUyPN0e5WWyGAEKi+1d8bzSdsY7+S3l0lsrcp0tVIOljTi9SafUgkhD1LgD7Oega/7KMYRK8213+ELSro6dXyQBI+EqzqEA7o=$e5987cb627d776f5602a8261e452591f$e44e5bd86634$872849ef11c80f8421411f9225b239e8d89623500a665bdb7ee1a1714770b621$6730 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.33.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aAc1BtKCTN9yXlS0BfgT9L1zXQNNovINRv3oh+4rhc/z2qwg+I/t+fDY/DXiYl/YIjMGskScVeMpekTEArW9+CU+z0nyuBKqw32jn4BE8SWIQz/SiLNm+LNZuafKj1dqBarLZp89oNeijHcpDZyqSIQBsgjqb3q207ymIZjxYjd6b8KkoNkXNAe7MmO/fvPXOGWwJyzCyUCJ5ZeHo+uPIHC/vy09UNJAXh0xzApeh3FzJ90qmhOXnY305q4+yRQI8JRwbVawxrtOwNGVApDced8ZuwCZChVbShlRjtipUdj55GbK2E9Y/L7SPRROfz8JGj2vyvcoPvqs1TwpiR7kDVf5ZV7O0uAb4TkLhonqnRfHI6IaNzqsxs1ORHf5XmMybDtJw/lSF8xC/L5vmozmTh+0ZgrRKi2gTWKu/CP/M3TdVmq6fNLMf1iDbgkkpWU5F9rw9IXrYOWod8qYR7q03etpeA/yxD7BRpKZPKKj8GJ97spSLuQg/GGcpFIFKWGKaj00rPNRTZ7cEfrDEoYNkt4NdnDfdtNg+FoloOHwWAwgqTfoc82RD7FEm6U5NQI6TC6q5CW1YJ+N9ZDDjRFnApd28FiedLn+qSg0qLZPoZztw/Ko+tSdO5XiToP6x/tKx62LvY/RMmK02W2PEibFg83Yf1Q1Zq8f0/we7w6ckQi38GImBUnt+9qFJXwjQ1CRqBV76gBDf1GED0E/GbFffyOv2kSzLF32fUmgSutTOLbcxlpxtNGAeUYoDoAhXqJn5Nz/wLE2HewWoprf7HXRgJ7Le9optDTi9M19HEiYynlw=$1d07b1f6b743ace44d4db762cddbaf85$45bd2d1d1163$c7c17d4d0ad3927aa82c2208e3d5b027d56e8e8b567b83fb151c19eab8986e4a$6298 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.31.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aTgawEBujxxbjaMXW0DETtZp26WqHxEizyD1a5D10R6AvAGaUHLw2BQPb0K3++tkhiJZkSIk/vbotfehDQUDYsFi9Su9vZTCDgtZ0i3mkqPBX2RN0kaiC6bFougJ6bJ5g2nJpODKGVFwo7J6JXep61ytLbjwAtLUXchnZR/E21Au0WpZR5sY9Kly8GVlrD48RlC9Ksp5qprqstzw5qsiaWKDV3vvkmwaSM0jOWcBoggrKiVqj6DA9SCOadHHCwOhxlte8buHQxOguEzomkuGI4dzGsMBx6oKtf1VdzwVuuOE6qRbXVcKFwLHUhQegXkbuQW8eSfHoYRX46iJPvOvAWgyD9zgLRRC4BO9keldnzM2w2AC1dOwwjHlquGgH5JDQ9IujyQgogOEGd2Z+BnDIkGRovADFyz66QwcCY6eK/iqKKlf+qcSOrjJknJej9ujQyd7EEYe9ORuHOTI4BDNDCHQD3lcwY/2bwPIe39bTK2GLc8s9C3s7OiD50gd6wGum5gRDT2mB8F25ZH+dpE+2qsjNlFgdYNb27s5OotonaNPH8Ihh/KVI1fHb9usqcUMFMNT12mc8fgVUuy6eACVkXg88OjC0BI10w+EAfoxpCVoW/mZsr0O51XKkxHLPcdj/jUssW70RW/zsqn3mpYMWhDbHO3s9KH20KFPaiG+usIvwX5eqPQ0q1Cot8ow5iVnkRzwKcVXA/MSou1+OGuZqaxOdo83GbQsBR4f4VJ7NyCXoXIuN/4jedHL3Tzda4XD3kiSS7Q0YXxyvggCliQt1yrgErwFDzQ5zFCu10b2oaJ2ZVEYhF2/a09JvxPCmTFI6$c9449960cf0bee335d0ac71ed440d6da$6f47aa65e91f$06ce71ec47da44d1f2abf917dd6e9f1a61c3754ef6852c2b0c0062d4a9aac995$6468 -------------------------------------------------------------------------------- /test/resources/vaults/format-b/test-vault-5.0.0-0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/b9CUwGKqwwCf8buE0yxqPgCx7YzIQmt1kJAdc84OVeGF+gCoaTqXqXvXc/JOPAtxa1bDltnsrLv8DvRCaw5uSBS3r0tkdqwu9Vw0p9B3aCN9AMz4xXF7nawKdoKwmT52q5eLI9s1t8Rv/J6NY8mT+xCkSXep1eV+faHLeI84PaxkzNKIruYPaq9GTyxBE25A+w7oFfVj67BbNttDi9HOClM7Z4jN6rilQkmHxUQWm0/zSlpH7ykMzR2xL0k/7sQ4uEF1x2SRnv0y3epSk6w1LvmttKvKL5HlLl8bjSLCXXyeavsl/sK0Q+Q+wPfHUtScERER0rEoEyLN1QTXlMsMIUaPQ/yPsqt4y4uDuiWXtXc1msKoU3mk1nBsXssPUHUT81qSGviR5JzpYIfOGktU9dBiMKJXGr2IkNXr3FvhOAtwkDIJZau0PmvFSHP/OFYTLftpQHlV6tsJ+nM5VBUOIMTdXBywEadGR8PF56Y50AdrdXglgaCxq10VzFNCEbj8bTjlL5esFgSPrUbD1ZBAWT+bMC4pViympX3hOOiCddE1IItvbeaKgvqZ6MZEHXZNIqeYTIzB7QcBBq8PDmEuvbK4u7rE/SBm2tXCiYIb119SeMvx9bxSdAJ0SeQQJdkITuC/7hjmZgYtYgtm74V6y2g2BTUci8k/AXI3S/OpZL+GyySbTw1HYstPiKezsj407wcu0oihzTuKdkMWx9ZYEqQvVgOj0x0oR+efLekCEY79LR88rGG4z7ctU5yx6J66Sjow6gHVViwKhU3MIM6bLhjSThyigCNFu7d5H9Ri53l7HdGINgS6qQ/dCEH4NcDDu$b26f7a4211e56a4377cdd8ca04f4d5dc$z79QfUEYDlhd$22f9981a0df605dd7bbbd3b4b25a8e83b0ab5c2afe0af0e6633fd23dfae5c1bf$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-b/test-vault-5.0.0-1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/bGS8U6wwO4KrHK9EXmaKfW/SNSxoFnVcjZG7z1K+1T1CY1X7KIPadaRGPlnSLpENvSUAgkb8U1a/ry/UQpUP4jb85mz0q0/kO/O5LV+eynW2+yZtwRcwM7ZAX5ECh6Cvc/8ZvkLcEvYCHMp6Q2FCBrpy4YE1bQlQfTRi7r8ra+zRr1XOhaEkAGiDXxziDKHqkC/zwdrWWan8O3Kh8ENIBbZsy+RN0ug98ZdTHUIH5Ysu5aG31h2nXnoajWQvwKWLksTKCQN4WYV908odgf2LSDxtLPtty/T3gmmpKcqIh2r03zr0e3U0CS/Moc6k7EpBjXdpKIwSubEPYojHayTuPhjQa3UaPBYuGhVXHy1BBi7UOx6h5ugu4i9TiT94uurBowX0VnGcikXUJ1zb3bt5BS+fVympX2tPHC9uGChSDd5S8Na32G1L7hyS6ORj902ljRMRJ+wygocDA68mRlLQW5ee7xkaC+I16fmx9EqN6tAnvFgLduuna//NXOLfljoDPbEBh6BwlTZ6PJP5Yvd+YYU77ks15HgZKxjTG1cruYI++c36Xj4S1SwzpdZVE7fOemt/zdBwzJNrbas4791ctkVIR9XsJHmcRJ9FnH47vMj+pBFG6ElrIHvANKEPYCbgKgiaOV8MzOsAkSVNKb0tXgQTkupDqFjkGVejgxrDHFAjZhDOTppgMe59lQRBAKbkCI1M1O1wghUZjD4xNYjCaYBVzwGqZyo676uo4gmOIDpPoxkx6xAl2DleKNOI7hugcfimWr8JCdQNs3yGfbeRvOmqHbDO/w7Z/h6srVoqawHexKiKY18iSMB5+JolU4VuNmqOhZrPsyW+dSCjxKhgvPQ==$764e830c7b2ec42061e58ea79031c0ce$g6V8zW1itZgV$8a2ec031e4ded131cfe9412e1963ca40d67e83d3a0ead6bf84d474c69cf16366$10$cbc -------------------------------------------------------------------------------- /source/storage/StorageInterface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Storage Interface (stub only) 3 | * @see MemoryStorageInterface 4 | * @memberof module:Buttercup 5 | */ 6 | export class StorageInterface { 7 | /** 8 | * Get all keys as an array 9 | * @memberof StorageInterface 10 | */ 11 | getAllKeys(): Promise> { 12 | return Promise.resolve([]); 13 | } 14 | 15 | /** 16 | * Get a value for a key 17 | * @memberof StorageInterface 18 | */ 19 | getValue(name: string): Promise { 20 | return Promise.resolve(null); 21 | } 22 | 23 | /** 24 | * Remove a value for a key 25 | * @memberof StorageInterface 26 | */ 27 | removeKey(name: string): Promise { 28 | return Promise.resolve(); 29 | } 30 | 31 | /** 32 | * Set a value for a key 33 | * @memberof StorageInterface 34 | */ 35 | setValue(name: string, value: string): Promise { 36 | return Promise.resolve(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/resources/vaults/format-b/test-vault-5.9.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/boTqKUwZSJrAX3wxKkNioCdzXxq4cs+s9t0W3XjeqHvcxlCf8OcdZHq9ZSlc/1IazTmNy+JMa5RevPXoB5NuJFkE8rF0wivJhez4p3UPPVFI5i8WciDXoR0yK3AbmX9odRElYPtUwxZhPD2HQ9lHd7IaDbVcSxvectLJZqzqOJzjxD8Aw+p4R+QsY9YzrNT9TtINXE/M4RRAkh0nXP1qlV5AY69k4/DUJp3XF2lRRPIlJtwdbP4CR8H2VGCUWvYSsOGISoxsYjmT4zPPXUuIpDuDuNLedMFkt4WG4gTutVBjjzxOB60V8tMo/nNTLtSGtF0l9MkOhLt5ftMSsEVCe4+GlxItDhhCmHV1rNPAXqy98r671anTbr1cgypxCYUBIiKUh0wja9mQuIL73WlLpqsiDunaRxpl4ykWJktZMom3AVekdluur8dOwBqCoJ7D8fUuV93cM5DA1mavZdEXpg3K91/IQzC3h0cL+E1xbYawM7Hu09rdRdSqrT4c5yyMy2YRhuo/oYKGrq8L5Qkm+/8YzTYSyteswLibYQNLHFH+qR+qEU/gjZr2iRccMHQO58FjkKXlcj+RkQePlpKCgm94Z0ri5XAl/7ZQlY+pcw8rjEPfXRrVlrcos5N3/3aZxqTixq/PsqoI4A01qSe+T3tNcU13clem2h0A8Ndhm4yWozmxJwMvh0IKkmzAWTAKIX0eVaNrYzqrSMk3xshJaR/NDQkdDLg0+HJxmyeT8K3IeGYuY3nXTs6sj0ak/49cTBOV0+fSykZMGkLH10y+MJDBSICo/MzYCnA9WcpgVFt52JXskK2WxGmEIpjgx98UD1pt4R8vTmL7tuyqXLSBpmWtGQLpK+uiAQirkf53+bJ4=$5db3826abe0ca26906e026b2f2dd9eee$iDnpSqVWohjf$f69a26ff0743b45274fc57ba050b34c1945e97229acc51a3013ae738a9b70238$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-b/test-vault-6.0.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/bTTLIMqgubGuDD9SUh8+J4/HtagU3LwslGz9nvRxPJUyyVnizK/KRzG5rbMdzFGQxohtkQdzRfjnuCQ0kvzkOdC4yhvKpQQO1hr4vOuagq2xoGUUwa4kUvXCFX2Kg5E+WbVAx2VCbwCKEGQWXRyNhocdS6THavDdU1tEtVy4nV4oHm/vmVACyLldGBD8kPXseaBVQVVLMC74Y9bEJoW1pFOdkTi+cV5+lRuUt0M1mWAL1V3I24jVsXYB6lr1G65LnLfxlKBpR8CeI2+wCS1ImDvHei3jlW9+2tnVhonXAVMH3/rDrZ5ziIqPFQTkS3Zn6rMhY0YL03kMWcYYZq5B+YX1lOv/fhb95B8DwEIqIk419/Z91ufNKUjPHiRtP8eOt1j/0jwLwUuob70CPJfCPJuF0cuOG6OLdu5i3lLdUwdku8U9j2qCxyMHGqCeatof8/+cJrgMGAB4bTH6L5T1t0UL8f5VeRQSSm7lAiEir4juS9ttyu7zxQtj7+1tmXMX9DJ+ZPHl4JbqTj1GajOfE6NimXxsGrL0sDbU1twKDv9yanvX0ABludTtaWaMj8Vp+6cqZN53r6JHA4sq+qSRhGMJQC6zteRRrC4GTHyUSZd0b0Ty2wR3LZFgbzS5aD3+owH8W9u+4BtnVm/ccRoLkQFW5zHgV5lY1xDl5EqsWYzpdjiGUIbVa/c8ahq0PhQUjhJUKOHUzW+QhTJhXkYtgJaU2hw/U6MLncAiCR4gNXezSqG2pgJ8w1e58+7Ub7Dh/lVPS0k6ruVr6jP/WDaSyu0YodCvjo5rbVTi/r0kqf3rEHoi0nFZP/dM8P7d+jzQmIa0xZ0YlwGdBXJ25TiM/3VriLbIBHFZ/11nxjkbyy2I=$e1721d4b954e37feaa3e1c1774ac6a69$1REjBjOLbZ8a$aeb9f3c3bd5bd8b81708805d65a9f5f56b387cbbe9066be35c64adfbb332b0e4$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-b/test-vault-6.15.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/bhQ3WMIKzVJ887sjsJU3xML1PgmGAW/w13+9kHqc1fyWoyibeyXDcHBVTQj53X9ZemQFf0q4uEFKHQUFZ+sq0XBOSjgduOMtfcRwlTczVQ4JRS4qy0N8dwW6QfVoYb8lq41wva4lkx3OQRaPUeHBcH86vnUzq8g3Fye8nFpYnUX09l0t6wAWl2Lpu6yGpFW9vpPasmHbV9AcloiTAYE/IDye6k7SQ7765LpZO2HzlhlKSoRf5qzkgaQP5Zo3NaH19wqHN2ALkV3vUbQaM/5Pq4JsX03B/hYbERfJ6LZvQq/zBmjOYgEEPk+o2sR6WSsbLsIZmPkU3XF3Fv871/lQnfvff/QUn1QzrHddgRnka0d7zVSs0x4s+4LZWQAmhXaUyemadNZ5kDbgQ/lHGd23auCMx4b2/inBlR5Gu13ECKsKESz8g8l33gX56E3+D+cgEZ2o7xu2gCRul3juYE7QuIGUa9Z7hAVo/iF1W55Yu0XHgCrJwmyiiCAhJwEp/79cGxXF5JYMxhMgMkG/uH9nUIcTthZ6S3eu3pIHxxjno1x4lzl3M2yTh4AGBR75o0MOmHWL0HkSrI3Po8U3ZWbZBUOJFFPclnzPaEsW+KbqMzfk6Hl0K9dmsLiNpMsHlYZPfyaRl/5zOGVnYcck6ocbBCziq6zbDWy/UOZtgkQXaNMxnhYLmjMmbq01ZOIBjvVQQNDscgaMgKHGZzTaxB3qc1pNnPSJ1TuAPD9tTCefsvEAl/Q/+rVhzk2ki2J7SdVJCgooUKGLsDJKy1BhbJWBhfiTRvNC1jZy/+4Vo0/VFGhGYKJwrn6hcaALeAshnKoLftf8P5Eebz1rnSYZq7aWpVtSupRvIH33UcWStFL05u+U=$497a2cbf1761cc5f0948175b597bcc86$QZ9+DT9FvOdk$a32ddaa653a13150ef486e415fcde6993cdb68ad9f73fa9606e56cc2a1414d80$10$cbc -------------------------------------------------------------------------------- /source/tools/vaultManagement.ts: -------------------------------------------------------------------------------- 1 | import { StorageInterface } from "../storage/StorageInterface.js"; 2 | import { VaultSourceID } from "../types.js"; 3 | 4 | const STORAGE_PREFIX = "bcup_archivecache_"; 5 | 6 | export function getSourceOfflineArchive( 7 | storage: StorageInterface, 8 | sourceID: VaultSourceID 9 | ): Promise { 10 | const sourceKey = `${STORAGE_PREFIX}${sourceID}`; 11 | return storage.getValue(sourceKey); 12 | } 13 | 14 | export function sourceHasOfflineCopy( 15 | storage: StorageInterface, 16 | sourceID: VaultSourceID 17 | ): Promise { 18 | return getSourceOfflineArchive(storage, sourceID).then( 19 | (archiveContents) => !!archiveContents && archiveContents.length > 0 20 | ); 21 | } 22 | 23 | export function storeSourceOfflineCopy( 24 | storage: StorageInterface, 25 | sourceID: VaultSourceID, 26 | content: string 27 | ): Promise { 28 | const sourceKey = `${STORAGE_PREFIX}${sourceID}`; 29 | return storage.setValue(sourceKey, content); 30 | } 31 | -------------------------------------------------------------------------------- /source/env/web/index.ts: -------------------------------------------------------------------------------- 1 | import { getCryptoResources } from "./crypto.js"; 2 | import { getCompressionResources as getV1CompressionResources } from "./compression.v1.js"; 3 | import { getCompressionResources as getV2CompressionResources } from "./compression.v2.js"; 4 | import { getEnvironmentResources } from "./environment.js"; 5 | import { getNetResources } from "./net.js"; 6 | import { getRNGResources } from "./rng.js"; 7 | import { getEncodingResources } from "./encoding.js"; 8 | import { AppEnv } from "../core/appEnv.js"; 9 | import { AppEnvMapper } from "../appEnv.js"; 10 | 11 | export function applyWebConfiguration(appEnv: AppEnv, map: AppEnvMapper) { 12 | appEnv.setProperties( 13 | map({ 14 | ...getCryptoResources(), 15 | ...getV1CompressionResources(), 16 | ...getV2CompressionResources(), 17 | ...getEnvironmentResources(), 18 | ...getNetResources(), 19 | ...getRNGResources(), 20 | ...getEncodingResources() 21 | }) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /source/env/native/index.ts: -------------------------------------------------------------------------------- 1 | import { getCryptoResources } from "./crypto.js"; 2 | import { getCompressionResources as getV1CompressionResources } from "./compression.v1.js"; 3 | import { getCompressionResources as getV2CompressionResources } from "./compression.v2.js"; 4 | import { getEnvironmentResources } from "./environment.js"; 5 | import { getNetResources } from "./net.js"; 6 | import { getRNGResources } from "./rng.js"; 7 | import { getEncodingResources } from "./encoding.js"; 8 | import { AppEnv } from "../core/appEnv.js"; 9 | import { AppEnvMapper } from "../appEnv.js"; 10 | 11 | export function applyNativeConfiguration(appEnv: AppEnv, map: AppEnvMapper) { 12 | appEnv.setProperties( 13 | map({ 14 | ...getCryptoResources(), 15 | ...getV1CompressionResources(), 16 | ...getV2CompressionResources(), 17 | ...getEnvironmentResources(), 18 | ...getNetResources(), 19 | ...getRNGResources(), 20 | ...getEncodingResources() 21 | }) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /test/resources/vaults/format-b/test-vault-5.2.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/b+ZFiS1yW1if7V89mK0dxr8YmL4LFuTHTZTnHXmeqfWLRGuYN2f8HZZf+6LTlTiUmqp0HwGRoe1H4BRYql+L6H1M8REtwqqXB6zox63SaGs+bPnWawRk0XEm3FP2qL3eMsfMTlzUOLiHhNc1e1XDG7cznquCYkXBhZyfivKbSxv9n0wlwuhRRkbgYLpN9kniLx+uixjQK4JdzNijAA5gG+8XZO0kZvJAhyomVdNBiaXqtK0kV6UttbVUal8yG8o0SUBgr/GL92u1cYjDP69f8GSKXtooNy110PhtfIoxm/1IqH9EbF61WwuyycKvoBl7KVgGdvZDmAq79Jtlv9e9QjCLFeDGnIyaAvnokYcaqG7m3UGnV7mYjJAn/JLpJE2chvwXDffTvovoGMiVYuluDcNO7hwYy8MLQllIN/RpdWvV/XY5/pf+fI9IeOkOUfqcrdk+5kD5da2LCKkT8/KyiLdmEk9fwVLen887wBcrhB3OTPNF/qnwfUGqvtW+ICAQnuZI8t9GzXOrJUsgLph6ZHUEuACKNzBe2r7XQUU+tqxrHkbglq0ztubeNKe1HU9VE9bSZfB8GAl9aFjTPCuUgHxeHp/A8Ydv3W+2280GJkXcvReTqDH+l6s0lfOuPYuytvW8m2l6Mtb4QKAYulKXin0jBUmlvtpOFEuOUisy2vKR2PIwtrIRRfTvFN1lauyDd+0rbkLmphwo5cQngZTKctpuQ7C6JSHlQEobk0yG3U0C6IJSvUnQXfVn4RqggKMpYr7f6aR7b2fKOgOKKc7DD2v2wuVJcLT+VniM9vHpvV50LPXkpQz587Gk5x9pZ9zWcS6FCP0ZSaILo9MVONxxR2gF2FkoXxS30G6UzINWJaDgvDtGmtQmk2jkpzyf5S9t9$733c88bc5079c7720ca7cee6a98fc7c7$1u2ewySt3Yck$1331fb8d93540e6fe9466f24c7869f9b957dcf208fae7cee6882b5403fb4e16f$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-b/test-vault-5.6.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/b9dnnn3eKJgVxsDIoDbo0/3j2VDf+pc4UOqOmwTczGRejknlLd4zFzv4C/dNUFz+gB5qvKEb237X/ooyIBfYRv3CjVTgeJILaeC7yzJGu1d7ctjxjZPjK2+tYK4PMFvhtBUY+ikmVipdDP3CfoAazHswv0uxyXmxt5hJa3wYzYg9jtxvmcdiefK2jBSduzDUzBLsfddo3/s2Put2+CKi+EYS3KG0pQLThZRA//3K22dP36YdTRiZXkSfPIDdUMo77HtEd5fAWQtsyrmgU2Iv7mlm0W9nEN8s28g4j39a3XRy2NtHlyS+5i1uDrFUWNwHpmyJJ00pdnAiEloDTnkl9U2XHXtvr4U9c89HCFVAt8j38sqR7cYHakCLqmue95cOR72LCYj+zT1rSMnE/5kkssQBMO577DbIsokGMqahmpt1xT5t72sUFUu9UCw6DbX71Fk8LaRicWxKN+2A7albIM2yzPukyRKpwdeIJX0/M7GZ41pkbcNgWPknoj58Rr9FqN0kKmruGJ+yDYvjuxPRUSMasPCQNRbP5Tx3N39IURZjref0B7AdeLTaQNSdvA0ZUL7l02bSUONc+3Um9/cCOatBsUkYeMH58L1CxcRkgFysSo0ePaqlOsb3muJNI6rGksY513z0oBdXvs0Oe2h2Xi5lzcJnJ4ftvwQPBiGyq42uaSzXRSvosDDzxI89XWhEt+xZTMgChZDWeT7rePjBgr/iMT6is8PIvvxaxYVPPBEYXZ7yvMkrHAactqcwksNW2Cz41+zYA5a1g+g7QttWo4imcT/o0L5uJVnQgcga9tfJH9H7rOvsz7zcMROquTPb0YuhxnHmeKnaHeCt9Uv+NZFFDjnjYR8zNIBLoj/uydfAb0L/cfKEICA9tyzRMzYez$8f068929b774c4011a3ae69bad21879e$aySv3mvptzFB$c23e136af0e54862e90def49cd70d3e754bd221a6f7764df5c9b8e9bf08ab698$10$cbc -------------------------------------------------------------------------------- /source/env/web/compression.v2.ts: -------------------------------------------------------------------------------- 1 | import { gzip, ungzip } from "pako"; 2 | import { base64ToBytes, bytesToBase64 } from "../../tools/encoding.js"; 3 | 4 | /** 5 | * Compress text using GZIP 6 | * @param text The text to compress 7 | * @returns Compressed text 8 | */ 9 | async function compress(text: string): Promise { 10 | return Promise.resolve().then(() => { 11 | const output = gzip(text, { 12 | level: 9 13 | }); 14 | return bytesToBase64(output); 15 | }); 16 | } 17 | 18 | /** 19 | * Decompress a compressed string (GZIP) 20 | * @param text The compressed text 21 | * @returns Decompressed text 22 | */ 23 | async function decompress(text: string): Promise { 24 | return Promise.resolve().then(() => { 25 | return ungzip(base64ToBytes(text), { to: "string" }); 26 | }); 27 | } 28 | 29 | export function getCompressionResources() { 30 | return { 31 | "compression/v2/compressText": compress, 32 | "compression/v2/decompressText": decompress 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /test/unit/facades/detection.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { isVaultFacade } from "../../../dist/node/index.js"; 3 | 4 | describe("facades/detection", function () { 5 | describe("isVaultFacade", function () { 6 | it("recognises facade-like objects", function () { 7 | expect( 8 | isVaultFacade({ 9 | type: "vault", 10 | id: "1", 11 | groups: [], 12 | entries: [] 13 | }) 14 | ).to.be.true; 15 | }); 16 | 17 | it("recognises non-facade-like objects", function () { 18 | expect( 19 | isVaultFacade({ 20 | type: "vault", 21 | groups: [], 22 | entries: [] 23 | }) 24 | ).to.be.false; 25 | expect(isVaultFacade({})).to.be.false; 26 | expect(isVaultFacade()).to.be.false; 27 | expect(isVaultFacade(null)).to.be.false; 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.41.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aTUeKKw+56C+MF/EcGh8bpfJVI1EA4PLujO13kuGqX0z8bYpIdaCr/plv3Z9BI34mbqex9WWg8LKwS0pzYW0uqb16xrxkQ7OELmpOnbPRCd84z+mroK+LPsJF0JmalO0FAqiEGjxt97LZz78xkmX6L3H8tx06XvRBx9GzmreNbhBpP911bdZj+l3qb5lc/3jAnjR/KmfvsEusGDlECxuvshj9vMCEJ8zo2EauNT/unf7ybouvMyiWgGfxPynbzmJQ8yLm/6p0mxuA2NbUgsmZVisduy4PK7plgg9S/5OXWibRmndKY25EAyF13SHBcYRDvH9NHz7uGQOcHGVJbIHZ4hXkJtjNg/inmWkmHpJmQz2FWswFVJa0vPEpPFMrRxBqaaukdLjsn1agKuEtgm5I++KzgViH6XuBwyM+2Lz4xyRk/t/0QcZew4ztE3PQeENO4tA9GMqNYIWdk+r5TxzTIAG3Mi07t6/qWklAYCWcZFMjau5/3FwiZonCmSUGXIdYNQWkk+67DLlzBf1sntxrOFECgMU04IrK9vsR7HUe5sU40yXgoB4Zbr+mSNy0j6Q4AtcK5e3idaqJvA9mTpTJPL/lu9SXA5ixezKeDS+YIqHCpDIK0GALpgxvYlCcJQ+SlwdEVcPDU2RVKA0Xmyq9yHcm4EsCFmakRQylqJXI6b23OEPRqXBd+1wfcpP+Fsy2RmMITFtOA9nxKT0FEm0jPTr/HGaLbnb/Ii/dxXgpG25Cgx56vK+PE8RU6Zsln+5xXdWEf81W8EjHWe7+6taL27SPvlvTLoMUN1DJNyveiLw6AFp9XNSYk+sB5nPQVfPS8b/gwyqo9gkV/v8LgxxqcocuxItpd39R1YY2/2Bv6N54D0RvnOh5jAv/iSmccRm54Bk+Z0t+Np9b5lRWWHvnFpfxGIRPFvV8L+ys7ErBYBE=$5cf32da35b72b23a760a127dec5a8f2a$9f5bc7043751$93d110f223f18fc661b6aaf330e781570f1e4b6dad255ff264d24df9c9382a66$248661 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.42.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aVa6cXrZipsMDp0fys9jKcZ/lhVY7CXsMsAv42z+HAkLWDQ9AGsXsfwymNrcO5DMAIZWlsugVCewB7LeiqfUOv6g1zQ1/6UEW5saRsbL6kvIpK3DbGUkJsEOGLLox8xjOJMwPGUNGYNw121b7D9H3909iNUZ0c9o18Sjrp0tuod3wgRSledMb4/kcduCvHJvBy4Ygh9Q99+a7yNFvFQ0xeOIoRreh4dHzXCsNjIHcfV8mYesHCcQ9LSxvgNXJCi0xB1Ves2RDq0Kpie3AbZA0BiOx9UBbWZMRHM6Gt92y93br2Z1i7RriyWp/eZHLvAnd4+tGy/XJpb+ozLlBv+RKK2pRr5iFVh7yIKzDfeEYppsj8czZc51xWTKvDFntPZBBe+FtCMGHngkdFqBd7tWn8Zdg+HpP20YeSABXGQ1nMONUu6fMMO6m5EA1HLepilEbuKvWDtA7T0WF6YmOU8TV2oDpQbpknvnUNm9PyZp3zK5ZEIvDj8XEXReHdY70J5qT95LldxMezvqhcPiQpSiA+34f/9603grLgIppz+5MlrcaPAjyoSHWvn7789zV6Q/LKIyNOFPuGUpp0CNXuumfd2/QYu9RlhVnMDr9vUS9sAKkWKac7/aOIZnktOux0mNo0Dcb5QNahALeuHhkRrO7L9vUxVnunLLDRpfT8pPO23D6WkV3KNIDW4fFLPQOrW43CatlAW+TmOVpUoHQjn296m9Wt0O2PixLr6YPqoClDKYvZTVFX+wSkzAuzl4cRU04OETuukSB1Zeo41BZaprTLaKGNijodIGPhzuTgfvQu2DX0Lk74B5pgLzm8etSMw1j8ZOuVK4ShektuWlwXO+j8TxN9FWW7XlgbGLHccI3d4VYhoenZECPFAWkr9uafi3u0K+GsMfqQR33qFf3lnva+VOpYbvExARU3hsRCUsY7a4=$841ab90a7d5946322fc290639f216614$c44f4d02c429$c7ddecf16de7dfb26f46e6e647bd7a44630868549502f53ede67361c57553757$214216 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.45.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aLLfr4pAFrx2UKOXQiwjNI68FDVWzYjTN6CbZS+Epm7RRvubWfUVkpl43NRcCPw8folr7DqdOAi2LLW8HNM44auG1FXVJNxK3yxaNqSy/P344huXN8HP5ExXrNGbEQdpEHl7euedJz61a0/4zfHv7OvfZG2xwKXQM1rfcvgtxUNJz3mDmHcKaWZk/7VSTLx2YuUfuBWhZuKEhKgIXETgpT0vfpEUAQC680niqKQjKKnNnEGnAo1DVUF0BGrW5+X5dHHd9CxyvNpBzHsxfsGBXVDy+mAZpcVNj7Dy/Oi2UYfm5dGDVmzLI9FvwEAMAkwHTtpxGUyGS2ybp4zPxGGqwUUYT7Q7A/+435RvU7aSRUN3rQIdf5cC+JKnvESRtehpff2QeWpZshn92GT+qhvUwL1l3XHcfDlRgoCZhzr4oyX5y/l6vdQpiCCLCSmZX8l8Y6636bIJnQ0hGcb5gY9YQirQj1XPXVaSC0Man9XO62EqprTsop7uYmY4wE4aiQLb1aUzVjC7+WbRzWxrdQvN61f3/Y0RDegZ6Qn7kG7D9+QG2b2/788nsS0VOvm2H45yKaSJ6s4lrY/YZU4SO6rqA0ydBEbsst2vDWdGlUGUi1NUnf5zEnid1oWbXAHNO7Vy/GF+OFQvN6tbU7DXSFrePwR8hyHW81/ywi93PEISe0bGs0h1ZF2nY+Os60I4QhmJ/0toUYEUrSEelBC56Nque94/2DirwtF+0ddYuvZUmxLHGH+SAHpFm2DH9ZfVVz6RN+GEdwh6EDzybAfsBCM/br+Itzk4SkddnVg0X5cB33qI9FlOUNED/k9XXW4fT7MCEJp6sInxS5hQUOh6Ere/ZD69bUqHg6MDK9OookIScJfa07swXi+UBLCBAAfWPHFQSyxKvd+kxPgOvAHC7wBjfFZ/8UcWIrGhpGO8T1FPNrEI=$5e4d01c4fcd5689dc62070b6323c6dca$08a420c7bb37$dfbe944ac22f1fe7e9046e753a60398a4ce1f44615042bafd9735b95e5eda800$201744 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.49.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/atpkOkN9hSOEHB1lg4BAM9qX5+yaLiZ1KiQx4K4ZpUhs7wN6wmWx4aaklX6EgsWI5G1O1dfj0fXr0ep8wqrv8V/jPLLOuU3kzSpkshmic3t4vVXvlzW40ZPoUPWUn5KB0GCe5LbSeROVHPI9exQHvWuEwNW53Wd5A8iwi74CQnzYwO5dIh2jCQ4OlY1tG4Qcmm/T06fU/+K7s1NWaCUArngqwxz3MxIHDV7kRga4m/p7bXxWD3e69nmLgFZhr3ho8LQGQsrH8I2mpp4vPvFzwzjviDIVGY9moynA+6XZr0rQn+2WcyRke1c67RAhWqSCpDYqPMV9K5PdzV5Im0dbG0nIWbdsBIqX3rAxm1PStnlb51Kmb5S3ZacMV6MwSHJQeB/oq55DGjDh/8RSVqoV4rUVAa6/LHWE9hSxiUMtUsE4dFsJFvQyLNvpNCf3KJeVqQC2W6yRtt5dAWT/bW2PVqIQSdqPFyxIIjyu6z/68iZS6Jt9AdEsi2rVRrxykzeygCU6aTQrt0fpH3BvwPNxa4uWF/0S0nj9PhGqykk36P86iqIYkbMwarjVm9TYqDvIm/FpnlE8tqluy3NbAm9wIxdzbZpJrbKtY1TXjXSGD0DykwXAAtyzzF3GiCKr1SNHSTJyFhNTwchYr9AzIjMJ1yWX3SyHT545ZAEaJe5HCMtbI/Ser5vWJIB0cuhXMkj5/6yTWnRHdnIhi9EbahnVwwr5o19j1xwZUqfPDabrA9trS8yJroDCTz+vlAJ1twT78WWVhpPFgM6U0VEkLINPk4eUO8aqjGjUXvAwRqFv7oKXBIpNce52jTfeTeNHQ7tCFDivPkj+gEzL+oZVCwg476BcGqpTurjJ4I4EEYnL7DeIS7ZfKds1h3wQEkwon8iy4+nVcdUtMG61Ifw8nmelaDJk49lV03MooCC2L1rk7QkA=$0580273808b74832a1115abd91dc5237$e662c9223221$ef0b04357330f9f6fd6eaf69fca5762bdf71fd0102bd0e939e10c108299666f2$224244 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-2.0.4.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aePm5Xme2Ey3ne8QozVK7bIhhNZX/RUitJGUK3zJqTIpFp7Z0tYr1pg4UnblSpU05lYBZo3IppFGg2HhRiMvagOFTFYRHXzrEskC7pD1Ey1Jl7cP2fAHh/k0BiSaP26XTIMoPcos7OudqanYBWX3rDkeLTGLAh8OL6xdP7vRhmALAASYMAkrwnkTUQGY+Dv+sah6fEp70QuhDcydmS627g0nxI0AgeoED7FTDlI4F7rv7DzCaH5A0nQkU0N2R8xDFrnZBC2A2CqjgEem2orUjFYGBQKRdsp6mCzKshjITzjq64JM+LPrAEnWhaUqwBu8nr/f7F8xNW98FqqLT7zXoQR+OyAIerxhPBzNZmJHbDgNFOjLCzldBCqRGrYP3zyijjJ5uhjchtiXx6wGbCvvjboOnrhW8I0VZCvzSBRlEab7BAMCSzO3MFsyKOUXbON1UqNHLeYfioQ82CkNHrOIPECbcqFfSYFcXs1nEs3c7RmMr8d15NyuoppTstl6gZpFV3mq6DNjZsxPKDMGg4uhzkL4x6a/UWgBTgSjIwhwxxffL24GR/WLkRxtm7RH4l60RkToCRiLkbfeAo3dSNUWJ0JX+tR3ECetFTxuyeGx+s0yqB14RxztBRTa9SeQtUP0W4KO8om+x78JXLXPM+x9Z+J3xbLSmTJdc/DulBr4+ByEd+rIkfEfmYjTR2epCLeAR7Xh4feOFO+7aGdqRkDyQKucWsv3ihk5h509/nxbQ7iXJB3Tl8qRHko1cKqHlxUTQ01X1We+Qey4fQYQKzotsZHgkKnhhjFkSAoRbwKbcTXaAd9N5z/GqljbEVj/Cp/DkLrstKAXaoJLBSxsq1oY7b2EMH0Zfp7SW6lBG2CQZkXA1IwjRcV9Bub/jH+wtOy1RLpnk/eaICCDPB3wOm/8u6XbDFZJIO+6O44gr0qVhcbw=$3d5e3ec8f08ae2854d5caa55dfdd398b$H8U5rBSx0vq9$4f3742e809e654f6ef9c71bb475ca6ed6a7a68c545363c39d4e9d5ac07af9dfd$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-2.12.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a7LPgKkjPo4YBLzo45TrRdAigy2AOFhfWSUEh1k5RsWt5RP0x83dIb/ZNqzdJYWzHUv0A+aq9U1q3t6AGvoxscu8RzHVhFQbKTnuB47Xw1tJh2uM+YS62qa+NnrtJZN89RqjDipRVZP4CM74by5+WubDOkHEb6mBVKaRVcAJVhIG4l4cjan+KtEDXaCnN+szdCuOVw5GAMgEeBGmkB8HHnk9ooPd3NwW9AQpPUVE9Z/V8nI323DUBF+LHb+E7RfWcs5jJVi12RnUNW62sgHwH2Cy1pjzF1WYnCMghIB7Pg0OWOdc8+Bs+mVq8+WjcMMpZRN0xy8OzCY8TbQkFvNfuvQ43epx1QUfCv5BjVOtkIgq5pVgpxbJik1TIWWj6xB3h28SpuIHvR7CF8SLGxPlp/rHdbYd36b2Mjke/njLg9FjWbCIrI0/MlgIDR48zgIn2ll5gCRH1UXrLmbajprdI4NM1uXoRSMUMyX0W8AtHLPdKzdXeVP+NqQdMs6Jnvwf7QOaF3V+MK4AtkxLXLPb0Oj42JXxQWGnSY8dM0EC4R5HnvPRQbUx0Im8UrPzjTvLtR2OilxdLJemGtSZSXXQC1Xfzat9KZK6tZl9EnE4ND0qOY5715i0ESs89Mq3X1Ct4bouhTfqbX32tQBmi5W7bZaP2vme+YHuDIHBDlJ0lWnxWwGgOf7stIS+SAXUQysfIhP8CRmuQi7KW9Yu5T6MPBZgtHfu6wDxTEabQ4DOII4/DAXp24bKsyKAvZbYBG67u1QFlXnQocPmtGZBLi2b/G/wJYEEMkRmbH3eQIBY3rI6XK2sgxLX8MTqDjFA7zXRfuGNKl23Ihgs1oBxD2cS+CP4oAUE2J2E9ObSQcbIgV5FvSIysI+VPCZTXMNHdlBJ4iqeNgMp7t/KmCxjO5uzO5hglkUNRaunFuCx97LgysCY=$55bd1e60eb0b13e55a9eef557e680d5d$QIidrCW9ml0R$2dc62c9e03da6efb0dab3cd3439f6d37aa3ca38f6cac75f20bbd8e05fcae7d5c$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-2.16.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aADRLaPvIwYrBfplpTbEuYEtLUAGeD/3JstSRJqg06SKs2nGcgbfb5P1D79SuUAe+Ncc1LChKq+UcODFP6cqukda4R1UhuX39kWmHG6uqX/xkAlylEmqrt4B0EY1kYbJ8PbZaOD1lkNeaO/f0obUgLZWrRxcmKFmRZBHm0hZ3qMSTnCAgAA8SgHrQhSBHyy4Tve+VNRpfWEt2Y9yNXo4VOnCg7QdcZdkl6Wy7+b/WmUSKEVfc2xuHxMe/VT2CCm7+/54D9Ee/uSARvjUj1BbYw/ETmfcY7LtzCzZSpQiSCmun144pg/osKWh03WMYWk+N8c/Ayk1HK00ejsnTbWKbXBSueX9IjS7I4UzsWTK7k6o1dsLrFFw6C2Dt665cGJpMO7S9GNSuu4Dj84c1TQXNW3BOMhEaDub+aQgKWCRC0aZnhN6djdbPcjsmTwWIoSX88YkpL5D3ExHK7mXhKZOdzKgAUOfLmyzGXUym0CduW2vwRjxVp8XDXqFC43frbqJVKWJL7Bz9335wPR4xI9QPsgaEGekWHtoAgO9i+P+fzzHTByz9EU5fqtzvBoW27zqaMF1CNi5K8BVNypMDOgMCUVBAN0VxwDAslZatlGImWpFuH7KUA6IVZHGcBUR8X3OPkWJrlrlsN9Fb2iplLqd6IvEsCS//ckuZimwY69HCljgQcApD8mVE7tqroy9OJX0nuxHZP/D4JTZObjjQgaBPOmMgdwuzp7izkjYtyqcWkGcOQ4+5PdiiN7Z5ZWUgi77oGhy1GdJiVA1wfJLhBIg3muQXErQzbe5xTn4uVQyAatdsvrYwsFMj+BQax665f+eARe7Vxq8VchNHgUU5LqutpgzOn09BluTS7qVUFxsaStZJkNvgrpa7Q2jOjUhRbGhGBjN/FVzikhKRsml7DEqKYQmgFO0Od+RRe4BNLClSAyg=$22464129e2448a3f7792a25f9b318ea7$pQCPMd1pCB8W$97fad969bf3fd707ea1e51bc481340c953a847874cfa3991675f645d62ebbda3$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.36.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aBWcSVYSm6Y6dwEx/CYtn7retEVUj0iVDAq+0moxCBjDazYpPCpJzuPrVL17MKmbEY5LJiR7bF4x7Zz+mWe9TMUP2qUTKAGwXyKrCSr6EKU0t7m84aeNC/TwyLFbr3zw//6G/+TrAFF5nPLnQPF3B+hwgq+nPU1nnPtYHTy0hkOmNYWFnkFyZEQU9c8j0X8tNkuzhR/poSAObIuiD1zK3F2Xf+h0L/z1wpskRhXXEsF7vpfBmF4eqElXeyhIQ2nmDd3fOuPU1qJwSItsmSbDzExl350H4pYvkYMGWy2HnuDieGu/WLUNKRDeJuw04oymfaNHGxeLY5Tb+HB9SaS6Q8VKaLZkGeRd+rX6SP5fyECNfDM89lVxH8XzWtYOGtzPIVUvSRJxd213yrhCuXTJEHJS9u6ihzzpUCVhaRD7pFvLDsqECNGMH5iJv05Nom80C2HgbQ8fKJfebnQxBOm3NRWRF7rRZXsKvDtZbcN4YPVOKf6VnuAlPzgFmVRqRW/0Mz2cuEnfUMy+v91fIRwk+DmVMEiPtSniJ1ACXiS+v8jHzIi0QlHtYNE1rwarr4AHRhtSLBcjOsRAAirXhipteomX4LVfd2KFNsvb3yAxbDmS2bAeZp2+26wx+sFg0eaGY1+Mdu73aa8syDqDX8ALOuKcx3mMjA4i8Y8mH2Ddo5EvPJkKyV/S7mrpsCaEcocjV0KV3bSNkbuQQjTXNH7lxPVTEW6BZVWvTsG8lXNslMIIfn40HRKu6qMJbMYGD/MBYR1X8LtE6pt9jxAZKbUMvKxlPKEbnel+K/B0prucUYwGIUf2hZ5cX254ZGfjivsy4zAdFeU1TU2A9I3XjuSoR5m6JOxXPyEtrnosJwZbovU01OHRKT+mtGQaPVLh2NYVyFxvUunktt1lR7kHtOdIM4JSG7NO5cOap2i1iIjYbeNwcN/Ve1PJ2iFhwAW34i6nV$16829120760e89860e9b5c7845b7e5ee$fd2ac940c1f0$f69820413bb9b9f9d969437ad13dc1f50f8301bcbd5338564c4f200bdbc16125$247846 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.38.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/arX6rnpKIUfpci5r/4pFa1n7FS9Ui6MYFq+oRcPMX3YNdNBYnj5aCcfRgh1cuOb8iMGXEa8rbEtqhC3pv/dA+O9Ev6r98QVkYA5CN496T5R1hMFzIWxBvNOQI/hqFyFo7XyIHTDJe4vH62rOBpBJ9JZkU+Gd0MFBVlBJqFUgrDtTKPqNa67HtRVyCvjfS8U7cZeW8ed1bc+WDKQXtdzdX74i31IDWfaou+iJeEHMfuMavNIkn4rImSotHzIfKuPruVfaYzwU2qkr4wt30GnU3uyKY/K0SWDjYQN15kGxXEnbG6ksfiOV7IS+kSDrk1bG6KSSubvBk6MZAT3F75nZznKWzEq6snwPdxw8jm4toHmpAqjx/MD3rVIC9u4l8SwCUzKxnRR2T0clXpkmeZOtdiCMPL3XLaA3B0hjS/wkauHlUVzKaXvcMMTv9a2Pt5GQ8MLXfHAxwQrP33yrasAvmrVCRAKhKaKvdZLSShDswnKeJ3FRicksAnQAQ+ueVl3H/C/HwR1x94Wz9LQPz2O3q1VqRUdn7vrqifMB4CLbr1qwzTCNA/9ISEnPPwNq0Bo2tkgKgRZ2Xt0kRNxU0UEF4SAQZwEUXfy2Xz+Sp2qIN9MPc2IWQbdMlCy3LKVTTUYag3cQF42FIlfIjKKb1ZyyNabfkWS5Tn70qyGOPhfRSCr+VD2d/ExuVBaLBC4BhHZVpGy+GnckDJg4oqgz2VvfJzgW5OGJAnPKyyej/vxvGy0rrkDSzMVAu5gL4Zawx3XDZFnWk+VfYCTqPsSswEPimLjLHXBYzneiAOwVCz5GoS/XO3ymabXx2cMIoySJbePe4KFTd+kG6MrhJzthgKcdp5fJtEaHXHzL+DQhfbgYhY3Cu0PFOAJF62GfATV1LSwKKjbS4FzmhR+00ac/5An6K7EAWRsnPL5pCxXfWjS+ikXlIEdclqKoBvsWYqG1+NPwV$0a349f153328b9c050ab243a6ad7cedf$a4395a8c5ac8$6d2f1221518a330278879933f06331308d9af3bb6f563562cdd9415bd2046b8f$242699 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.39.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a/QGOUh8yswp3XfUSfK0iIOBjRHVlvg2OqWg8QtnNOMfrOmTwND0KncdyDO0PJum2wx5xDZN/PCDgzdYFAsMXQNpW+WwjL9W0/frWQcwZvEasDe3WTq7LnraB72TbGVCKbqRquhWHrSzLPKEgUI2xnSmZw8XU2MoSpN1CmEbzWiDZChKbIBMdPCxOwVghlZmEyWZ4pNYfQLOleUF/JWh0vOQRUEMBZOUC1Ijq6yg2tcRbdIecNc8SPIxBPSh0zqSOJ/xTbphz7obRM6M24KdRfgeyYej4jeCOzVzDBOwHPjvihlYrGRTWZH4KgVVH3bTp0TqAWQFvLvGemnsvZSfWDX9kEXrx1yYgHgLrZAzrZcPgOnKO6Fvs34YI2og/iW866ch1wgROlEZG78Vw7rxI3b0aaOMf2VI4g3X5w5TRLGzzqc7ewrP6tM8YG0AN0SF8jODllGU6CVE9+IQ0TYadEa+qtetNCC0rvib8YHx1GRoXseGy14TI8b55lryzFDmFRVRDOIPpov+ObAfRMKzcEYW6AuMYUaSQwuA5V5vn/wBLGGYP5O9hYl3iTONuw19CDFPbw8TYPE8pPQp13IH4hVnHPQSxL3YQsGROpvcCjjsEMZSmEeS74H5vfY58LBfMpZTXG9oPW87g09MzINYnMMPFs8QWD6JeZEuNWkEeR1Al2WBrPYSH5V4/MPuTKfxhR01fgoi9hLtaC9kOkAd2R6zQNrRN7z36k7sRpvEyq9ri/ooHCEqE6xHeWOAQzp/AIvTWObHLiWl0eEDgStkjmiXdyIVCZCBwrgdSRUtbSWCe1miy8eVBykmatIvj9UO08LbCvVJqSrH6CWsNyMbNLKuE8dx6Dqhq+orXd0rGjrQvhF7dmQ50d7JrlCmaiT0cZnqHtVtqYdkB9+rZKk4pqJUgYtWCccR8+nVB8nw00paUOoPDIsQS1T4SMICcRPY+$36dee29f2e679e0d42215862baee34d2$912f20a0f8dd$a1341b1a7155228812fbb0f0b1ef2e7b4bed4acd9ceb34f076c9a6f127a281cf$234048 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-1.1.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aBr6ZA9DC94BQLQhvTRvcm020BJ/VvYJ9PsBCNduiLD+FI+UrAhV0wmYZ412t6pY3R9WGTKUVD8Fkln3W9tHxSOEZB1fjKnlgbJoCpamKKgyvpFX/fgMMeRc5+oV9C86hU9ZPFfdGfp2nAVP1r9Fz2y6HWhF+MH+IxHud5MnWYcYZyb1PFxf7+kGJoDnWQvlzkTxL32/7uypnlRFfuWdRS2c3OjTNh82ak2voVPDcqK98xY8KsRseh5dSBiB1dW+ZjAORW9J3sJWmjLTmvHPpyYLGSYuxoEac3Z+uceveSbhIBx/esrMjiYbaT45yFXYyNrVXGxjOKKNrBHSHHof2sXCuzmwBuvqC1LeJ1qAvXb+rs1/aliLM1n1og2DOvZECc+VRoompjNQBPByEZYeD1AHzhYmGhoiXmqgEW7Kbp/9mXPxuoV3AHlaeUGgBAmIhsFqUZ6S7RFhuePFu3TfpAdoA1r1urs8S5FmcP2neQdGcUoLS3427A8t2arMm0wCZTQ41n+lUOCoBdBJ5mf6PwNH9FmXDCvoi1TplIDHZ4op3EwToecHjTglgZV0u6lUhWKLp0mUhvrpS+eBv+ZoBDAro7Z1UuNeuHbRGQ1SYTjjKfDXxDG7dlpFleDNu/ptCAJTfaCPgcffNmEW5GIsQLxef+EkE36/Ze20jCAwP48zt4yrjXF2ZVJ7OLvV3PJzmI1SXvFokCzX2K8Pmork0mZi43mexkQWNtBUPPHfgND1+vnqpn94I9r0YeXxcMj3kX13MLfP49Eky2fJQ9fsfea+AX9Vdz4dwhVn8NbqUlUiAx1YZyoj6cNS6yKTRgs+aSkAKOQm0A5AIZXMGJaodZsiWgToztX5LCbRAzR+p2sBhgcSRGrmugiI2CCrsZ6mzFTlLhmY/s61azWHROpQC6mK/zkAPrXx7AYFulc0YidMYUe66q68GnJM0kgJSuKzh$06bb7e4ce71eb3d2339c8c8ecd77100f$7508ecc7a3d5$3eea14bd457dd1298b8fd21f72d1e3731c26d12700b5689e301d01d4d6fe2d72$210723 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-1.3.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/ah7Fh4mAJ3IkWxcZOtRudwnG70vR2En5Rj8R6Z0LKuF7eYfPsTiO/TZud/03g9BjvMkEZ8hNOGtPb+O6k0X/V7UrQLwstL7pzHxVznzURjDqkMypx5NqekrZkEo0EpykhjQycSQOSVCXspnK4AgvUiuhmz7ljeZPlKlpPGoZTMELuodwBexN9GDUs58Empb1/m8zhxfafvfsYGmX+PhfBJYHuvnRHJudXsHB9poXp4vN1tcWdnD/uqnPCe/U84SQfzYwbjSeybIvcxRaaU22Zz/y3KgTTtY4yRvtCxmEJxpY4T2Be2sPPKKRQ0t+xrCfRgiJzyOgQoJbbisCelaeAtOIBUXW4pR5JFBJJT9kM+UTs8BCC+C4roFTaUhZnpBMmYDbrLEy/lBx5E3ZUFBhRj1bdoLT7CleYZ4t4fpjKrPWr7tMEVrG7lt5OwmmEp6VSvJ5S0g2kAgni5Sw3EkRllhH7Omm3hKmCae5eLQxWyPIahKm+86ldGFj2gkSApDjcXughXQ7ostYBjOZ4J+0vnAVG/K03RLNnHMcJoOSV8Cbh6rN0STE83rZUVliPCqNksd0prF7mJPbgQRbMXCaL/GI7WN7ozzlBY2uGZlY3i34iCeN9/NjcpSkJaDRT50QNxRNf7BAo7VdknjYlT4FgwMtNs0dqSQPfHmqoKMbGtnTPSISN+NvTeaNr69CEni1fFN+fM4gl7stGXODdyFZapR4sbL4UdsrCE7ePQBPuXw8oBZ92DXG8feNIJMVgq1qRkfHovIRV0NYRPojb0hpb3/+F+raab53OYv74XtA+Z6vdAN6NjawudiWhX5XTjP1JSRu9a7Wyvk5AyuUCuuXGchyG1xv/S8qmLEFE/kzfPIdAsp3vf295j7w1E1+V/2QvSGsoUeJfopGc4OE2FeJ2xL1PZlTu+ao8ogytkpwcLV2NA9vErO0jX9LIrxSjjfgB$f9e130ed5dc9e7fef9355888790a3bb8$4ab1a44e9045$67441ba1b868a35805c4d7d051666c2054d2aa1c51310802ebe79592a907e6b7$245199 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.35.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/ag+y6F2zGHAGM0ewEi2AmP5H5HyODwgNW69yNJEkhzwUTi3917wdMBUguAqnctsmCr5AjWz6PTGCIhGIWJp78U/UDGz0twCUf7mJoZ4Px7uHe45kjbr1b+9QMB7wzjFXn5bq5GYMkRMOKBD/9ICkWW/qid8g1kY70tXkfUUpZhuJMeWwnDF294QrPyh1/UGwM7LP1RdduCqCWWWn2MoEO8rznwX94Sc2uqBAsThW5mJCOQfMKU90Fc319oaCOQENv7uP4GvLCL++5QwQu1HkPWf89Zk/T6RbNvPafBT2/QJtjEJ/zl1HemiGpxaaqAgNlP8Lsfo83V5keYNsZbzQDfHztf4zfCgSV+AyRwUV4WUgymurPgNJ1DKFS1maTOifMBZgiGii0aUfS8LXfing5AGfqGVgRndDwygbnim8263aCsW1chCSiO1BjmSCr9zg552YW67BbUX9MnLQulE4w1MO6wEEWwrxfNKbPYfx8Dk0jTyJq4QSdrd/ls2BFyS1oCYO87ttU3DMeZmf/6gr49MVES0/uSzQC3cWq6Q5X0cSsIOa51uRoxUFTkXcFXoxYqUuxboZsh10WaIkCNgI4/8THRyCwjaksathFrut3ICNGIUmGhwsfBCNauxGMB3biktfURSrWe82Ovb3I9ZIgcLtmmR32VE5ektrv1jYbDlLnF2tnXoeruXby96MYLlvYDuN+CRAFZnQAK7oP+FmIrQ5akSqH9+WImY9FWbCYehb/0/ffIC15oSwBtDYkD/ZHxF484Shwqy7Z2tymW0TuFTvo2Oe4crj7Jv6K+eptmx725DSKpJ2d7cfEyIqOKhL0dnkVangqN/pXY3RGAGcuV/PMusp+Un7Hx6wvawUmdGpMzd7FPTrilhdX1bvQBIie1z9dJQnn1sjM8tSK80Wa9uLEr7358D+CiOtjVvyXelGY+Y5op16HMVCOfOoJXOOPJ6IJMCdJAUd8Lh7I57H+pA==$ae44c655245c363cc9aa87e355a9524e$7d043dc9135e$9f640a5590329e303c7fcaf7489ff22d5326e31649e589d3433bcee51f72ed81$217759 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.37.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aKxUHTPS1ORRchvcpUICstlIH2qJJ+Ryq8ATAVJ5SFM2OQPiRaP7isaN5aywoMT10p5mHOKJAw40QKN+ykeE0sedbC1dB7iGLv3xRjoEvoAhuHvVqNE463WE2d86SUPBkwHhOh6wSEpio+1kN+TWWi4VReYngN7wmv2EoU39M0gAm/y6CnuHPeMBcv+2e4T04PrNVIH6EXaYv/3urwO00YOu3Rp5txseHf0xMXAf63TajtKEv/1BE7PUAut27Ho1FU84/Hznj+6UdplIDmv7bZ3Li2QEycn6vk0AR7lThHb1K3kDQLVD70/Sq9c9UmJmYZzMrI2FX7aWyWGnBjQr/R+zGsCn4+LBRONQmTH9Q+kcnjl66OSZA7FEE7APsRZRZbJt7QyJT7y4Gsy39J3GXDhsa5vRxdPvALxVYLdee+3GHbmhawgKWKavgxU7PPEt2KDR0ibCQZbjgCQuZcy/dxsRpM+rjo0rhOY39ZESgCQw8116kaOkFSaqEG/MffK3ehnd7gruxcX9yauWaruANQF1/FF1JbIP/ob9vSDDvMbeSeuuxlgpjWx7ACo/fnJAuWXdtmTbDCoC1eFKBET6xvwVpczEHX4Iul9/Nlfph/QREkgE5rNKoOdiDzPzjnN9f8DsnYxp+2u1UnXrfxHO8oEt3eOOByVAjhvMGO16mtiZdtmZfmFn/E5E/5lAsHqbe/qF63PKjmiWZr3o/WvRZneFDcnB/QbxL/Ou+W8GwEcfPe9hz0EZkmKM+23yPaZFdXJzmlmJ1BGzLBSQJqV71V9frtJ2I5NouSWV6ahyNivLVx4Pp3Ju2XoWKac+j+BmDkMGPk7QRplez4JyneAbK0tp2Hqz1kRErrbT7mOy/Is5pR05WzM3XKTATk3vZOnmNk80gd8sWaxaDN8UB9lBIxq1FvI1ErF+/932DJS55QmvMcckg7dP3DRhup4VUG7VE4tiLwYfn8dgS7Aw71KTlRw==$df93cac15a79fef2bcc53cbb6ab66e9f$2e01e1809961$e2760066c394f82c473eb126db066a906cfe2de9021d7c24698e6fc7d06ff9ae$221338 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.37.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aNalitHmU/RBYmLfM1AGD74ku2sv487CWH+br7WU9e7wMsWeY2Kq1d2edMAkzG3AvD5J8dE0gURo2u0/+I/+cAZUUxCeJzZy7beFDzzR8sFhymL8icjdxqyzA8uIK5qFqraeEhHFUFd1Q5MqB103opZHx6IVOb2vZQYgXmy/EhPiP+VJn47jzunehJnuTGuAlZfNLhO0kAXsephBzNv6ZHsGmCzMbCnUuXIM0P6RTOW3VyR/8eCCY4p7BptXVq23w7GFJ1SGMZ0337uz3QLurgO8TV2U0NNDvrgMW3GPYUAtkm7zelZ4SiGcSsXai1B/ZwGBsJt0TNxS1UKuQrQS0V1bCRzSFeJUfLXiKUE16xt+sMo6MPNHRXG0yQEvgIxMKBMl6HumE9iOfIoVtimmRkIIUoJbDwhxQYM7Rtt9be0jKY22kOum6YFO2wAxFUYSnSMeg7RLJxCOIbp414TQHJ69fqWSfS0jfwV262E5g+GdBXmVPG5SbYX8NJ8+0L4ndbBxb+AbO5Klg4XcK3xXI1FjAUMoy9INGGtrILkHrw9zfQ0aWg6H8duWof+Crge9o8lqD9CWcQXQLl/sO009yaSCdafCHNsVKtxHfoTWRT7KAXoZFxlxyHM0KoMbZpvFcORHbZI4WMHb73vPOY3eWUE1mnME3UJKKDEfF6x9S+ZcH+Rv+OgGYM0UKYxWVFfCG1/Az163srCeC6wK5F+jiHSlCOvCHSlYmsh4BSEtk8Za52PI226X2JyK7p/31mWwx/SqKVuR1OYte1uGbLu9GH7xLd0fPsNf3QBGYtAPJX5TECKLHUOjcd32z2e0+m3dYTYOEM/RhyOnC0RxOSR66Gbg160+R5K2I15Ve5JenSB6BbF67CFOrDjNnOWfg+dvEp/r5opO3fEsbZrBDGPGeWPfa5dHwVPAEOlMVremKQCehK6BXeMFX9xnxA1mL9V+ehOH00lNRAEwbOVVaeGtZSg==$8eba1df263b7f1975d292c582e7bf960$de2283ce4484$f3da74f5c5412862c3af2271bc1a2137a654a2772b84828d9a1b690aafa39983$235229 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.38.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a0AxtmZBFBauqiaNfU+7Z5bSbEJUe7CeRN0The1YVmrA6YJr3CNRsvi3qbi7dlbGchovY2Bc9cMiuHhDxY/TXidKo3N4HtmvMbYB7VFkGMz6Uamfhm08jYcTO5ch1KgOr2unccnLdKYRFPF0pCzBVQVJotJOGxhcXx7ygDsT9JzV3xkXGUFJbs4otb5x9ztV8vEWppNpLq8tavdkFkT+D3I1Bkyyjdjlatd0u1bh2W7dXznonw5aU6g7RVHjqJgnyZUJG+Uke6XFTkt1zZXU4kyoyGBpJ9sme0aP3RuhlAqbaGehnRsf2jj8ghgw0SNW0LqMimKUXEmdOiQuEkdb78g1HspwUC3CgOMP7d6F/yCwZ8OmB8BYTZ98EybQXvueUVBaA0/pxva27hiWfCnimsGqBaiJx6TxKHZA5EXDAH5dpBj2AnPVv9gnhZ+TDrOROaPAk56hxcckF39g62B5JILboWkP/p7FupAK/9jJ9R+qLSbO/DJfTt3C3/uABu9kiX9hnhhQu3OETn6cqoS0IzLX7XBoatzHQA1KZhjJBqrnddcl7wekwZfgrV0clznzakRENa0x07MSvbSxS8jBlUX+wW4ErEPAhX29eGcOi9+Ila4p3n01Gm0JkZCX3xnKRFPyTLCXbAcGQwOLuhN8R3YoCdC1QOEeKyK9I2WIH1Gy3Xecc4MjgXPMwNIth2pxjg6UZuGygZ9D6MGahu3XDqHgLBR33hwTFH6aA9u1KCVRV1j6nEnGr3RvQkQsjsSNogzOy7YdAXTdPmOKCJ7vpsfv8LQ1ozx6rzmwqu8Ipu0nShbYETO4q5lPDXNe/uh7XBcU7AyQSmeQIygh44+NjwV5ogyBM3LWvRmXOB1xbcjoC5qABYM2Fi+Aoysyufr/WXCjEw2zTyUALqrufLsGYKTHQ+vjImO/Uxy5PZI0jH7vWHoAqa+889pLA2MAkonCnBVzBHyrcNKEpmPM55wbrhQ==$796a2c6e0eba703aff182f0d85c9c22a$620529253691$d50f98b4b9a9eed105179c3d2130c5027288034b1a6e5bc4096ad921f9ccaa59$224032 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.40.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aA13JAZPDxKEexlmWeO2sQ0ENXKEbTteTmn7kulnbjF6xheK4z8Tefj5wYDu7wWp4Yq2T3rimHz/Q6V1d4nt7jHUNZMa3MR2w0qtTKNqWzVJbKWNToU7LkFfO5IbCvlLzNVILRgIvwQUTFfU2BtwiwSGKhU/fu5KzIktqbvXKhFePmn3rC4V+t1og9rdg8NgkROn5ggnaLNpR0clhaC9+7TJeYifBGKO2ryXEqEqauu1Ouc6KMw4UFuLVnUBrOGX1cz+A3FHEY1t5uBIBctejTV5tzlhxDfy5FFXK+fYg9BZKIK8XxY1ca3Izy3KyLB1ceVSh1ygz4hwdFIJJ4OIVSzbNVux1/iA2oUbRj1PLROlnXqiptHajExxTY8KjupIbp+GfrjC3jRT6pT1rp60xGYfTNTrnD6kLbRGeoVD46QNxbXT4JKJNMAjyMuuFunrq90ZcJ1G8DGRz4AHC7eOuSjl1hpWUyLryjjBl7tmYnhJY0MvGziePhQ9cENq97i+ooyaRmqXne/Cu7/Q50SYpgc4WK6pj8sRgzP3llQKi63MD79ff1lmsnzfJTj57w+qix1mk8YfAuVHKDe0/bezXoJGdJMUUOQ3YVdMyxQ6fdRZG2kJakh9uZFboIX8feVMQA8IQyEuATaMBw5JYpFBSzftmFJvtZZIT7DREXvRNJ5G4a8xOQiTmH6JB2n0wHgZuWyzOP5QcIb3YGO/iZuHa8Vg7J5Z/L4Ezr+xwb93En5d53rgTas14L3vaIxlPHgoFyxJC1ONaKKrbsNEP9jfboR4xtcffOPcOvfCy7iB3ZGhu6lhpO7gU6YZA7VVVaR8ND2xXk5pFEecdHgE0GQOsc1u9o8iQ5TgjX352E4Ghd9L2EC8Z2VLtmYM/UcDzZ3akpmD/1CvbiNQ7XNhsvDUIkSYpK7Qjx27U96oR1UdZwvdfS4zoVStQiUUkKnTJm8lplkK3B+ZztRppJZcewmK1Yw==$310d06d32d377af6354b163da699a624$937418ead706$e5d767625f54a40a69e6be355960b53ca2ce05baea190c582448f91fb41e2a05$229751 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-0.48.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aSdEqnbuJY39UDbK5BxmrRgb2rqb2tQP0Oby7neBBqUzi12MOQRuAUBPVUwYafMU687qbmJg+th0JG6BmETplIHcVzGY+yBNfvaO0VXnv4d7urMq1x/JTdrQHwOylfokgMvZhkhjN72bxmUY8ViOIfVvZo/bCDrtfLuKxGLIWRR/FfZioTL0AV+nOjMZ2FSPi6pCKwKPu+zhSPIqI7UOJWM7sv4Yw/AWvGecC4EDhnv83eeuc8ZH+alT5IkxhgGGjuJIWVzcXqylnKvx11l0yHYjizGwk7znuy4ukXS9fxfHrQyac1jRO+uPYZOqnL3YegdPZb6NR0htjPh3NgjtIbFx/w+YPWRpy0fSIjy4++787V3jBDrxOyGHZRR3wWSnQErbqCnmniX+ikVBPv6JhntX5/dxk6Intg5r8g3Fd3Z172XxZUez1y1U4S9HYc/0yi2rik8MZt3xcVDTU6LtIMwZETbnCjs0EGxoi8vclI+f032/X3iFXxKDwe1IzXMdfJv8y3soLfNyZTYj6w8Ylj6tvMc/HN0OT9z7EXSWCcqQnP86ErPU4JfJlu+j8DBaB21V4zv/tOhvfC2aum18bk4kzLMNwTa+5fCNHIbNr46dfjsjM12wrYHEe14SnF+13LX7hHfe5bjKynLazQNxPTHw+kwAVzkUAIfWxCXITsIZrekfmQrf8Kas+pFtG3Hli7Ef61Z6Go+C/TqlOx7oFuAUJNGc1TPyhinKjsgqYyWWc49vXthwhIG6E3Nf1RWy7kzFn+nzw10wS5OE8c3IOg9d1TO0EApaevRTHaAOzPen5exQoi0Zizkk20bdT+PemJt9Px4AFMHePv/YTMKy2a6v7jQiC4aKfUn/UWi1EdLMmK7EN8mFv8dVfBF8m3GPY2WMXGBm2NyVJ6g6eOgqukWqgkGqOqkAinB2gd/Z/xSQVrLhmlK6+3cWbMUObGc8LMavCtpLPOI05CxiOTVGY1Q==$ade857ac4f26b5e8cf6b202dabe1ad0e$f7fcaa28b58f$41672e1f5c1738095e91069f55c8941c68b2853a1fd180cb151dd19de2b943c5$228960 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-1.1.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a9x/zlkAvaxIiyWhvV63hvJmXqkIuxUM3XEdbEVBBCYr5l42dKKuQ+ICRNiTeJZGuOYB5Q440zcljJZmNVmnH5O6V+34MBTP4+D3ipMMbd/Vo38UMfp+xnorohNeS+mBTsvy311Q0K49zxVbIaRTjh42HHhoGc2HscYQygBLKoBbe9ZP9cTXHvUBd4+chgWkFLhwVjKdfdEDxFUCfRQiJg3HKOhZCpkeHf+LbwcQEoFXb+I9knhVIOqc4jyPzvLFonR1Hipqx94ZPuSGC2rwIj0o5oZI4PQh7GiZd5UE/Wj5wcYznhxBf99zeEcWKqJAnPDcAusCGTQsimQ/2Zbpmm99Orf/L6ZT+JzqmGcYKUIJoB/888TXHbssNRIZwvmWt3u/+/6ZSE0JXBC3kJ0hhpeCoEiRlbUk8U8jSgBGnKtvvIRQKKNhbqNdKjMS/vhSqKTTi5eDiLaYxP4A947G2X1D8tjPuP7veeZIDhrGL/OwLzEXYD3oq9o9vZUhsSllXW6P5OYDR3OfTD5IpBROSuf1pObT27ZaSDK27boSS30QvER6qPCl9wODOgNN/FI+jLkU/SJ4n+uXnHUTttES2TAWsB1FLlnxiG2oDK0/hyh0hGwdsCkkaOBCgeq3DLJDmseWI9MxcNFE2oFvHTm7nydLUsd+8+G52HiKmHtLd4+DE/f29aE6y46ha8Mmh8XD9FkmuBYQwqjaQVAXpYACIEF9U1ao9LUrHpnWxLwbsCQ6NyHGqtUE4tE+dGnYmSAFljtp1qTWL8wGJKoenScMTfvEqoB+qsxCg9xbmlnXdVZDDz5xEiZd+eD4LZNF9I0emEl23SoGaUJBwnYYOHGj0foziFDyuNC+VaosE9GiHUkrMn2pZm2/A/+z3K1T3ATKugCfiW1HzHcZqxERwjdiyoCEqbN30kqhoyd5Nxwp0MVxeYwJlbVn8nLut6/Wyg1Dvy+vfvkVhing1U6B6S1jURQ==$d39d3e33ab0ba660c0c565805904fd11$a5cba5d071df$0131f0ee6de8552b2f75f6455aca386f06266ab37fd4a6c71040bab18864eb5e$249901 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-1.6.2.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aFR9+anj+0v3CY1nL+FWJzeV+Fqcl4QSMW48RTGOmLgNYM9RtxozI42GEtpwfbo8kRJ22BL1PNy7xZvD8HbvpzhYykZ5hBpHEGRJrGaUzQtOoz4ANgrfLBUai7mfRRNKPEogMhy1j///HyONHV+rI8WpSRovj8z63sAIPqkyniwPpIl8Ndu6pC4GcAAG7IheEgUE3kxepnVryfuvhm1e0CSYeY4Vq5PXYhrnyZuCD6H1mbJryct3YcFmi3F+GxFWbN2aoPDxHmdRI0UvbFscqHf0hVw+BfMOVCccCBy/beLA7nyMqvHnYIcfi4U/ClrBY9E/8jtOK5YaP8/JBmDaYFXZFUp/p1ISEm4TeQu41Zomni4Nelp6BaxW0YsO1xIPh8Mxnw1rnQDHhHurT5ommKnlHW0mAK+NXw+V90kz3PPGXqaBM1iykYhYcjnnsQ8EyWETyY6cXkdpeaZKWIvBGJmcBqXcYYqN72u9g52TdD/z2+vpKuVMkcapDYgRZFHnW4etSLp9Xiiy6xbmU9ZJRkNx61tu4pqOXZ4g7tlUZwo4foPQ6x7jpUiOMb6Nlg0Wz+g5Hc+x/1sVV+Q5VbXfpe1+LovXr0596Q+/eF0q7kzb+SnQ5j/WblODRbf6Lx55Ycs4JbxhHCii8dGqS2jjFM/wE9EAsMJ0SxWMGyjp8CGNVZV+4EA6yAB8FWyyLkB1cjp827dKi6TMgPvrzzD4hzxsS9+Cn+vPrzgDZy/xAtVc4h196eEaGMrIUVv6bpCCTk9wEFd4I2fX2NN294GvqDltLwZtXLiCER6H9jBE6VBwz/Ixz4UjC3+kl/V3k/+bQNEUVxrRelggfBgGOGy8xM3sQauO22eTe5hB/ZxmWMNCE1vvwJKILD2Zjh+b6dWsdUOWcB8Q5BEjB2lcH+kfeLLB3ptI7XMNvq0DU12aV/bGfC0qPI7A+q+/HH9RK7z7lpQbCf74jOk+dHURy/vCX6w==$53b7ab36e5907a9abdb17a9b836a7984$ddbee5a34f5a$34c628be853e2277482bdc055d3109e3a743f37d659f6e80701ba6d15c676366$228354 -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-2.0.0-0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/arlgpFw1SaF931l59pqd+SoFvO9fnn50PM77PkmaxtpSSaVpvA7IgYYS993ncJlSqgJtAlP90WzIux+s9x5+Qb/aTL5LHQ7FPRViTpbBgEOYH3dz63LpOrUciSMH+hphudbCOoHztpPJkKOgKPhNnweoEGYDmlp7iYp1PiW49niPmBTUuWAfdZymzN6Ksvc2nCd0wMSOqwzbfW7efL8GKLGPCGVhxYlk4xravsusk09hYXfyWEuyNmgvCyzRWqkS784xi4zttud97Wep/5EghJ/RrirWBgZ1dvxjKE+Zise9WMFTngSN1u4NOsmiyU4MZwmEhZCeF+b0R64HefVgIoju2XhmVqnAuz2tcihkA9HRZzkAv8EWnL+SBogeWMa35suWSM9KkkhhuTr1N0oMlChg6BnKjDLRtXo0yaBAwsW4JuIx14iVL0CwoPf3MjYkjscO+sX+1HAV4EcpPti9fMYwh0LDKF5JdYN+CfBGAiubNcyDm9ke0f4P+5kfJhhKFBUa5JF8+VnAYrIGTgrM+1T1fCciAyJVSPf6B8AqawfhRgtcR0z1W1IWXMu5YHUzQ4dTihhLDa5LIdcX/jtMqEmKICv9NKrvFZ9vOdqjtM8rLRy/a7q4jO1WX1INjFVHdFkKRNeM6ku4bzcxbcwFlkAt/2JKsTz+ryIIbzym24cyToc6T7Zu+idaKmgXYZKkUTqWpuCP9i84UJDPUP2jJN9+4n9yo5LH7pBcn+B9NH93gcm2ehPEwjk6NBWHjNrO5IXsb7T1xFaOBNfWQtwGTJJZHX+Ip8Irly58SmU+9TUDy/90dBFyAKC/a1xm+ZKekGEUD3BTBDfxMx35tm7TEm7/erNVV4NDHTsmovxACitA4h50Z1EJIAFDBOZMbNnPKwA3ItSJDGLPmpwSci+M+0ZXnZHFFWXCVyFha35UibRujOpvsPxcZ0ECmbDAxqzULIiqJGEhtFJ13ucAgAx7T8Q==$1f4fae6b4741b244f554f237e147f253$XoHTHpwKANQy$254fba486d5c0aa8d2ca24474b35c86dc06506024a440bda835ffca13945306a$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-2.0.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/admBGQU5yE64/TqKqc2/zOZizgwOheHbOxUfPTTScB6xfluYegkpSfoyygu5NdZcs10DCSd6pbsTlNBwDLfaBtMTskF9b3hyO/2D2hu8NNaGs9UW5LYZEiGbwF/piaMSxFriX2yKs9BjADdh6oOINE+88vKfxfdbDrzOqKR3Rj/sRcAa6WswuRzw6rTi0SR39VXovraYoN8RVzyhl6y9Bw+JRMs7sJ8qcOc9xFx0+iCeJPmG2xfKyP/qrUg0zti2X90XSWDuWmHtAar6xUhkc27E5AfGm1Y9E52vjOUQMxyhNXOEJZ3qGqulASF8Zk+Bd4S1aR2w0tw4c+lYw71P4BJjNqNz9h/iuU4VPFhuqTSxhN77/pgqAkUjNm5iW9PM2QySTSs4Rx9eCe8QjxX97Gn+zEtbWPHVWkOvqDE1mT8m54YMHaFQZNGR5Omqgx0+jVxwwnNbqywV0qW+jWdoKl9pPOsS9+zrE0d60uzKXTRSW2umj9AsjsYVvNXmq2TqOLnn+kuHr9OMKQhsxV4cd7cMlmsyXBweB4Paa1Qo2R9gILCCcqKosVIGcUCPLUtZU1+1Z7d92AuIbfCwq9bHjsvKnsDS4JBEuteY5NAtWw+hjwzQw8EtwBuqHV5IxDqP9FOcXq/qihOOqGVIDGqIjG3KqE8aSVBbMCBF7WjMb1lUMXhmhcNp0U6KlmAuS07dKnGsovyBE4x4WqxQllkh5v8xyWYsEc4tnkL0e7C3lHHitidac+oGunWj86MOVxr9urOIOzitLICNbrHtBVU+iyB/TT5bqRS5N+vvbJWigyzZH+AeaUrhnuDELTz4Aw36DfgFCSX15GI+HuXFq5Oqm8q4K+pT20k/N382XHHuWnjtYD98pe5IFk1cJUV+qwAPdu2xSK/kjVHzkdsSPqiiXqc6RKeS3bD2L8aTbC6+oh81+MiJXnfFO0DIE/t7xsJGrAm1F4RlOctOtAuGeKM7umA==$43bb552d4621f00b516c18ebf3f433b8$ykr0VZELEfYT$0ae06ed0ae1b12440e1550ef79cc9c67bf5fe2d65a0fa1c7143029b1fa42850c$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-2.0.3.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/agkEE/Z3TvwF2QGPjJ7HB6MOs6p37cbzfvQTCyfXXt0dRIYOYT/EGz0fL43zSxlwBUIDCYIYEN9QAdxCAwHMP9Bs51nSi3deAZy/VbptTP1dBcJCVyVIYlagYv/Wa67Qvjru2VAJ3kvdGsaaMSNsdA7+hOuD1tswfgYVVRxPHE6My0OKpa4bnCmVhpXecXZqXH+x5ILcuX13v8GOTVfHOoionMk+5uwca7S/Mi6j6bp09dvkY9+MKrPK9pPFga4cRvByNim5ExnIZ76haM4puCU94TSE90GecGihOpujqYvIyMzEl+mWl91isohnOKZc9Z/AKwpshtPBJ6qkNFktwOAL1MMwuq6EcrAalh9IBKsC5PWvKwpHQUQy1H+xcFcYNLg1JsrR56SM574Bl6cIhGb7pd4m7ow8Uw9Jq5rHOdNOG4yHM3W5I0NPV6QIgjQ6cZSouSjho+HkcUu8hel7KsVn4XWLUm0xuMrWsKUCB61yJx1zF4iLnhmkzTvKtSdU2qavjYwnfHrb36A4/9P4mDovqoD8YMXXzEyp4NttuvUVOhU75/IdT+SItTZFJECbIL4lMAO3u/NUiLqbYs4DmS77POA4yMMocKfwll0WU8MrIpntucchI6RAU9meD/C0ygHzxs71KBqw4LZEXhWu+8aJVptGNOwRD7HzWbkLKMmnrIyH9XFm3ffbX/gKcV1xvNjKbfPiTZqoJ0T9uJ3pwibkM9w8u03Vd2kQU0Ahtld5+2B1ZJEu/bkBp/CHJ54ZVg4B1KkB6ijehZGxbYQOVBAbr2UEcXZea+AGKy5jwNnsj4DkiYYUFy34umtGoLD/BaddkQ34kyWaUN9QbqdnT4LuVjg6+CJecVNL0ZaaRcL9FvPzpPGWsDruvYgBPBy90C9QEvNbGHtmu/QHvlXgrOC7o5W/Tv9JmfrtHCYUT/40BDtnkcfdBWtyyN/S2vTgnF2EMZvK9EiFehsGbIPUWLg==$66655cd650b15dcf8e369e96fdce871e$r0MyY5lObnQi$1813cadd7b7ddc3d02ce46fa4925a4b28f413442d34218fbf92068d4e5167de2$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-2.14.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aRxHW8l9Ch66yF9HnjtHD2/c+rnxYg+w+W7HqNzS28ExqWw7CuGvcSdthfY//8EY0Qssq6D4idyMgN0VVAs2dQWfS8VWg+CCmV1BuObeHQcJxgjZbrwsEKK0Tk9rDqE0w5tZaZ37MvSoxv/CnilhTN3m31+KNawpD1XHFU6hff5643mlCLxmm1s2P87fzEz9CoWqGHfMffPl5n24KM0NjyZsdJH2+OSqdCb5RRTfc49k+OEPEvrvTM1nkyWV3TRudAC1QF7tM67pBF9gYgfKOBZcCCQnkOlHirJrCaSXUiSmvScYVCNiPonRAd4fi7PIr4/WEjlzPl3YtFIENpEmtMEml2IAdHJdTc+XMdco/VRKh3z6BMuZW4AhMgaynvQbwzTf1Gg2LZu0KkGPM4BmmBbfisIeft1WL5nkm6lfvT+wQmmdmVTpftJr8mbrua8lzmA8dIyNmlaPgJj9schnGtQDc6vUx+OQSDnRQM5Ta14ae4eCrJJSCw8WXPQlg/RFY05X8mGBoCdZR0RwSx7NevwqtoFmfTeEm+wm0l/NuUzXRtTGdxKlqciFvk5J++0N9nrYaurIhzZgju1TapgleiOk25lQbstod1vTA0y40+dvNXaF0a8bwH68SG2bEqrMpsRs3imLtWTEEjP7uLvd/75Uec4t8pjHYMrx62fuh/NTiHApTzXs0fw0m3xlo5841+ZKkMZXm1SaM3AvXMJUlDDNveH7dgS87hXgO9++NAze3PeV8m46mWynPLB7minjSoHjYIKEBpx32TIW8QcrUjEf13pMCPiFxFN5VZYqAm+WgcVjfKgYasrtsGS3dMO/1ZHzVyhFGVR/ETTnh0JkHDef3eYmSKsgl7vOHjw/5Mew5A0Y9z98jf9sxfr3Rn6nkvkeQwzdMb4phSETGjiSLlMMJdtEHlidoXmOGWoLbJp9cO8ANF2NanB90nxpfd2km1fhAwh3bJBGs7Gpae2ON7A==$f768b467fb1dfbbd92766d04b6d09f20$LcI3ZPXTXL45$2763673bbce8d05617ce44a4d4a17de88721ff9abfdc5b047a1ef511d5ab3d88$10$cbc -------------------------------------------------------------------------------- /source/env/web/encoding.ts: -------------------------------------------------------------------------------- 1 | import * as base64 from "base64-js"; 2 | 3 | /** 4 | * Convert base64 to text/byte-array 5 | * @returns A decoded string or array of bytes 6 | */ 7 | function decodeBase64(b64: string, raw: boolean = false): string | Uint8Array { 8 | return raw ? base64.toByteArray(b64) : new TextDecoder().decode(base64.toByteArray(b64)); 9 | } 10 | 11 | /** 12 | * Convert bytes to base64 13 | * @returns A base64-encoded string 14 | */ 15 | function encodeBytesToBase64(bytes: Uint8Array): string { 16 | return base64.fromByteArray(bytes); 17 | } 18 | 19 | /** 20 | * Convert text to base64 21 | * @returns A base64-encoded string 22 | */ 23 | function encodeTextToBase64(text: string): string { 24 | return encodeBytesToBase64(new TextEncoder().encode(text)); 25 | } 26 | 27 | export function getEncodingResources() { 28 | return { 29 | "encoding/v1/base64ToBytes": (input: string) => decodeBase64(input, true), 30 | "encoding/v1/base64ToText": decodeBase64, 31 | "encoding/v1/bytesToBase64": encodeBytesToBase64, 32 | "encoding/v1/textToBase64": encodeTextToBase64 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /source/tools/attachments.ts: -------------------------------------------------------------------------------- 1 | import { getSharedAppEnv } from "../env/appEnv.js"; 2 | import { getMasterPassword } from "../credentials/memory/password.js"; 3 | import { Credentials } from "../credentials/Credentials.js"; 4 | import { BufferLike } from "../types.js"; 5 | 6 | export const ATTACHMENT_EXT = "bcatt"; 7 | 8 | export function decryptAttachment(buff: BufferLike, credentials: Credentials): Promise { 9 | const masterPassword = getMasterPassword(credentials.id); 10 | const decrypt = getSharedAppEnv().getProperty("crypto/v2/decryptBuffer"); 11 | return decrypt(buff, masterPassword); 12 | } 13 | 14 | export function encryptAttachment(buff: BufferLike, credentials: Credentials): Promise { 15 | const masterPassword = getMasterPassword(credentials.id); 16 | const encrypt = getSharedAppEnv().getProperty("crypto/v2/encryptBuffer"); 17 | return encrypt(buff, masterPassword); 18 | } 19 | 20 | export function getBufferSize(buff: BufferLike): number { 21 | if (typeof buff.byteLength !== "undefined") { 22 | return buff.byteLength; 23 | } 24 | return (buff).length; 25 | } 26 | -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-3.0.0-rc2.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aOifc24CLkembp6SC9Fccj4DkN0ax3kYv80+NN2YAA793QyJ1jZXsHs4CgoesLragFkBE1+BT1VczPk+P8w/RCJB0w64b8gGTWEV4AGuzaxmqS587Yt3xuiav5DneftLIsRnA7Q6oC8bK+YrTa4mxwB/uQaw2TkWMH7XEO+yap0suoJq2ZEMcJIFsX0Dv4EUwC3p3PKyARkiaYbBNXrx2+AHLuk+yAE7pkOTZcZD+cgAd16HlG1D9f9i3CQ1Nohg5V8YwBahjFj17cC8RfH76KZB1UliaAymdghQdvIEjz7cCILcSWSgPk8KWw4eb4t7Nsxiez8yZ22ocaJ3MULZ0WIHiX+5wRv7Qq5b4VaJmp2LDubMahPMDsCoWJehDy7wQ8ioUtS4EqxnynUnGN46IjguM85PXZXsB9qXV2uRy0K7fDOT3C9OYWrXloRglfucd4QR8zc0whyOvYzEkcQYIMq857WpcVjOlFA11tyt+hjFX30ZhvlhM44Cq1WQyoZeBBdsjmdDINJ9hQl1w5Lp3G2fzkrNN0K1C4yoYJdIsWhqrnl9YpdaXvK1ejZnoAE0mEhRjr3+auOgLBa+n4XQrvBlEs1TeWknTzm8k3ANI/oo+r+aSa4D67+9Z1NbzRk1B1eMmhbVDVWYX4fu3Cd1qDncyEg+OheG8qToWiRTp6dAydXVj7UugXolc53Q+wKl5ICNmJ108or9Se/QJgVy+/6TcUA4c+T03ldgZc0UsQMzGHy2aZWUuTs3wjPdTsz9b65XuBiT3Cv9aw1rwIIX21w9NYiL8FWHRAB1qB1zCQ1odjtplO/L7CjoSmgnzIrhRf3pYQzBI7mg+EcdcTO/69ePO8xKb7cNrMIH1xQQMqsna0MIghPJkVGwxzMFmHW9PsS+fJQjQdBGC5hpwJ2UbknD3YaztpaY60zaVwoy/repQhDOaP9lfJ1PChq8rWxe4Ju5VpFqTqLYsMW32vfjcew==$017c6631f0f8327be6a0ce8dc84fbc8e$WHO84Kg7uzxk$43b76087e49f12050c3dab93a80a33b6771b89df2294e08c1066fd5805024295$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-3.0.0-rc3.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/abFjjsobafRRfV1ku+fD/fvKtdWPqSGGDYy7nphmu3k7wxuWnlzsx/dvNhIvnU6qPvf8YNVsdh//jWE+oHJnkRIlblAkVFYolsNT+sP3/F191zgCVO2k0W5affB2q2Hq+R4x4dftB8poJHbp5u3Giz8i1IJOOxqfD1ePY0BBiZqplkRPa02zVjVf2528Jpouryim06wCjaRKBCtQnSpJqGOvI6GsaTQ9nDjgwuOCdyWLoSN/Q0inGsAfebENhgVB8D/RcLIC94YZztctivGH8IF5bfLuK2A1tSaRm2UWhf4OWmv2Ht5NEKN3pN44M0E9N+P7YeH6Te0QfUpnjzp8rOyUj62B/Nm/xkd8/cI2sYsRO54fVF3ZZk6eSAuOxCgH9POeID9pkugWJXkPS3cdxfVBziPE6cp3cQQC84J+cs9oM2xb1n7svZejnae/O91MZvvC0aYXsXeKzVVzd0oo+vXAELgkmxN+4AoIPxoG4nrP4eLbPQwHrG8gq7oiPpMj2EYKCDJqd7cJPNTW0Cx0VFUGNVNz+g263d92q4ZM7jnMZUQXjNrkHasoyQt8tlB/cCzjAnWRDulVFvbhVTl63rr+NfkZD1mKWAoPYSIEJj40t2zgItb8I6jOU+gNMCg+rLgI5WyjBGGbSY3aBxpK2REChOx65473H1MUpGTdFIdaO/v9yrXD4+XzfV55dDqOm/oADwGmgBGArd1ArrrCqj/cwy74Dn1tuM/EL48N05JlajkRAoW7rYn4s0qKRlDOPI8uEYguB41d8t6AkQt6xyf8rxVwA6+p2ln2mH1OQfz1810cAWj0A3kAOR1aq49b5nUtrLh4919Xy0BtuTJNWUIiptBRK/aCbNHzcgU39o7swAka0hSXFA9L2Xknr7PwLYgVxZG/kaEjQLDiA3pVfFSHSRo/BG/BoUx5EZ5MU3/AW7GVZ68sYnDFUjMCq44IYjvJP8GNcFoTBoVj8z8f+VA==$3faf91313e745577c42cbd47290dafe8$o6y3hlggoT2s$3311ca7b1d8856fd3fe10ae77b4a972459a6ea1d89f0adc50c6925ffdb97fbd3$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-2.3.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a5+qJMZykDOzitx28+osM61tI0FwoFd2dHxiGKzX4d1pJ1hfFbIKbKnH/WoppwZCUxMIXvZ6cPesOxVShy3BZCfzYI4QRKtmEyoMhcv7BCo7J1OJK2VKPTb/PKXq38R7n8Ak16AAS85tt6ntBzPOuKu1G5XeZa2UhndqDG3ZJYOIH6vJyiqgLLbopHkRNIIcB4FBiHhGpMdw8PdziNqC8NlWFa8naZXjP/i6hmRsEyxFXxeoThKayLPvMG65GAxQJ/7lA20kAjdWKWXohSmWYSPXcaNwznQt9AKo5IqrxF12wUC31D0T5EWzieienMK92aFoH6GtaAaVG8f1aA1VLATO2nc5h2ik4TERCaghsq1XJ26a4zEK7fwrjLBxGDPJTePWi5Clxsy93pvOSdFr9vOv+iEQLcSkwnGOIIrFteshKh3DHTb+YsS9JjUuJGwGdQ2c3R0yJUaZmbN/WW7AZYkKtXDH+8tu5YIVJbiSTmvg0qD7lwup9ScwX2m3+FYc/+O8JXiVkz/t2icEnStnfRW6HgirSC8goURMhdSt5G0+EqjgsT+ImucWUGWnIFrkeQXMjZJf4tNloAH5/5Vuo5rEzYQaoGd+FAPDwxf2EI+rFsrj4hKR18+D3BFVVd6+BUBQ9BDa3j+WP8lnkgcypiiYUaP1/ax83PC/sGKnoftfuEz7XmGRQl6v2FG+JMAYioAoOUZV0hXycfYiYslBfun562IdRSDOUYt9NZm6JV1AkSFo6/2m6MC+FjsLl/PVRLOjWWxOlR5T56mJwrxCN1B//FFLEeVpOJGSSrars2qyZH+HfXxFSuHamIESKFExm5YpGlGOGLN8hE6vQpZIgpfCQ2vfHX6ZjLQEKL5rZVvPuymXk15cwPnBF0hv7lBqkaFfEAftJtKkN3SPP5Ko9Xib9/xuqlFXznkIE/tHpSN1UqVU4srocr4oOlc0fY6qR6XzsqOjXkC6TInrcvhvVjc6ZI1GwqTOogj6ijctAQ74=$6e75f2c2d605778b3c3b62b1bd68110c$PkufoNg8KdOP$fa426c2db328e9afc0eddcea2c8442796cb74cd7673b538ccd31cb044db278bf$10$cbc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Perry Mitchell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-5.0.0-1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aAoim86Lm1d3B6u9wrXGlAwI1Gc+GGuscl3XhJDKii4Tx2Kc04G9758XvaXxBUjDr1kCiSG/PCOnSdFr513U8X/Utr7th4x3HmALWqmyKAehUcJvKItC+i9IVs6/ueWnaBcCrSuPJ620VPenSqXHAp+Jusq3deAzeRsdwUPnewGde0xnd1DJgOWsQwSlpQzIc399eYjESjdYXu/Vj/X7Ls/ab/lK9KZWwUuxeiwtt2SphjzxEX4gbm6njEqi1orF/6I1Ay9mu3xZ76K2GhuV67PBirw3B9+vQ8P2KZRNdLAqASngU009iFk+ZRxrZKVDdEPjpzVsy8tYBE5DmDy2zJm6ZE6KsjlsPBQKqCytL0a+f6Y3Ppp4PKpQbW6cCVg5GfsFie+2LtCbGfJkDfzZAhgDPTcHea03XP7YFhXkewOYC6OjXC2SsZXgSF06AfrZceiK+ZJ6Gv9IDCvE0MVmJLkBz7jqYmwAW6mZPsRd8GG6aKJRTa+TAXnJDSuRan4npAPaaP6kZlgyHOj7f+mGpAsnKQoeEQXuG9V2wHNATmnTiDuOjfbNJ+TwWBzWUPpF6/wmCtkAtp3OZ86Q73+Bn3Lxv/Vzed9wmiDCU5vtdnUMT6K7HLAlfvzlSMXkU32e5x+l2fiud1U6td5zcr40QZgX1kdwkq0QrWITdixrEnGw8zlLJgWG0wMpdVziiLZuzwT9FZrkjsKobH0CS1sS2/PXx9n+J5n2Dh5OlGK4oGcfp7kN5ZoVX4QeMo5WeHFsepvIwzwTBxlzsaSv7B/l7gS170Bj+GiuOebmxfq341OLWzz5ZNMpk078tdmrCyB8ROdK/ZrO5yAmshB20nv6JFIEuotEUe5QuVaiwThBzgEX0Z+Iz5vNctQHqhbN0vB/EqbsUhr4czqEKIwu1yt3Zpc83BPTt9uaXTOi6w03ITFInSXbxUWt+yqRtBQ4yztbZSC5mUoDTl2vrgPNvVTWNkgBC5JuUYsgCSPCcVo7MsMXHO+fjJyWVUhsIkWkXPJl3O7HCkxAAfFgVEBMVFhGp9A==$7bd1a3048ec73dd64bac4692b29c43a6$Y06DqjtvGqAr$b33ff45eb0e0c99c91b6402e1dc283bf14fe8d320798f5db296167dde65f7b56$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-5.6.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/atPSbPfVCsQnC069iQUStUPkLqRBztt/lZ2baAUjnSLtFQDphdYyDqz+7RiBlzkZ7B9NNjQ2UGZseLSoIcwkOx6AWPigaux1bXRW5IlWVBd2BHU0NU5t8XR+Z4HYRt4UcXWbhbnf4caSyhZpbe9Hf4SPbH0LtKpOaysAHYWL+TQpqX/RiyKnJSzHsLViXkt//XhCpJSa7McgGTFrSwPIcDOIvODVkYRBTzNY943qr+XWIXZ5b4wdJpwiWqGoIkwzGtkUOL1Aq8l2u5VmKqjZ2F1aSMcaGh0i8TUpBU7Tl/rk0090tMJbhME0Zo6JT5sybSHBZpJB0c3nqPwsprwkhIe7X86vuHaaExFD3lgrj0HuN/eEzcAgKO+h5D/2au1CRYDMklSeaMyiqUxZe2Zy7p9IZ9NFHM1miyYxnNAk++SpkpeyTxFvLZqZEqKq752ZSy5uw0oYl7KxmMpdF7cpMCqMPx2MMdTmLUhKF+mnsSLPQgGUXzwDS8smf+Y/6zirY9OlgJk5XRsnXa/cQSSizELLwECvUKFktxWeOu2JCPfJ3aEkw0RxCyy6rHYXTblkOoG0Va5lnSq1X5Dl5IZYSE4LvN2y9bOaUCPjpBr5DmwPEcKKfRXcy6aARjVEIp1EJXfavC3rsGqCGQJeJhkTc2+1iMAyxDjnn8vK/uUROf0NM4r6uzWgK4n96CHj9UWSHz7SCewFsqWw951+t+VcJPP0YyLAfbGhDOgzeWDFd18I0d/CasNJpPeZQ1DGlj8jIdA5ccYUZvwWjyBSGQNMe0Q26araxV7Ilj3b9kZcB8dupzJpn1zlb88XWc+hctegbBwhQwpVg/2BRqbgKLg5P1hcrlQv9rbXWcmKZnVTdmhqw44XQ77me22mkvNnlWXn0Z7C59opH3ZVMAONANaiYWcOIp3nPeeE/XRZFFnRAFgVyF+qS8JF29tNpnBFnMlxuUSaKNOE4Vs4jYiIroZA/g/c7fTh356xYoy5cWB7CHQIn1wV1w9oPU6DSeb7libVYBHyqzy0KBPtSJen/8Y10VA==$0c13e0f036a5118de159538ec0f5239b$UyNsiZcvF0oY$1a9f2d3ce85353d87eee1213495bbb6823adfdcaa25d672145556fa5ee6cccaf$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-6.15.1.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aRukE4Ne1Dx2F9LY0ZLfPJ4xNToT9XT5ymXN2DRcLQuyDajldJ4AY3XJFxkqlh9kWbZZ1Sbpy/NgWTfDUmtCrVJNlPjtXwIDuogG7IL2YSVBCrZbKD3zgm5hIDUbYCpQmhBTfGhDpvDBWrvaS09ik31bKQe8FI53r4EDl2LOoriC2TRlwtIMgEUeI7ryURCBz47ry96fY0b8KL7rqgZn67KkLFNjJDs8PeYcZYkgvxkDf5iUr8VCtg5c2UBSi2W+hc2QyFTRpnr0GGLeHAMidj8XQmDDJ4iPjFvQ579HNnTQkRbUJNukuXl499yGBTId6BGFjHDIVyE/TgoxCbCneZwo/aa52KgBKl8ejOKQfSPWg8oLeYkJG6WIfVvq0M3O2XHPaM3WpTOf3EuHp6gkfZ+NQtWRdJJbQtqDtEeTagQfxRrAuZXAwC1sjwwD7/KaRugg0JoMnsS+cPeJ3id8Kgv8bbnbvms+mQi6NzD2pnaYKZxkwpvcIfxx+yJEsG2+gSXOdXVdUZHJMrm8DSHv7840P9RWC30mUtbedgqM3xTqVw6ZucM4asfjCseTMjngN88fTI3Ll2r8wtEy2rTDTLl4Q4syKdMmSc9aybuVZFvF+0fhEb34L491sSPqefQfH67wHw4F7dW+aCrUeO8iTL+VCac0imHcr0JvA/jsIC4XP5RJZI272lgvC5Qym7FD1D6UAUhNhNdOYCAFaX3KKzgOIB2FVW4R1Hoi/cj5EJmIYhsjHJ1UIiFLZKZn53b+W7+vkS9JLQWvpDzfDO2EFVqydl6EgZwP0a633N1yoMxkcfOoCB1Nops4Tipifje7Z0qqkbX5W/NeMs0h0QUWfXG7/zerObWkVRplsaLJrSG7NpD23ysnIUSwo/+o0Del4t+yUGme4AQPpGYUNAYrQDacSsYh7+jNmKfYlqfLjfX6bsLapM1ZMz3CuDU329Eq/r/VUug7gPmjdASKmc66/9jvMIycWBT/z3GR21Uh31KX9NfFshPrWzymldQJt0f19QqEMJ2t4CjhqlAh3RMBncA==$ac4a0004da32e92999576287f0276136$fQ03c/IMpwHq$7d1f31ca9ba39256c84b4640a913ed2b3ae2c2879f07ac56f62622fec67db496$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-4.0.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a+mXTwklU+B1Dr6C9rJzvlCU8hVSwYOPzoD4bR4LX6DOVgLlR6trdWwMEdpz181E2NGQEi7Znj2iPX88AXSRpcFn6zCX+CReZ78HtC3a1mo0CWeB0gD3Omkj5BWDTiaNWlntVIbrcjN4QlDTcpSjuy2wZbdXt52mFZYq1YcbFylwUOXyxhYNRr8nTxSuoyVgPTvutAI4fZVqw3YO5Ltgo3NzHsaJDPQwVy94qv+f3bqqexfMwqg22ezTsh1Wag3fYnygF6if8tFpcZBj6gOzI3QrigBH+1TLh5UkFkquUmPWy4LYFlh6SFYJBgYBWIH1uO/qenyaM4CL9ak950/QXbu1Nnpq/xP9+AS2X997eylbuMMDZ1mXLLxl0rHaLGYlLEq/TddewnlHDESPmex0ab5zQi4fNWHLoVPDZUoyGWmnq8NevAFVSqNvTxrHGl8twROwX8IeaQRJCUtq57VyBcV+XOJmisRgegnVSBua+T/X6+JFxh7E8E88MyAgAGB/Q9FT+hJQ0U438fI+ZFhOBM3f+7zzsRS1+Pz2xu9+LBT5R2VhzF0RLkNTVNjOGNXy7qk4gzmuBnXC8jarA+5g4OKpMaZuQjkdYd5jUfYqlxeqgYkK0MZeQGVCCz+WWX5Vk8OyL5rOnMZfLPDpDzBEdSX9GRqEoJkGj2bA0M6KdF2iScBQXUdFV47O1vHOKZ3bE03CKKzsmVdrnwnBg5u9H/QX3I9IwM8Ya5lV/AsFS7RVJ5ooPBe3N6c7rP9LI+dAcr55imB4jpec+d/xt+VGd68pzzbrjsKpkVzDe0AtGrsdL8oHvf6U3PiStqhP6Y4OeomsOmBCAOJxW+nQMmSO9CSoObMeVOwnsoJgWdwACrrs3HYq9VEvtehUFbZIEaJ72yA1sUBvHgxlzbtWkjxUGVSGgP1SxdW9xO4yTZYgKRqyzrfXnEyMoyEMWOyZvGlnIx8gVW1dfWrouKCJ4iDzcJ2/9ZiBCBeqZZfdMgzAt7vlk4qJg99V5FUxC7mNnwNDrCfzatwUyqk487sA1QReonImnj0wfhIEAiqJPGIte/qk=$45b8efa1fc0e23f37441a3ab426c3044$qjE8GD11ZyPH$c317aa18ac342aba1c119db643666a16963738c79200a9138f4b3fdb63469637$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-4.8.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aU4RiL5g80uslUi31m1G0Mll/rU2xsJ9pJVIEea7N1PPcDEHuI9rpdTzn3INRRoAOcueO4pKG+FTX8JCUacO+DPX4dGY1DIM/m+zn0Gp2QIODiYheB6Pw5FLvx5ZCVvXBYNTCldjRRQJY5fYTRjDldhhxjrx87Z3LD89vm/J3JaE5+jPA3PR6uot222BIp7CfP7xFVb8ZaCsrdom99lIZMJhhdb+eP69+vArhiQN9v5vAriMaM20b75z/eST1Nb58mu420wjw/1lRHfvxo4wRHwshV0jXzDXhA5rO6Kn+JVJ0w0G0l5CvvPpw7LBRs39TEjpFV13EwMTfDgiOnY6oWpxsRg10+v910CFQmDCn3bhI2Oq8hBbAm3kXwFNfy/TCtRFWNdS6ke3SaFRqbWcQ14nd+AoDYXG+NQbY1wA0jzRoiveI0iC64zGz3dzAKUADsYREdiTPadgy+GfT9ArBGn4Hd3wYGehaFU+DxzM+fZUkkrL6YZ8UM3LVzxPqBKkeNL1CPjCdiNrAZ0jhTCr5hSyIH52JlAeKjGX1MI53TDKiGAvCYxli+OPsPEoC6R5iY6Z7CxNSgDRjtjubRNnQ8V1EMLe8J50UQP2VSDLpNKFivMLvOoNbsewI9+NQIaag6mZcKhxgyhlIpQOKvzxdwMqhpkUYxvefBA8kRXiKBPKqsLKAo/PXDY1dikD22n15x6i5pINR4rHe2cur+CeH8+nagV6MH+Jl/bsaBVNntNgzaQV35a8lMvK4JRdy1Hkva8jP8ZEOXWtvi93A17RprYcXPkaoEAaFFNVqKelcsztC2GgXhlM95Lvr9pgYU0qk1EWAkmrWoivZIs/TegZA8a59wIY5EIDhRriyb8Sz6QCo1wDI0hQ6fISGQVWTxv9T6M2YbWf/Nqrss8iM5gJxvVK7mB+cEMuqYSnx85pDEcUh9Ag7ZnkpZEm/hSJUuJ7BCZPV0iCYtzRXmU+BD+Wh0cpkxnq2izh2dALBXAVHJ2dvuuE2atlwLTv8FAeZm9zoI1afXaShOeaSJ9R39PFw2gCC9H+RXBwCcsYb48h80EI=$acb0e4006de7ff5c9d9841ca8f1f7147$lFtMit+fKTqj$9f05edd5bc05859aaef0d514ee15736147b5d8aea5cf6a57885fcecb619f741c$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-5.2.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aRzykPhMjMfZ376U7uvW+3220w0flt0/dG8B/BdOzXIK1+8vSfFgQMRFrookEqLmZhlmj8x2z5f3t/iKf3Gsb0DErnZiGIsuW4lICzYzJSvDJuzZL2yhJPy64ZbRFfTlL2aZcLd6GsVbkuktQuSKeVNsIFQd1C7k0JOalQWwBymtvPf4KTFnrk3+ERSrkFN2HP4F+bPL5pD95qidMM1wjBfEJnXx6fXKx1e7ypBLrIgGEnQtlOx0jf4vrw71vCgoO9dtp82ER6Kc4/kUQGPKDmzkUjmap9XPH9SDjsU0FkqhgU8RiSh7goCyXkcCYZvUnHl4Gk0IpnwyGBhan3pB/UG6GWPTbQ3bq7nDQB8lID5IcZdI0rvVQaT13JRKwnaqr4qveGeekX/SMHdpLOaQ3EK4IIGgd/HMngFSjDfszgO55ls25/kfO1M/sRP5yG7RkHjKW+Z4PrQN04gAeD3DOZOXzCEB0WhWcROviefsk/jyTc+WFQxbU/JWJJ/JgAvXs8dsJMNRGbu6gjlRbWK52IelI4raDTE17AhjuQoTNuDixrkpl81e1UlRUuFISG010UaPBjuqs25oSneAM/U1mLn4BLukGqL/6nU6EU53DhAIHq25KT6lOgJmOu32ZLJZHWhdw4AMJjubgsjAuMxcuvXZXVv1QD/19JbCmmNg5wCcJEYJkqbS4BQGFrk7n8fOIb/47mYCMDHKkZ+UtUYQaxhpyVB6N2oru5NJJzDYXAbEEJilLwfQNZ+BKy/pGcqfa8wnOPdmTrnLngx1J6r7F8TPA9WRnas8MigZe0RCNHNT5Acs6nOeB6o3DuynC8bg19IOz3+U1aGqxaqu7T8yerYNHXyfyEcXAurPFej+BJud7haDOuunr0EDZ8lvu77UAveGlyxIOm8CwQD6cAqjmzOj74aEY/bu0pt3T+jLgREEFicMl6/zpmpmnQWEgyoMC3UVvTATbcNT4amDxaXeSPiaWE6hoWhLIn+2qUHjvFB3pg/FwFDT25ndl5vOL8zKuPxHYxpSMAZ5t6XSp8Smkj1Fu2fZg0Lo1jUSF+SdpXNw=$86b87f2d5b316cd69d82b454925e216c$woDAiVQqeb8T$959ff628060aa0c65928fddf2b7ed4ef9e5a19cad55917a079b1656dc3b60739$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-5.9.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aTY3qEjnVQaPXtuwf+vB2tMXSommrLF2EZcX1Ns4TAU5HQN4GdJkfUDrodtTmVgqIG5n7XNP9V/GWl8IeEFMY045Ql4jxBPNOPZmGoWRYiKR2y6Cujbp+mj0fJ7rMdysghdNtE0ZbaA7cwkQV31jWDPDjbyG0osbmchqU/QLJYzywdCSnfpC9gijmVqltXMNvaNILGvlDGJqOJefh4wi0O0xBZoKYhmHX1DNMxOeCtfk4jt0CYAL+IpTOnT48tM4AVmBrT9vcg8WqWcX0eaE9NpeqnryTbXKa3vIAeJdm9JkMpy6E9onu30uIuHE/7oUX/rugDhfqTlDf7OP3CezaU0xDJerslLjtjhrDQlJIR+Nq80TfMJmmbTvWyC7pelvdoPulSd0ekti6WT3gieihD5zBi5ZbUVU5VhruFAYPktmhWjZ0U3qb46aI5X5xUCjjFG92oxi3ROPmkfkV5GhKkNl/tMxEPuugKZKjMMe24Mi6cc5Zz7TOYnUU8luyexERvq88MsKOaRqQotEaZUsCJGj+k7qh5+f6+gGPfSDS6tqqrJ0JaiD7ySemlvxnVYjyH0fTXiTWTdbj+XgCuRXGMBKVJAxg9n+F4l/IHKLqEIYimZ7Ek0ElwGIa4c0+j14sWnuqh9TVG+aws8b11DBUZpGszOHREDVYH5Hu7sv7rrUdwZz8z7WzddzIQjQGzjVFf1C9XYDj06Qxbkf7CpZnwqqBf+Ehze4xZNU/eCg9HIRxJavsw4HN8LJJP+/L1+HZA7n4hSAL9uyxahRTCkcRrM1kcUH/1v669daLPTArsuHSjkWp8dDvi1OMCfZzRe3iRe2Ji/petWJFsj63gjOrgi9bOMaOR7lH8NXaW9UshHpuUzpqeXu1h//CNuFXX0dvIsrS6d2bPcnh8ToaLWR+U25+hwGnkDEh0vGOCmHVqZI3TguE52Ag70SzAbARhl1teHyXXn0dVB/bwCFqghENud3u0ND0QwB0Ca8Lr7KaJa8wwNO4m1RDrnunYyvEr8InZsXx1ZQj4iIqD6QEG0zO+zPVXXmhWSC/bC7e3mkFYz4=$6aa37ba90ab0fadef140aa741c6a32d2$LthWtGrI9GeT$04c053facaeb603b1e8bbc162888705c5589cfd8ed755f01ab399f6460533753$10$cbc -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-6.0.0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/aNapA2M/PR3FPP8sAPnXf2qd4AAgJEjfzm02BCiuYA4q5FhgdP6TCqcy/A6Cdz7e3qDIr84Dq7vUWvJhvsL2OgRl0z1sGjdDUOB7HT+Cvy+lvlgygxB9mJkuu7LvHV7HGA29DAgnkNnmIACHoqMoi0ZC408KKYIEeZHV8cR1sJ0F0Sr6LbDNdhPtdehnH3SVzhFif1z/kHj5Lxghlt3qR9+cKbFZfchJ6RUI4ULTzyHYxNk5uVZ4YwGPqyCb5+IoZecSw8qnw0bitu3/TxgG6p1MmeCLBCmK+nRgr87g82nHoNiJIUvERQFEO62lAg113P+MkiZC9MzYBchJEqQs4B+bmtZ3GDZGBQXf9qr/liTHjsnZB6EApJB5SbUiYmZEUp6wn8EGreSJH/NCljX+mswTFmOSNAZ92aJ6opJBqqHTTQFUBcM5fFKdUczjudBB7x8SywL8X2iAfc8oauBCJYFzsi8LQDuaM47EeewmBRBMGleGOVSszDvi7Vkqxv8XKeSv/TgduouV/oJE5K29cpiZ31n2jJFGQm3QvhRFUM1/V49acuqPsPs/SNASyJHWIt5OfUF8r0SkaNs9M+0jSe937tmPbGyBs+Ys4OAWomA0K1hmUHScvU9RERUBvvB0mSFAd/RpkENBrZqnd/U0wErTjIRHpSGTFiolWBK5S1u0kVEucvEBPZX79+66APYEzSqK7+Rl5C6i6DHP16AIZUmULISmUlU68plKuKZaXRSui34PCG5FsbffGO3NwY9Y5UGjp9lLDYLK4s7+aNf4Ee3nSkrugMCPrr1BUFeBGBTEddXAv1Y1owo2IAT7hXaq6+eUaxrGcA7Oq/Nf/eR+Owu077aebpCpxymeEuWVXC5qulj3koihJkkG9wwwGFMlaEJs5tgbiDvAw7w5g3OZM/++i59HVJs7+QZUUuErLuCuF2CSNK7MIzUE3R2qugo0x54GfWCh9iI07q6gH0lh0q4sy8x/d2usRTtVKUeCc3o0BvK0GvKNHheJS5HnyHpsEc4ARgLumsWnXGWOpd9ViwLKGrh85V8yuNA84RVqDY3lAvCyLKkLrVxiUCTLhq7ht$b52023c9269b6eab26433ec09fe44835$i+jnh5iM9Eod$134684787968c31ad2a1303ed621462a47fe4863674699ffea4a9b43e22dc7c8$10$cbc -------------------------------------------------------------------------------- /test/web/LocalStorageDatasource.spec.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | import { Credentials, LocalStorageDatasource, Vault } from "../../source/index.web.js"; 3 | 4 | describe("LocalStorageDatasource", function () { 5 | beforeEach(function () { 6 | this.vault = Vault.createWithDefaults(); 7 | this.vault 8 | .findGroupsByTitle("General")[0] 9 | .createEntry("Test") 10 | .setProperty("username", "test"); 11 | this.mds = new LocalStorageDatasource( 12 | Credentials.fromDatasource({ 13 | property: `test${Math.floor(Math.random() * 10000)}` 14 | }) 15 | ); 16 | }); 17 | 18 | it("supports saving and loading", function () { 19 | return this.mds 20 | .save(this.vault.format.history, Credentials.fromPassword("test")) 21 | .then(() => this.mds.load(Credentials.fromPassword("test"))) 22 | .then(({ Format, history }) => Vault.createFromHistory(history, Format)) 23 | .then((vault) => { 24 | const entry = vault.findEntriesByProperty("title", "Test")[0]; 25 | expect(entry.getProperty("username")).to.equal("test"); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/resources/vaults/format-a/test-archive-5.0.0-0.bcup: -------------------------------------------------------------------------------- 1 | b~>buttercup/a4QUzXyb2LmuhEIculRKpVcfhIzn3E3H6aoCyw3JPJIDirfKt9MnX14SvC1x7yuEy2MbesLtr8HlHOufElYq2NCR6k674PYzDE15aW+FutnrHwCq0AzkHngG0OzvHX6+sIWGI78mK6aAObLK0D6YMYT4wyNvYhQqGixPKhvqHYPTTj3tVz4FA5CCJlxOOS2XutUFhG1I0edzRisqwz3Hg2OORa6UdjrAWh+ytRVNsVJ1/3xwfcTBrfQYD1WLSQop/Lmdhg0/zjgkF3YDDso/oQ9ah8uCsF8l5PlwOfsiBoGxNpwrjzMeozhbL93fiw9SVYm4VleziGTR9eNTA4pRXR917tiiDVD1SSPRpT6PBnlTpwQdVH2TYMXgLiFP9naJZS+JbO+422/axIoRIRcHbDl+eGK+3ED0Po8sBC+MJWjgzn5jQlHbnXQI8ALE9pj84/YOXsH26wWgyDWJKRNho4sTFHjCjFemQGqdr8w2rIYlr9eQbs5iM5+AcjG55zuXLUKyu+Ji0seraVH2xPaJ5aT9hNiyn2uhDb7UnmVpRlflX8M8Pr8gkS+6Wgnp7P5mgytSSEE+eYdDC3TeceMr4TQTUk02otmZ1jhMBlqDjz67JuvGF5tHYd2gl+IYM/Lbp/zJT3SpLPsRPHCblps04lnLOR6ZdgpF4VQk9ScgK4hfyFrvn5I5ikhCMTDK9HlOZmB9d0FfFWuZK9orm8ojLxEb6WB2FEwSw79GOERPuPCj5hIw0KHQh151JL8JEiE8VIgBY+HwsNA0GMS7H/mACjxAYOTvz5Q9ZpisO2Y6FXERfidZS7bA+TEf74vn2pEFCAHFZaCNs55d+UxksKr2LbsDoeJcouhwS3ALRPGqZ3n1TTkXahPqZ1MjvIzBSSp7Z3c94Br5RNUax9dPYwB8fvBJgGQ6aUB+fFAl1kp3IRc9NCwlRUNDMCCf86wGKMWZhU3fL6XsMryQgQErP0zCbNq94ku05yfnonzSazODlVOPy0iqzFtyI0/pimXsS32grQ6pHq2IceHKZGZKtHBOjbImDf6OdGw7WKdavL4pp/Hh/kinslqQBkvuVLY42FR78Uu22cQzZ0rRciHmTrpkC1w==$8d890f68ddeed44e8a61ec2eee2f04a0$tYa1R3IQ8KqZ$c525d5a721d3c6c92fe45aea7fc943fef60a2b2ba648c13c652240dd0ef7df54$10$cbc -------------------------------------------------------------------------------- /source/env/native/encoding.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert base64 to text/buffer 3 | * @returns A decoded string or buffer 4 | */ 5 | function decodeBase64(b64: string, raw: boolean = false): string | Uint8Array { 6 | const buff = Buffer.from(b64, "base64"); 7 | return raw 8 | ? new Uint8Array( 9 | buff.buffer, 10 | buff.byteOffset, 11 | buff.byteLength / Uint8Array.BYTES_PER_ELEMENT 12 | ) 13 | : buff.toString("utf8"); 14 | } 15 | 16 | /** 17 | * Convert bytes to base64 18 | * @returns A base64-encoded string 19 | */ 20 | function encodeBytesToBase64(bytes: Uint8Array): string { 21 | return Buffer.from(bytes.buffer).toString("base64"); 22 | } 23 | 24 | /** 25 | * Convert text to base64 26 | * @returns A base64-encoded string 27 | */ 28 | function encodeBase64(text: string): string { 29 | return Buffer.from(text, "utf8").toString("base64"); 30 | } 31 | 32 | export function getEncodingResources() { 33 | return { 34 | "encoding/v1/base64ToBytes": (input: string) => decodeBase64(input, true), 35 | "encoding/v1/base64ToText": decodeBase64, 36 | "encoding/v1/bytesToBase64": encodeBytesToBase64, 37 | "encoding/v1/textToBase64": encodeBase64 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: push 4 | 5 | jobs: 6 | specs: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [14.x, 16.x, 18.x, 20.x] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Node.js specs ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm ci 18 | - run: npm run test:node 19 | web-specs: 20 | runs-on: ubuntu-latest 21 | strategy: 22 | matrix: 23 | node-version: [18.x] 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Node.js web specs ${{ matrix.node-version }} 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | env: 31 | MOZ_HEADLESS: 1 32 | CI: travis 33 | - name: Setup Firefox 34 | uses: browser-actions/setup-firefox@latest 35 | with: 36 | firefox-version: latest 37 | - name: Setup Chrome 38 | uses: browser-actions/setup-chrome@latest 39 | - run: google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost & 40 | - run: npm ci 41 | - run: npm run test:web 42 | -------------------------------------------------------------------------------- /source/env/native/compression.v2.ts: -------------------------------------------------------------------------------- 1 | import zlib from "zlib"; 2 | 3 | /** 4 | * Compress text using GZIP 5 | * @param text The text to compress 6 | * @returns Compressed text (base64) 7 | */ 8 | async function compress(text: string): Promise { 9 | const buff = Buffer.from(text, "utf8"); 10 | const compressed: Buffer = await new Promise((resolve, reject) => { 11 | zlib.gzip(buff, (err, res) => { 12 | if (err) return reject(err); 13 | resolve(res); 14 | }); 15 | }); 16 | return compressed.toString("base64"); 17 | } 18 | 19 | /** 20 | * Decompress a compressed string (GZIP, base64) 21 | * @param text The compressed text 22 | * @returns Decompressed text 23 | */ 24 | async function decompress(text: string): Promise { 25 | const compressedData = Buffer.from(text, "base64"); 26 | const decompressedData: Buffer = await new Promise((resolve, reject) => { 27 | zlib.gunzip(compressedData, (err, res) => { 28 | if (err) return reject(err); 29 | resolve(res); 30 | }); 31 | }); 32 | return decompressedData.toString("utf8"); 33 | } 34 | 35 | export function getCompressionResources() { 36 | return { 37 | "compression/v2/compressText": compress, 38 | "compression/v2/decompressText": decompress 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /source/search/groups.ts: -------------------------------------------------------------------------------- 1 | import { Group } from "../core/Group.js"; 2 | import { GroupID } from "../types.js"; 3 | 4 | /** 5 | * Find groups by their title 6 | * @param groups The groups to search through 7 | * @param title The title to search for. May be a regular expression. 8 | * @returns An array of found groups 9 | */ 10 | export function findGroupsByTitle(groups: Array, title: string | RegExp): Array { 11 | return groups.filter((group) => { 12 | if (title instanceof RegExp) { 13 | return title.test(group.getTitle()); 14 | } 15 | return group.getTitle().indexOf(title) >= 0; 16 | }); 17 | } 18 | 19 | export function getAllChildGroups(groups: Array, parentID: GroupID): Array { 20 | if (parentID === "0") return [...groups]; 21 | const getParentID = (g: Group) => g.vault.format.getItemParentID(g._source); 22 | return groups.filter((group) => { 23 | const thisParentID = getParentID(group); 24 | if (thisParentID === parentID) return true; 25 | let nextParent: Group; 26 | while ( 27 | (nextParent = nextParent ? nextParent.getParentGroup() : group.getParentGroup()) !== 28 | null 29 | ) { 30 | if (nextParent.id === parentID) return true; 31 | } 32 | return false; 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /test/unit/credentials/Credentials.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Credentials } from "../../../dist/node/index.js"; 3 | 4 | describe("credentials/Credentials", function () { 5 | beforeEach(function () { 6 | setClosedEnv(true); 7 | this.credentials = new Credentials( 8 | { 9 | datasource: { 10 | type: "text" 11 | } 12 | }, 13 | "test" 14 | ); 15 | }); 16 | 17 | afterEach(function () { 18 | setClosedEnv(false); 19 | }); 20 | 21 | describe("getCredentialsData", function () { 22 | it("returns stored data", function () { 23 | const data = this.credentials.getCredentialsData(); 24 | expect(data).to.deep.equal({ 25 | datasource: { 26 | type: "text" 27 | } 28 | }); 29 | }); 30 | }); 31 | 32 | describe("setCredentialsData", function () { 33 | it("updates stored data", function () { 34 | this.credentials.setCredentialsData({ 35 | datasource: { 36 | type: "file" 37 | } 38 | }); 39 | const data = this.credentials.getCredentialsData(); 40 | expect(data).to.deep.equal({ 41 | datasource: { 42 | type: "file" 43 | } 44 | }); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/unit/datasources/GoogleDriveDatasource.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Credentials, GoogleDriveDatasource } from "../../../dist/node/index.js"; 3 | import { getCredentials } from "../../../dist/node/credentials/memory/credentials.js"; 4 | 5 | describe("datasources/GoogleDriveDatasource", function () { 6 | beforeEach(function () { 7 | this.credentials = Credentials.fromDatasource( 8 | { 9 | type: "googledrive", 10 | token: "access-token", 11 | refreshToken: "refresh-token", 12 | fileID: "file-id" 13 | }, 14 | "test" 15 | ); 16 | }); 17 | 18 | it("can be instantiated", function () { 19 | const gdds = new GoogleDriveDatasource(this.credentials); 20 | expect(gdds).to.have.property("load").that.is.a("function"); 21 | expect(gdds).to.have.property("save").that.is.a("function"); 22 | }); 23 | 24 | describe("updateTokens", function () { 25 | beforeEach(function () { 26 | this.gdds = new GoogleDriveDatasource(this.credentials); 27 | }); 28 | 29 | it("updates the tokens on the instance credentials", function () { 30 | this.gdds.updateTokens("new-access", "new-refresh"); 31 | const raw = getCredentials(this.gdds.credentials.id); 32 | expect(raw.data.datasource.token).to.equal("new-access"); 33 | expect(raw.data.datasource.refreshToken).to.equal("new-refresh"); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /source/env/native/crypto.ts: -------------------------------------------------------------------------------- 1 | import { createAdapter } from "iocane"; 2 | import cryptoRandomString from "crypto-random-string"; 3 | import { CRYPTO_PBKDF2_ROUNDS, CRYPTO_RANDOM_STRING_CHARS } from "../core/constants.js"; 4 | 5 | let __derivationRoundsOverride = CRYPTO_PBKDF2_ROUNDS; 6 | 7 | function decryptData(data: string | Buffer, password: string): Promise { 8 | return createAdapter().decrypt(data, password) as Promise; 9 | } 10 | 11 | function encryptData(data: string | Buffer, password: string): Promise { 12 | const adapter = createAdapter(); 13 | if (__derivationRoundsOverride > 0) { 14 | adapter.setDerivationRounds(__derivationRoundsOverride); 15 | } 16 | return adapter.encrypt(data, password) as Promise; 17 | } 18 | 19 | export function getCryptoResources() { 20 | return { 21 | "crypto/v2/decryptBuffer": decryptData, 22 | "crypto/v2/encryptBuffer": encryptData, 23 | "crypto/v1/decryptText": decryptData, 24 | "crypto/v1/encryptText": encryptData, 25 | "crypto/v1/randomString": randomString, 26 | "crypto/v1/setDerivationRounds": setDerivationRounds 27 | }; 28 | } 29 | 30 | async function randomString(length: number): Promise { 31 | return cryptoRandomString({ 32 | length, 33 | characters: CRYPTO_RANDOM_STRING_CHARS 34 | }); 35 | } 36 | 37 | function setDerivationRounds(rounds: number = null) { 38 | __derivationRoundsOverride = !rounds ? CRYPTO_PBKDF2_ROUNDS : rounds; 39 | } 40 | -------------------------------------------------------------------------------- /source/search/entries.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from "../core/Entry.js"; 2 | import { Group } from "../core/Group.js"; 3 | import { GroupID } from "../types.js"; 4 | 5 | export function findEntriesByProperty( 6 | entries: Array, 7 | property: string | RegExp, 8 | value: string | RegExp 9 | ): Array { 10 | return entries.filter((entry) => { 11 | const props = entry.getProperties(property); 12 | const propKeys = Object.keys(props); 13 | return propKeys.length > 0 14 | ? propKeys.some((propKey) => { 15 | const itemValue = props[propKey]; 16 | if (value instanceof RegExp) { 17 | return value.test(itemValue); 18 | } else { 19 | return itemValue.indexOf(value) >= 0; 20 | } 21 | }) 22 | : false; 23 | }); 24 | } 25 | 26 | export function getAllChildEntries(entries: Array, parentGroupID: GroupID): Array { 27 | if (parentGroupID === "0") return [...entries]; 28 | return entries.filter((entry) => { 29 | const parentID = entry.vault.format.getItemParentID(entry._source); 30 | if (parentID === parentGroupID) return true; 31 | let nextParent: Group; 32 | while ( 33 | (nextParent = nextParent ? nextParent.getParentGroup() : entry.getGroup()) !== null 34 | ) { 35 | if (nextParent.id === parentGroupID) { 36 | return true; 37 | } 38 | } 39 | return false; 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /scripts/gen_vault_format_a.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { Credentials, FileDatasource, Vault, getSharedAppEnv, init } = require("../dist/index.node.js"); 3 | const packageInfo = require("../package.json"); 4 | 5 | const PASSWORD = "this is a long password used for a test archive!"; 6 | const { version } = packageInfo; 7 | 8 | const outputDir = path.resolve(__dirname, "../test/resources/vaults/format-a"); 9 | const outputFile = path.join(outputDir, `/test-archive-${version}.bcup`); 10 | 11 | init(); 12 | getSharedAppEnv().getProperty("crypto/v1/setDerivationRounds")(10); 13 | 14 | async function build() { 15 | console.log("Building vault..."); 16 | 17 | const vault = new Vault(); 18 | const mainGroup = vault.createGroup("test-group-main"); 19 | const mainEntry = mainGroup.createEntry("test-entry-main"); 20 | 21 | mainEntry.setProperty("username", "user123한@test.рф"); 22 | mainEntry.setProperty("password", "* পাসওয়ার্ড! "); 23 | mainEntry.setProperty("test-meta", "test-value 8"); 24 | 25 | // Datasource 26 | console.log("Saving vault: " + outputFile); 27 | const datasource = new FileDatasource(Credentials.fromDatasource({ 28 | path: outputFile 29 | }, PASSWORD)); 30 | await datasource.save(vault.format.history, Credentials.fromPassword(PASSWORD)); 31 | 32 | console.log("Test vault construction completed for version: " + version); 33 | } 34 | 35 | (async function run() { 36 | try { 37 | await build(); 38 | } catch (err) { 39 | console.error(err); 40 | process.exit(2); 41 | } 42 | })(); 43 | -------------------------------------------------------------------------------- /source/web/LocalStorageInterface.ts: -------------------------------------------------------------------------------- 1 | import { StorageInterface } from "../storage/StorageInterface.js"; 2 | 3 | export function getStorage(): Storage { 4 | return window.localStorage; 5 | } 6 | 7 | /** 8 | * Interface for localStorage 9 | * @augments StorageInterface 10 | */ 11 | export default class LocalStorageInterface extends StorageInterface { 12 | _storage: Storage = getStorage(); 13 | 14 | get storage() { 15 | return this._storage; 16 | } 17 | 18 | /** 19 | * Get all keys from storage 20 | * @returns A promise that resolves with an array of keys 21 | */ 22 | async getAllKeys() { 23 | return Object.keys(this.storage); 24 | } 25 | 26 | /** 27 | * Get the value of a key 28 | * @param name The key name 29 | * @returns A promise that resolves with the value 30 | */ 31 | async getValue(name: string): Promise { 32 | const value = this.storage.getItem(name); 33 | return value; 34 | } 35 | 36 | /** 37 | * Remove a key 38 | * @param name The key name 39 | * @returns A promise that resolves when the removal has completed 40 | */ 41 | async removeKey(name: string): Promise { 42 | this.storage.removeItem(name); 43 | } 44 | 45 | /** 46 | * Set the value for a key 47 | * @param name The key name 48 | * @param value The value to set 49 | * @returns A promise that resolves when the value is set 50 | */ 51 | async setValue(name: string, value: string): Promise { 52 | this.storage.setItem(name, value); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /source/io/formatB/signing.ts: -------------------------------------------------------------------------------- 1 | const BUTTERCUP_FORMAT = "buttercup/b"; 2 | const BUTTERCUP_SIGNATURE = "b~>" + BUTTERCUP_FORMAT; 3 | 4 | /** 5 | * Get the current signature 6 | * @returns The signature 7 | */ 8 | export function getSignature(): string { 9 | return BUTTERCUP_SIGNATURE; 10 | } 11 | 12 | /** 13 | * Detect if a string has a valid signature 14 | * @param text The text to check 15 | * @returns True if a valid signature is detected 16 | */ 17 | export function hasValidSignature(text: string): boolean { 18 | return text.indexOf(BUTTERCUP_SIGNATURE) === 0; 19 | } 20 | 21 | /** 22 | * Sign some text 23 | * @param text The text to sign 24 | * @returns The signed text 25 | */ 26 | export function sign(text: string): string { 27 | return hasValidSignature(text) ? text : BUTTERCUP_SIGNATURE + text; 28 | } 29 | 30 | /** 31 | * Strip the signature from some text 32 | * @param text The text to strip the signature from 33 | * @returns The text with the signature removed 34 | */ 35 | export function stripSignature(text: string): string { 36 | const sigLen = BUTTERCUP_SIGNATURE.length; 37 | return text.substr(sigLen, text.length - sigLen); 38 | } 39 | 40 | /** 41 | * Check if vault contents are in encrypted form 42 | * @param contents The vault contents 43 | * @returns True if encrypted, false otherwise 44 | */ 45 | export function vaultContentsEncrypted(contents: string): boolean { 46 | if (typeof contents === "string" && hasValidSignature(contents)) { 47 | const exSig = stripSignature(contents); 48 | return /["\s\t\n~]/.test(exSig) === false; 49 | } 50 | return false; 51 | } 52 | -------------------------------------------------------------------------------- /source/env/web/crypto.ts: -------------------------------------------------------------------------------- 1 | import { createAdapter } from "iocane/web"; 2 | import { CRYPTO_PBKDF2_ROUNDS, CRYPTO_RANDOM_STRING_CHARS } from "../core/constants.js"; 3 | 4 | const UINT16_MAX = 65535; 5 | 6 | let __derivationRoundsOverride = CRYPTO_PBKDF2_ROUNDS; 7 | 8 | function decryptData(data: string | ArrayBuffer, password): Promise { 9 | return createAdapter().decrypt(data, password); 10 | } 11 | 12 | function encryptData(data: string | ArrayBuffer, password): Promise { 13 | const adapter = createAdapter(); 14 | if (__derivationRoundsOverride > 0) { 15 | adapter.setDerivationRounds(__derivationRoundsOverride); 16 | } 17 | return adapter.encrypt(data, password); 18 | } 19 | 20 | export function getCryptoResources() { 21 | return { 22 | "crypto/v2/decryptBuffer": decryptData, 23 | "crypto/v2/encryptBuffer": encryptData, 24 | "crypto/v1/decryptText": decryptData, 25 | "crypto/v1/encryptText": encryptData, 26 | "crypto/v1/randomString": randomString, 27 | "crypto/v1/setDerivationRounds": setDerivationRounds 28 | }; 29 | } 30 | 31 | function randomString(length: number): Promise { 32 | return Promise.resolve().then(() => { 33 | const randCharsLen = CRYPTO_RANDOM_STRING_CHARS.length; 34 | const randArr = new Uint16Array(length); 35 | return randArr.reduce((output, nextVal) => { 36 | const ind = Math.floor((nextVal / UINT16_MAX) * randCharsLen); 37 | return `${output}${CRYPTO_RANDOM_STRING_CHARS[ind]}`; 38 | }, ""); 39 | }); 40 | } 41 | 42 | function setDerivationRounds(rounds: number = null) { 43 | __derivationRoundsOverride = !rounds ? CRYPTO_PBKDF2_ROUNDS : rounds; 44 | } 45 | -------------------------------------------------------------------------------- /source/tools/sharing.ts: -------------------------------------------------------------------------------- 1 | import { Group } from "../core/Group.js"; 2 | import { Vault } from "../core/Vault.js"; 3 | import { History } from "../types.js"; 4 | 5 | const SHARE_COMMAND_EXP = /^\$[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\s/; 6 | 7 | export function mergeHistories(initial, incoming) { 8 | // @todo Share merging 9 | return []; 10 | } 11 | 12 | /** 13 | * Move a group between archives 14 | * @param movingGroup The group to move 15 | * @param targetGroup The group to move to 16 | * @throws {Error} Throws if the remote type is not recognised 17 | */ 18 | export function moveGroupBetweenVaults(movingGroup: Group, targetGroup: Group | Vault) { 19 | // Clone 20 | const targetVault = targetGroup instanceof Vault ? (targetGroup as Vault) : targetGroup.vault; 21 | targetVault.format.cloneGroup( 22 | movingGroup._source, 23 | targetGroup instanceof Vault ? "0" : targetGroup.id 24 | ); 25 | // Delete original 26 | movingGroup.delete(/* skip trash */ true); 27 | } 28 | 29 | /** 30 | * Prepend the share prefix to every line that doesn't have it 31 | * @param history Array of history lines 32 | * @returns Prefixed history lines 33 | */ 34 | export function prependSharePrefix(history: History, shareID: string): History { 35 | return history.map((line) => (SHARE_COMMAND_EXP.test(line) ? line : `$${shareID} ${line}`)); 36 | } 37 | 38 | /** 39 | * Remove the share prefix to every line that has it 40 | * @param history Array of history lines 41 | * @returns Non-prefixed history lines 42 | */ 43 | export function removeSharePrefix(history: History): History { 44 | return history.map((line) => 45 | SHARE_COMMAND_EXP.test(line) ? line.replace(SHARE_COMMAND_EXP, "") : line 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /source/io/formatA/signing.ts: -------------------------------------------------------------------------------- 1 | const BUTTERCUP_FORMAT = "buttercup/a"; 2 | const BUTTERCUP_SIGNATURE = "b~>" + BUTTERCUP_FORMAT; 3 | 4 | /** 5 | * Get the current format 6 | * @returns The format 7 | */ 8 | export function getFormat(): string { 9 | return BUTTERCUP_FORMAT; 10 | } 11 | 12 | /** 13 | * Get the current signature 14 | * @returns The signature 15 | */ 16 | export function getSignature(): string { 17 | return BUTTERCUP_SIGNATURE; 18 | } 19 | 20 | /** 21 | * Detect if a string has a valid signature 22 | * @param text The text to check 23 | * @returns True if a valid signature is detected 24 | */ 25 | export function hasValidSignature(text: string): boolean { 26 | return text.indexOf(BUTTERCUP_SIGNATURE) === 0; 27 | } 28 | 29 | /** 30 | * Sign some text 31 | * @param text The text to sign 32 | * @returns The signed text 33 | */ 34 | export function sign(text: string): string { 35 | return hasValidSignature(text) ? text : BUTTERCUP_SIGNATURE + text; 36 | } 37 | 38 | /** 39 | * Strip the signature from some text 40 | * @param text The text to strip the signature from 41 | * @returns The text with the signature removed 42 | */ 43 | export function stripSignature(text: string): string { 44 | const sigLen = BUTTERCUP_SIGNATURE.length; 45 | return text.substr(sigLen, text.length - sigLen); 46 | } 47 | 48 | /** 49 | * Check if vault contents are in encrypted form 50 | * @param contents The vault contents 51 | * @returns True if encrypted, false otherwise 52 | */ 53 | export function vaultContentsEncrypted(contents: string): boolean { 54 | if (typeof contents === "string" && hasValidSignature(contents)) { 55 | const exSig = stripSignature(contents); 56 | return /["\s\t\n~]/.test(exSig) === false; 57 | } 58 | return false; 59 | } 60 | -------------------------------------------------------------------------------- /source/search/VaultEntrySearch.ts: -------------------------------------------------------------------------------- 1 | import { BaseSearch, ProcessedSearchEntry, SearcherFactory } from "./BaseSearch.js"; 2 | import { Vault } from "../core/Vault.js"; 3 | import { Entry } from "../core/Entry.js"; 4 | import { EntryURLType, getEntryURLs } from "../tools/entry.js"; 5 | import { StorageInterface } from "../storage/StorageInterface.js"; 6 | 7 | async function extractEntries( 8 | vault: Vault, 9 | memory: StorageInterface 10 | ): Promise> { 11 | // Get scores 12 | const scoresRaw = await memory.getValue(`bcup_search_${vault.id}`); 13 | let vaultScore = {}; 14 | if (scoresRaw) { 15 | try { 16 | const scores = JSON.parse(scoresRaw); 17 | vaultScore = scores; 18 | } catch (err) {} 19 | } 20 | // Get entries 21 | return vault 22 | .getAllEntries() 23 | .filter((entry: Entry) => entry.isInTrash() === false) 24 | .map((entry: Entry) => { 25 | const properties = entry.getProperties(); 26 | const urls = getEntryURLs(properties, EntryURLType.General); 27 | return { 28 | domainScores: vaultScore[entry.id] || {}, 29 | entryType: entry.getType(), 30 | groupID: entry.getGroup().id, 31 | id: entry.id, 32 | properties, 33 | tags: entry.getTags(), 34 | urls, 35 | vaultID: vault.id 36 | }; 37 | }); 38 | } 39 | 40 | export class VaultEntrySearch extends BaseSearch { 41 | constructor( 42 | vaults: Array, 43 | memory?: StorageInterface, 44 | searcherFactory?: SearcherFactory 45 | ) { 46 | super(vaults, extractEntries, memory, searcherFactory); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/integration/datasources/WebDAVDatasource.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { dirSync } from "tmp"; 3 | import { Credentials, Vault, WebDAVDatasource } from "../../../dist/node/index.js"; 4 | import { PASSWORD, PATH, PORT, USERNAME, createServer } from "../../resources/webdavServer.js"; 5 | 6 | describe("WebDAVDatasource", function () { 7 | beforeEach(async function () { 8 | // Server 9 | this.dir = dirSync().name; 10 | this.server = createServer(this.dir, "basic"); 11 | await this.server.start(); 12 | // Vault 13 | this.vault = Vault.createWithDefaults(); 14 | this.vault 15 | .findGroupsByTitle("General")[0] 16 | .createEntry("Test") 17 | .setProperty("username", "test"); 18 | const webdavURL = `http://localhost:${PORT}${PATH}`; 19 | this.datasource = new WebDAVDatasource( 20 | Credentials.fromDatasource( 21 | { 22 | endpoint: webdavURL, 23 | password: PASSWORD, 24 | path: "/test.bcup", 25 | username: USERNAME 26 | }, 27 | "test" 28 | ) 29 | ); 30 | }); 31 | 32 | afterEach(async function () { 33 | await this.server.stop(); 34 | }); 35 | 36 | it("supports saving and loading", async function () { 37 | await this.datasource.save(this.vault.format.history, Credentials.fromPassword("test")); 38 | const { Format, history } = await this.datasource.load(Credentials.fromPassword("test")); 39 | const vault = Vault.createFromHistory(history, Format); 40 | const entry = vault.findEntriesByProperty("title", "Test")[0]; 41 | expect(entry.getProperty("username")).to.equal("test"); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /source/storage/MemoryStorageInterface.ts: -------------------------------------------------------------------------------- 1 | import { StorageInterface } from "./StorageInterface.js"; 2 | 3 | /** 4 | * Storage interface for memory storage 5 | * @augments StorageInterface 6 | * @memberof module:Buttercup 7 | */ 8 | export class MemoryStorageInterface extends StorageInterface { 9 | _store: { [property: string]: string } = {}; 10 | 11 | /** 12 | * Get all keys from storage 13 | * @returns A promise that resolves with an array of keys 14 | * @memberof MemoryStorageInterface 15 | */ 16 | getAllKeys(): Promise> { 17 | return Promise.resolve(Object.keys(this._store)); 18 | } 19 | 20 | /** 21 | * Get the value of a key 22 | * @param name The key name 23 | * @returns A promise that resolves with the value 24 | * @memberof MemoryStorageInterface 25 | */ 26 | getValue(name: string): Promise { 27 | const value = this._store.hasOwnProperty(name) ? this._store[name] : null; 28 | return Promise.resolve(value); 29 | } 30 | 31 | /** 32 | * Remove a key 33 | * @param name The key name 34 | * @returns A promise that resolves when the removal has completed 35 | * @memberof MemoryStorageInterface 36 | */ 37 | removeKey(name: string): Promise { 38 | this._store[name] = undefined; 39 | delete this._store[name]; 40 | return Promise.resolve(); 41 | } 42 | 43 | /** 44 | * Set the value for a key 45 | * @param name The key name 46 | * @param value The value to set 47 | * @returns A promise that resolves when the value is set 48 | * @memberof MemoryStorageInterface 49 | */ 50 | setValue(name: string, value: string): Promise { 51 | this._store[name] = value; 52 | return Promise.resolve(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/gen_vault_format_b.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { Credentials, FileDatasource, Vault, VaultFormatB, getSharedAppEnv, init } = require("../dist/index.node.js"); 3 | const packageInfo = require("../package.json"); 4 | 5 | const PASSWORD = "this is a long password used for a test vault!"; 6 | const { version } = packageInfo; 7 | 8 | const outputDir = path.resolve(__dirname, "../test/resources/vaults/format-b"); 9 | const outputFile = path.join(outputDir, `/test-vault-${version}.bcup`); 10 | 11 | init(); 12 | getSharedAppEnv().getProperty("crypto/v1/setDerivationRounds")(10); 13 | 14 | async function build() { 15 | console.log("Building vault..."); 16 | 17 | const vault = new Vault(VaultFormatB); 18 | const mainGroup = vault.createGroup("test-group-main"); 19 | const mainEntry = mainGroup.createEntry("test-entry-main"); 20 | 21 | mainEntry.setProperty("username", "user123한@test.рф"); 22 | mainEntry.setProperty("password", "* পাসওয়ার্ড! "); 23 | mainEntry.setProperty("test-meta", "test-value 8"); 24 | mainEntry.setAttribute("ატრიბუტი", "ความคุ้มค่า"); 25 | 26 | mainGroup.setAttribute("MY ATTR", "ტესტი"); 27 | mainGroup.setAttribute("ატრიბუტი", "ความคุ้มค่า"); 28 | 29 | vault.setAttribute("ატრიბუტი", "ความคุ้มค่า"); 30 | 31 | // Datasource 32 | console.log("Saving vault: " + outputFile); 33 | const datasource = new FileDatasource(Credentials.fromDatasource({ 34 | path: outputFile 35 | }, PASSWORD)); 36 | await datasource.save(vault.format.history, Credentials.fromPassword(PASSWORD)); 37 | 38 | console.log(`Test vault construction completed for version: ${version} (format: B)`); 39 | } 40 | 41 | (async function run() { 42 | try { 43 | await build(); 44 | } catch (err) { 45 | console.error(err); 46 | process.exit(2); 47 | } 48 | })(); 49 | -------------------------------------------------------------------------------- /test/unit/tools/encoding.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | base64ToBytes, 4 | bytesToBase64, 5 | decodeBase64String, 6 | encodeBase64String 7 | } from "../../../dist/node/index.js"; 8 | 9 | const BASE64 = "VGVzdDogdGV4dCE="; 10 | const RAW = "Test: text!"; 11 | const RAW_BYTES = new Uint8Array([84, 101, 115, 116, 58, 32, 116, 101, 120, 116, 33]); 12 | 13 | function typedArraysAreEqual(a, b) { 14 | if (a.byteLength !== b.byteLength) return false; 15 | return a.every((val, i) => val === b[i]); 16 | } 17 | 18 | describe("tools/encoding", function () { 19 | describe("base64ToBytes", function () { 20 | it("converts base64 to a Uint8Array", function () { 21 | const bytes = base64ToBytes(BASE64); 22 | expect(Object.prototype.toString.call(bytes)).to.equal("[object Uint8Array]"); 23 | }); 24 | 25 | it("outputs correct bytes", function () { 26 | const bytes = base64ToBytes(BASE64); 27 | expect(typedArraysAreEqual(bytes, RAW_BYTES)).to.be.true; 28 | }); 29 | }); 30 | 31 | describe("bytesToBase64", function () { 32 | beforeEach(function () { 33 | const encoder = new TextEncoder(); 34 | this.bytes = encoder.encode(RAW); 35 | }); 36 | 37 | it("converts a Uint8Array to base64", function () { 38 | expect(bytesToBase64(this.bytes)).to.equal(BASE64); 39 | }); 40 | }); 41 | 42 | describe("decodeBase64String", function () { 43 | it("decodes base64", function () { 44 | expect(decodeBase64String(BASE64)).to.equal(RAW); 45 | }); 46 | }); 47 | 48 | describe("encodeBase64String", function () { 49 | it("encodes base64", function () { 50 | expect(encodeBase64String(RAW)).to.equal(BASE64); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/integration/facades/entry.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | EntryPropertyValueType, 4 | Vault, 5 | VaultFormatA, 6 | VaultFormatB, 7 | consumeEntryFacade, 8 | createEntryFacade, 9 | getEntryPropertyValueType, 10 | setEntryFacadePropertyValueType, 11 | setEntryPropertyValueType 12 | } from "../../../dist/node/index.js"; 13 | 14 | describe("Entry facades", function () { 15 | [ 16 | [VaultFormatA, "A"], 17 | [VaultFormatB, "B"] 18 | ].forEach(([Format, formatName]) => { 19 | describe(`Format: ${formatName}`, function () { 20 | beforeEach(function () { 21 | this.vault = new Vault(Format); 22 | this.group = this.vault.createGroup("group"); 23 | this.entry = this.group 24 | .createEntry("Entry A") 25 | .setProperty("username", "user@example.com") 26 | .setProperty("password", "passw0rd") 27 | .setAttribute("test_attr", "1234") 28 | .setAttribute("test_attr_2", "5678"); 29 | setEntryPropertyValueType(this.entry, "password", EntryPropertyValueType.Password); 30 | this.entryFacade = createEntryFacade(this.entry); 31 | }); 32 | 33 | it("sets the correct value type when changing via property field", function () { 34 | setEntryFacadePropertyValueType( 35 | this.entryFacade, 36 | "password", 37 | EntryPropertyValueType.Note 38 | ); 39 | consumeEntryFacade(this.entry, this.entryFacade); 40 | expect(getEntryPropertyValueType(this.entry, "password")).to.equal( 41 | EntryPropertyValueType.Note 42 | ); 43 | }); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/integration/datasources/MemoryDatasource.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { v4 as uuid } from "uuid"; 3 | import { Credentials, MemoryDatasource, Vault } from "../../../dist/node/index.js"; 4 | 5 | describe("MemoryDatasource", function () { 6 | beforeEach(function () { 7 | this.vault = Vault.createWithDefaults(); 8 | this.vault 9 | .findGroupsByTitle("General")[0] 10 | .createEntry("Test") 11 | .setProperty("username", "test"); 12 | this.mds = new MemoryDatasource( 13 | Credentials.fromDatasource({ 14 | property: `test${Math.floor(Math.random() * 10000)}` 15 | }) 16 | ); 17 | }); 18 | 19 | it("supports saving and loading", function () { 20 | return this.mds 21 | .save(this.vault.format.history, Credentials.fromPassword("test")) 22 | .then(() => this.mds.load(Credentials.fromPassword("test"))) 23 | .then(({ Format, history }) => Vault.createFromHistory(history, Format)) 24 | .then((vault) => { 25 | const entry = vault.findEntriesByProperty("title", "Test")[0]; 26 | expect(entry.getProperty("username")).to.equal("test"); 27 | }); 28 | }); 29 | 30 | it("supports writing and reading attachments", function () { 31 | const attachmentText = "This is a sample string,\nwith irrelevant contents."; 32 | const buff = Buffer.from(attachmentText); 33 | const vaultID = this.vault.id; 34 | const attachmentID = uuid(); 35 | return this.mds 36 | .putAttachment(vaultID, attachmentID, buff, Credentials.fromPassword("test")) 37 | .then(() => 38 | this.mds.getAttachment(vaultID, attachmentID, Credentials.fromPassword("test")) 39 | ) 40 | .then((buff2) => { 41 | expect(buff2.equals(buff)).to.be.true; 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/web/env/appEnv.spec.js: -------------------------------------------------------------------------------- 1 | import { getSharedAppEnv } from "../../../source/index.web.js"; 2 | 3 | describe("appEnv", function () { 4 | describe("compression", function () { 5 | describe("v1", function () { 6 | const compress = getSharedAppEnv().getProperty("compression/v1/compressText"); 7 | const decompress = getSharedAppEnv().getProperty("compression/v1/decompressText"); 8 | 9 | it("can compress and decompress text", function () { 10 | const compressed = compress("this is\nsome text! "); 11 | const decompressed = decompress(compressed); 12 | expect(decompressed).to.equal("this is\nsome text! "); 13 | }); 14 | 15 | it("can compress and decompress special characters", function () { 16 | const compressed = compress("ატრიბუტი\nความคุ้มค่า"); 17 | const decompressed = decompress(compressed); 18 | expect(decompressed).to.equal("ატრიბუტი\nความคุ้มค่า"); 19 | }); 20 | }); 21 | 22 | describe("v2", function () { 23 | const compress = getSharedAppEnv().getProperty("compression/v2/compressText"); 24 | const decompress = getSharedAppEnv().getProperty("compression/v2/decompressText"); 25 | 26 | it("can compress and decompress text", async function () { 27 | const compressed = await compress("this is\nsome text! "); 28 | const decompressed = await decompress(compressed); 29 | expect(decompressed).to.equal("this is\nsome text! "); 30 | }); 31 | 32 | it("can compress and decompress special characters", async function () { 33 | const compressed = await compress("ატრიბუტი\nความคุ้มค่า"); 34 | const decompressed = await decompress(compressed); 35 | expect(decompressed).to.equal("ატრიბუტი\nความคุ้มค่า"); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/integration/env/appEnv.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { getSharedAppEnv } from "../../../dist/node/index.js"; 3 | 4 | describe("appEnv", function () { 5 | describe("compression", function () { 6 | describe("v1", function () { 7 | const compress = getSharedAppEnv().getProperty("compression/v1/compressText"); 8 | const decompress = getSharedAppEnv().getProperty("compression/v1/decompressText"); 9 | 10 | it("can compress and decompress text", function () { 11 | const compressed = compress("this is\nsome text! "); 12 | const decompressed = decompress(compressed); 13 | expect(decompressed).to.equal("this is\nsome text! "); 14 | }); 15 | 16 | it("can compress and decompress special characters", function () { 17 | const compressed = compress("ატრიბუტი\nความคุ้มค่า"); 18 | const decompressed = decompress(compressed); 19 | expect(decompressed).to.equal("ატრიბუტი\nความคุ้มค่า"); 20 | }); 21 | }); 22 | 23 | describe("v2", function () { 24 | const compress = getSharedAppEnv().getProperty("compression/v2/compressText"); 25 | const decompress = getSharedAppEnv().getProperty("compression/v2/decompressText"); 26 | 27 | it("can compress and decompress text", async function () { 28 | const compressed = await compress("this is\nsome text! "); 29 | const decompressed = await decompress(compressed); 30 | expect(decompressed).to.equal("this is\nsome text! "); 31 | }); 32 | 33 | it("can compress and decompress special characters", async function () { 34 | const compressed = await compress("ატრიბუტი\nความคุ้มค่า"); 35 | const decompressed = await decompress(compressed); 36 | expect(decompressed).to.equal("ატრიბუტი\nความคุ้มค่า"); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/unit/facades/tools.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | Entry, 4 | EntryPropertyValueType, 5 | EntryType, 6 | Vault, 7 | createFieldDescriptor 8 | } from "../../../dist/node/index.js"; 9 | 10 | describe("facades/tools", function () { 11 | beforeEach(function () { 12 | const vault = new Vault(); 13 | this.entry = vault.createGroup("test").createEntry("Email"); 14 | this.entry.setProperty("username", "user@email.com"); 15 | this.entry.setAttribute("testAttribute", "testValue"); 16 | }); 17 | 18 | describe("createFieldDescriptor", function () { 19 | it("supports outputting empty structures", function () { 20 | const obj = createFieldDescriptor(null, "Test", "property", "test"); 21 | expect(obj).to.be.an("object"); 22 | expect(obj).to.have.property("title", "Test"); 23 | expect(obj).to.have.property("propertyType", "property"); 24 | expect(obj).to.have.property("property", "test"); 25 | expect(obj).to.have.property("value", ""); 26 | expect(obj).to.have.property("valueType", EntryPropertyValueType.Text); 27 | }); 28 | 29 | it("supports taking the value from an Entry", function () { 30 | const obj = createFieldDescriptor(this.entry, "Username", "property", "username"); 31 | expect(obj).to.have.property("title", "Username"); 32 | expect(obj).to.have.property("propertyType", "property"); 33 | expect(obj).to.have.property("property", "username"); 34 | expect(obj).to.have.property("value", "user@email.com"); 35 | }); 36 | 37 | it("outputs valueType when set", function () { 38 | this.entry.setAttribute(`${Entry.Attributes.FieldTypePrefix}username`, EntryType.Note); 39 | const obj = createFieldDescriptor(this.entry, "Username", "property", "username"); 40 | expect(obj).to.have.property("valueType", EntryType.Note); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/resources/webdavServer.js: -------------------------------------------------------------------------------- 1 | import { v2 as ws } from "webdav-server"; 2 | 3 | export const PASSWORD = "pass"; 4 | export const PATH = "/webdav/server"; 5 | export const PORT = 9989; 6 | export const USERNAME = "user"; 7 | 8 | export function createServer(dir, authType /* "basic" | "digest" */) { 9 | if (!dir) { 10 | throw new Error("Expected target directory"); 11 | } 12 | const userManager = new ws.SimpleUserManager(); 13 | const user = userManager.addUser(USERNAME, PASSWORD); 14 | let auth; 15 | switch (authType) { 16 | case "digest": 17 | auth = new ws.HTTPDigestAuthentication(userManager, "test"); 18 | break; 19 | case "basic": 20 | /* falls-through */ 21 | default: 22 | auth = new ws.HTTPBasicAuthentication(userManager); 23 | break; 24 | } 25 | const privilegeManager = new ws.SimplePathPrivilegeManager(); 26 | privilegeManager.setRights(user, "/", ["all"]); 27 | const server = new ws.WebDAVServer({ 28 | port: PORT, 29 | httpAuthentication: auth, 30 | privilegeManager: privilegeManager, 31 | maxRequestDepth: Infinity, 32 | headers: { 33 | "Access-Control-Allow-Origin": "*", 34 | "Access-Control-Allow-Methods": 35 | "HEAD, GET, PUT, PROPFIND, DELETE, OPTIONS, MKCOL, MOVE, COPY", 36 | "Access-Control-Allow-Headers": 37 | "Accept, Authorization, Content-Type, Content-Length, Depth" 38 | } 39 | }); 40 | // console.log(`Created server on localhost with port: 9988, and authType: ${authType}`); 41 | return { 42 | start: function start() { 43 | return new Promise(function (resolve) { 44 | server.setFileSystem(PATH, new ws.PhysicalFileSystem(dir), function () { 45 | server.start(resolve); 46 | }); 47 | }); 48 | }, 49 | 50 | stop: function stop() { 51 | return new Promise(function (resolve) { 52 | server.stop(resolve); 53 | }); 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /source/io/formatA/merge.ts: -------------------------------------------------------------------------------- 1 | import { History } from "../../types.js"; 2 | 3 | const ENTRY_SPECIFIC_TARGET_COMMANDS = { 4 | cen: 1, // entry ID is argument 2 5 | den: 0, 6 | dea: 0, 7 | dem: 0, 8 | dep: 0, 9 | men: 0, 10 | sea: 0, 11 | sem: 0, 12 | sep: 0 13 | }; 14 | const GROUP_SPECIFIC_TARGET_COMMANDS = { 15 | cgr: 1, // group ID is argument 2 16 | dgr: 0, 17 | dga: 0, 18 | mgr: 0, 19 | sga: 0, 20 | tgr: 0 21 | }; 22 | 23 | /** 24 | * Process history in order, following which entries/groups are 25 | * deleted and them removing future references to them 26 | * @param history The history set to filter 27 | * @returns A filtered set of history, with problematic removals 28 | * stripped out 29 | */ 30 | export function smartStripRemovedAssets(history: History): History { 31 | const deletedEntries: Set = new Set([]); 32 | const deletedGroups: Set = new Set([]); 33 | return history.filter((line) => { 34 | const [command, ...args] = line.split(/\s/g); 35 | if ( 36 | typeof ENTRY_SPECIFIC_TARGET_COMMANDS[command] === "number" && 37 | deletedEntries.has(args[ENTRY_SPECIFIC_TARGET_COMMANDS[command]]) 38 | ) { 39 | return false; 40 | } 41 | if ( 42 | typeof GROUP_SPECIFIC_TARGET_COMMANDS[command] === "number" && 43 | deletedGroups.has(args[GROUP_SPECIFIC_TARGET_COMMANDS[command]]) 44 | ) { 45 | return false; 46 | } 47 | if (command === "den") { 48 | const [entryID] = args; 49 | deletedEntries.add(entryID); 50 | } else if (command === "dgr") { 51 | const [groupID] = args; 52 | deletedGroups.add(groupID); 53 | } else if (command === "men" || command === "mgr") { 54 | const [targetGroupID] = args; 55 | if (deletedGroups.has(targetGroupID)) return false; 56 | } else if (command === "cen" || command === "cgr") { 57 | const [targetGroupID] = args; 58 | if (deletedGroups.has(targetGroupID)) return false; 59 | } 60 | return true; 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /source/facades/symbols.ts: -------------------------------------------------------------------------------- 1 | import { EntryPropertyValueType, EntryType } from "../types.js"; 2 | 3 | export type EntryPropertyTypeIndex = { 4 | [key in EntryPropertyValueType]: { 5 | title: string; 6 | slug: EntryPropertyValueType; 7 | }; 8 | }; 9 | 10 | export type EntryTypeIndex = { 11 | [key in EntryType]: { 12 | title: string; 13 | slug: EntryType; 14 | }; 15 | }; 16 | 17 | /** 18 | * Default entry type 19 | * @memberof module:Buttercup 20 | */ 21 | export const DEFAULT_ENTRY_TYPE = EntryType.Login; 22 | /** 23 | * Default entry field type 24 | * @memberof module:Buttercup 25 | */ 26 | export const DEFAULT_FIELD_TYPE = EntryPropertyValueType.Text; 27 | 28 | /** 29 | * Entry types collection (all available) 30 | * @memberof module:Buttercup 31 | */ 32 | export const ENTRY_TYPES: EntryTypeIndex = { 33 | [EntryType.CreditCard]: { 34 | title: "Credit Card", 35 | slug: EntryType.CreditCard 36 | }, 37 | [EntryType.Login]: { 38 | title: "Login", 39 | slug: EntryType.Login 40 | }, 41 | [EntryType.Note]: { 42 | title: "Note", 43 | slug: EntryType.Note 44 | }, 45 | [EntryType.SSHKey]: { 46 | title: "SSH Key Pair", 47 | slug: EntryType.SSHKey 48 | }, 49 | [EntryType.Website]: { 50 | title: "Website Login", 51 | slug: EntryType.Website 52 | } 53 | }; 54 | 55 | export const FACADE_VERSION = 2; 56 | 57 | /** 58 | * Entry field value types collection (all available) 59 | * @memberof module:Buttercup 60 | */ 61 | export const FIELD_VALUE_TYPES: EntryPropertyTypeIndex = { 62 | [EntryPropertyValueType.Note]: { 63 | title: "Note", 64 | slug: EntryPropertyValueType.Note 65 | }, 66 | [EntryPropertyValueType.OTP]: { 67 | title: "OTP (One Time Password)", 68 | slug: EntryPropertyValueType.OTP 69 | }, 70 | [EntryPropertyValueType.Password]: { 71 | title: "Password (secret)", 72 | slug: EntryPropertyValueType.Password 73 | }, 74 | [EntryPropertyValueType.Text]: { 75 | title: "Text (default)", 76 | slug: EntryPropertyValueType.Text 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /source/search/VaultFacadeEntrySearch.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from "../core/Entry.js"; 2 | import { BaseSearch, ProcessedSearchEntry, SearcherFactory } from "./BaseSearch.js"; 3 | import { EntryURLType, getEntryURLs } from "../tools/entry.js"; 4 | import { fieldsToAttributes, fieldsToProperties } from "../facades/entry.js"; 5 | import { StorageInterface } from "../storage/StorageInterface.js"; 6 | import { EntryFacade, VaultFacade } from "../types.js"; 7 | import { isValidTag } from "../tools/tag.js"; 8 | 9 | async function extractEntries( 10 | facade: VaultFacade, 11 | memory: StorageInterface 12 | ): Promise> { 13 | // Get scores 14 | const scoresRaw = await memory.getValue(`bcup_search_${facade.id}`); 15 | let vaultScore = {}; 16 | if (scoresRaw) { 17 | try { 18 | const scores = JSON.parse(scoresRaw); 19 | vaultScore = scores; 20 | } catch (err) {} 21 | } 22 | // Get entries 23 | return facade.entries.reduce((entries: Array, nextEntry: EntryFacade) => { 24 | // @todo in trash 25 | const attributes = fieldsToAttributes(nextEntry.fields); 26 | const tags = attributes[Entry.Attributes.Tags] 27 | ? attributes[Entry.Attributes.Tags].split(",").reduce((output, tag) => { 28 | return isValidTag(tag) ? [...output, tag] : output; 29 | }, []) 30 | : []; 31 | const properties = fieldsToProperties(nextEntry.fields); 32 | const urls = getEntryURLs(properties, EntryURLType.General); 33 | entries.push({ 34 | domainScores: vaultScore[nextEntry.id] || {}, 35 | entryType: nextEntry.type, 36 | groupID: nextEntry.parentID, 37 | id: nextEntry.id, 38 | properties, 39 | tags, 40 | urls, 41 | vaultID: facade.id 42 | }); 43 | return entries; 44 | }, []); 45 | } 46 | 47 | export class VaultFacadeEntrySearch extends BaseSearch { 48 | constructor( 49 | facades: Array, 50 | memory?: StorageInterface, 51 | searcherFactory?: SearcherFactory 52 | ) { 53 | super(facades, extractEntries, memory, searcherFactory); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/integration/datasources/FileDatasource.spec.js: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { expect } from "chai"; 3 | import { v4 as uuid } from "uuid"; 4 | import { dir } from "tmp"; 5 | import { Credentials, FileDatasource, Vault } from "../../../dist/node/index.js"; 6 | 7 | describe("FileDatasource", function () { 8 | beforeEach(function (done) { 9 | this.vault = Vault.createWithDefaults(); 10 | this.vault 11 | .findGroupsByTitle("General")[0] 12 | .createEntry("Test") 13 | .setProperty("username", "test"); 14 | dir((err, dirPath, cleanup) => { 15 | if (err) return done(err); 16 | this.fds = new FileDatasource( 17 | Credentials.fromDatasource({ 18 | path: join(dirPath, "vault.bcup") 19 | }) 20 | ); 21 | this.cleanup = cleanup; 22 | done(); 23 | }); 24 | }); 25 | 26 | afterEach(function () { 27 | this.cleanup(); 28 | }); 29 | 30 | it("supports saving and loading", function () { 31 | return this.fds 32 | .save(this.vault.format.history, Credentials.fromPassword("test")) 33 | .then(() => this.fds.load(Credentials.fromPassword("test"))) 34 | .then(({ Format, history }) => Vault.createFromHistory(history, Format)) 35 | .then((vault) => { 36 | const entry = vault.findEntriesByProperty("title", "Test")[0]; 37 | expect(entry.getProperty("username")).to.equal("test"); 38 | }); 39 | }); 40 | 41 | it("supports writing and reading attachments", function () { 42 | const attachmentText = "This is a sample string,\nwith irrelevant contents."; 43 | const buff = Buffer.from(attachmentText); 44 | const vaultID = this.vault.id; 45 | const attachmentID = uuid(); 46 | return this.fds 47 | .putAttachment(vaultID, attachmentID, buff, Credentials.fromPassword("test")) 48 | .then(() => 49 | this.fds.getAttachment(vaultID, attachmentID, Credentials.fromPassword("test")) 50 | ) 51 | .then((buff2) => { 52 | expect(buff2.equals(buff)).to.be.true; 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const exec = require("child_process").exec; 3 | const { DefinePlugin } = require("webpack"); 4 | const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); 5 | const ResolveTypeScriptPlugin = require("resolve-typescript-plugin"); 6 | 7 | const SOURCE = path.resolve(__dirname, "./source"); 8 | const WEB_ENTRY = path.join(SOURCE, "index.web.ts"); 9 | const DIST = path.resolve(__dirname, "./dist/web"); 10 | 11 | const plugins = [ 12 | new DefinePlugin({ 13 | BUTTERCUP_WEB: true 14 | }) 15 | ]; 16 | if (process.env.ANALYSE === "bundle") { 17 | plugins.push(new BundleAnalyzerPlugin()); 18 | } 19 | 20 | const baseConfig = { 21 | devtool: false, 22 | 23 | entry: WEB_ENTRY, 24 | 25 | experiments: { 26 | outputModule: true 27 | }, 28 | 29 | externalsType: "module", 30 | 31 | module: { 32 | rules: [ 33 | { 34 | test: /(iconv-loader|bluebird|form-data)/, 35 | use: "null-loader" 36 | }, 37 | { 38 | test: /\.ts$/, 39 | exclude: /node_modules/, 40 | use: { 41 | loader: "ts-loader", 42 | options: { 43 | configFile: path.resolve(__dirname, "./tsconfig.web.json"), 44 | onlyCompileBundledFiles: true 45 | } 46 | } 47 | } 48 | ] 49 | }, 50 | 51 | output: { 52 | path: DIST, 53 | filename: "index.js", 54 | environment: { 55 | module: true 56 | }, 57 | library: { 58 | type: "module" 59 | } 60 | }, 61 | 62 | plugins, 63 | 64 | resolve: { 65 | extensions: [".js"], 66 | fallback: { 67 | crypto: false, 68 | dns: false, 69 | fs: false, 70 | http: false, 71 | https: false, 72 | net: false, 73 | path: false, 74 | stream: false, 75 | util: false 76 | }, 77 | plugins: [ 78 | new ResolveTypeScriptPlugin() 79 | ] 80 | }, 81 | 82 | target: "web" 83 | }; 84 | 85 | module.exports = baseConfig; 86 | -------------------------------------------------------------------------------- /source/tools/vaultSearch.ts: -------------------------------------------------------------------------------- 1 | import { Group } from "../core/Group.js"; 2 | import { Entry } from "../core/Entry.js"; 3 | 4 | /** 5 | * Find entry instances by filtering with a compare function 6 | * @param groups The groups to check in 7 | * @param compareFn The callback comparison function, return true to keep and false 8 | * to strip 9 | * @returns An array of found entries 10 | */ 11 | export function findEntriesByCheck( 12 | groups: Array, 13 | compareFn: (entry: Entry) => boolean 14 | ): Array { 15 | let foundEntries = [], 16 | newEntries; 17 | groups.forEach((group: Group) => { 18 | newEntries = group.getEntries().filter(compareFn); 19 | if (newEntries.length > 0) { 20 | foundEntries = foundEntries.concat(newEntries); 21 | } 22 | newEntries = findEntriesByCheck(group.getGroups(), compareFn); 23 | if (newEntries.length > 0) { 24 | foundEntries = foundEntries.concat(newEntries); 25 | } 26 | }); 27 | return foundEntries; 28 | } 29 | 30 | /** 31 | * Find group instances within groups that satisfy some check 32 | * @param groups The groups to check within 33 | * @param compareFn A comparision function - return true to keep, false to strip 34 | * @returns An array of found groups 35 | */ 36 | export function findGroupsByCheck( 37 | groups: Array, 38 | compareFn: (group: Group) => boolean 39 | ): Array { 40 | let foundGroups = groups.filter(compareFn); 41 | groups.forEach((group: Group) => { 42 | const subFound = findGroupsByCheck(group.getGroups(), compareFn); 43 | if (subFound.length > 0) { 44 | foundGroups = foundGroups.concat(subFound); 45 | } 46 | }); 47 | return foundGroups; 48 | } 49 | 50 | /** 51 | * Get all entries within a collection of groups 52 | * @param groups An array of groups 53 | * @returns An array of entries 54 | */ 55 | export function getAllEntries(groups: Array): Array { 56 | return groups.reduce((current: Array, group: Group) => { 57 | const theseEntries = group.getEntries(); 58 | const subEntries = getAllEntries(group.getGroups()); 59 | if (theseEntries.length > 0) { 60 | current = current.concat(theseEntries); 61 | } 62 | if (subEntries.length > 0) { 63 | current = current.concat(subEntries); 64 | } 65 | return current; 66 | }, []); 67 | } 68 | -------------------------------------------------------------------------------- /test/integration/formatAToBConversion.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | Credentials, 4 | TextDatasource, 5 | Vault, 6 | VaultFormatA, 7 | VaultFormatID, 8 | VaultManager, 9 | VaultSource, 10 | setDefaultFormat 11 | } from "../../dist/node/index.js"; 12 | 13 | async function createTextSourceCredentials() { 14 | const vault = new Vault(); 15 | const general = vault.createGroup("General"); 16 | const loginEntry = general 17 | .createEntry("Login") 18 | .setProperty("username", "user") 19 | .setProperty("password", "test"); 20 | loginEntry.setProperty("password", "passw0rd"); 21 | const tds = new TextDatasource(Credentials.fromDatasource({ type: "text" }, "test")); 22 | const encrypted = await tds.save(vault.format.getHistory(), Credentials.fromPassword("test")); 23 | return Credentials.fromDatasource( 24 | { 25 | type: "text", 26 | content: encrypted 27 | }, 28 | "test" 29 | ); 30 | } 31 | 32 | describe("vault conversion: A to B", function () { 33 | beforeEach(async function () { 34 | setDefaultFormat(VaultFormatA); 35 | this.sourceCredentials = await createTextSourceCredentials(); 36 | this.vaultSource = new VaultSource( 37 | "Test", 38 | "text", 39 | await this.sourceCredentials.toSecureString() 40 | ); 41 | this.vaultManager = new VaultManager(); 42 | await this.vaultManager.addSource(this.vaultSource); 43 | // Unlock 44 | await this.vaultSource.unlock(Credentials.fromPassword("test")); 45 | // Convert 46 | await this.vaultSource.convert(VaultFormatID.B); 47 | }); 48 | 49 | it("retains groups", function () { 50 | const [generalGroup] = this.vaultSource.vault.findGroupsByTitle("General"); 51 | expect(generalGroup.getTitle()).to.equal("General"); 52 | }); 53 | 54 | it("retains entries", function () { 55 | const [loginEntry] = this.vaultSource.vault.findEntriesByProperty("title", "Login"); 56 | expect(loginEntry.getProperty("username")).to.equal("user"); 57 | }); 58 | 59 | it("retains entry property history", function () { 60 | const [loginEntry] = this.vaultSource.vault.findEntriesByProperty("title", "Login"); 61 | const changes = loginEntry.getChanges().filter((change) => change.property === "password"); 62 | expect(changes).to.have.lengthOf(2); 63 | expect(changes.map((change) => change.value)).to.deep.equal(["test", "passw0rd"]); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/web/MemoryDatasource.spec.js: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from "uuid"; 2 | import { Credentials, MemoryDatasource, Vault } from "../../source/index.web.js"; 3 | 4 | function arrayBuffersAreEqual(a, b) { 5 | return dataViewsAreEqual(new DataView(a), new DataView(b)); 6 | } 7 | 8 | function dataViewsAreEqual(a, b) { 9 | if (a.byteLength !== b.byteLength) return false; 10 | for (let i = 0; i < a.byteLength; i++) { 11 | if (a.getUint8(i) !== b.getUint8(i)) return false; 12 | } 13 | return true; 14 | } 15 | 16 | function stringToArrayBuffer(str) { 17 | var buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char 18 | var bufView = new Uint16Array(buf); 19 | for (var i = 0, strLen = str.length; i < strLen; i++) { 20 | bufView[i] = str.charCodeAt(i); 21 | } 22 | return buf; 23 | } 24 | 25 | describe("MemoryDatasource", function () { 26 | beforeEach(function () { 27 | this.vault = Vault.createWithDefaults(); 28 | this.vault 29 | .findGroupsByTitle("General")[0] 30 | .createEntry("Test") 31 | .setProperty("username", "test"); 32 | this.mds = new MemoryDatasource( 33 | Credentials.fromDatasource({ 34 | property: `test${Math.floor(Math.random() * 10000)}` 35 | }) 36 | ); 37 | }); 38 | 39 | it("supports saving and loading", function () { 40 | return this.mds 41 | .save(this.vault.format.history, Credentials.fromPassword("test")) 42 | .then(() => this.mds.load(Credentials.fromPassword("test"))) 43 | .then(({ Format, history }) => Vault.createFromHistory(history, Format)) 44 | .then((vault) => { 45 | const entry = vault.findEntriesByProperty("title", "Test")[0]; 46 | expect(entry.getProperty("username")).to.equal("test"); 47 | }); 48 | }); 49 | 50 | it("supports writing and reading attachments", function () { 51 | const attachmentText = "This is a sample string,\nwith irrelevant contents."; 52 | const buff = stringToArrayBuffer(attachmentText); 53 | const vaultID = this.vault.id; 54 | const attachmentID = uuid(); 55 | return this.mds 56 | .putAttachment(vaultID, attachmentID, buff, Credentials.fromPassword("test")) 57 | .then(() => 58 | this.mds.getAttachment(vaultID, attachmentID, Credentials.fromPassword("test")) 59 | ) 60 | .then((buff2) => { 61 | expect(arrayBuffersAreEqual(buff, buff2)).to.be.true; 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /source/tools/entry.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from "../core/Entry.js"; 2 | import { Group } from "../core/Group.js"; 3 | import { objectValues } from "./polyfill.js"; 4 | import { GroupID } from "../types.js"; 5 | 6 | export enum EntryURLType { 7 | Any = "any", 8 | General = "general", 9 | Icon = "icon", 10 | Login = "login" 11 | } 12 | 13 | const URL_PROP = /(^|[a-zA-Z0-9_-]|\b)(ur[li]|UR[LI]|Ur[li])(\b|$|[_-])/; 14 | const URL_PROP_ICON = /icon[\s_-]*ur[li]/i; 15 | 16 | export function getEntryPath(entry: Entry): Array { 17 | let lastParent: Group = null; 18 | const path: Array = []; 19 | do { 20 | lastParent = lastParent ? lastParent.getParentGroup() : entry.getGroup(); 21 | if (lastParent) { 22 | path.unshift(lastParent.id); 23 | } 24 | } while (lastParent); 25 | return path; 26 | } 27 | 28 | /** 29 | * Get URLs from an entry's properties 30 | * Allows for preferential sorting 31 | * @param properties The entry properties 32 | * @param preference Optional URL type preference 33 | */ 34 | export function getEntryURLs( 35 | properties: { [key: string]: string }, 36 | preference: EntryURLType = EntryURLType.Any 37 | ): Array { 38 | const urlRef = Object.keys(properties) 39 | .filter((key) => URL_PROP.test(key)) 40 | .reduce( 41 | (output, nextKey) => 42 | Object.assign(output, { 43 | [nextKey]: properties[nextKey] 44 | }), 45 | {} 46 | ); 47 | if (preference === EntryURLType.General || preference === EntryURLType.Login) { 48 | return Object.keys(urlRef) 49 | .sort((a, b) => { 50 | if (preference === EntryURLType.General) { 51 | const general = /^ur[li]$/i; 52 | const aVal = general.test(a) ? 1 : 0; 53 | const bVal = general.test(b) ? 1 : 0; 54 | return bVal - aVal; 55 | } else if (preference === EntryURLType.Login) { 56 | const login = /login/i; 57 | const aVal = login.test(a) ? 1 : 0; 58 | const bVal = login.test(b) ? 1 : 0; 59 | return bVal - aVal; 60 | } 61 | return 0; 62 | }) 63 | .map((key) => urlRef[key]); 64 | } else if (preference === EntryURLType.Icon) { 65 | const iconProp = Object.keys(urlRef).find((key) => URL_PROP_ICON.test(key)); 66 | return iconProp ? [urlRef[iconProp]] : []; 67 | } 68 | // Default is "any" URLs 69 | return objectValues(urlRef); 70 | } 71 | -------------------------------------------------------------------------------- /karma.conf.cjs: -------------------------------------------------------------------------------- 1 | const webpackConfig = require("./webpack.config.cjs"); 2 | 3 | const CI = !!process.env.CI; 4 | 5 | delete webpackConfig.entry; 6 | delete webpackConfig.output; 7 | webpackConfig.mode = process.env.BUNDLE === "production" ? "production" : "development"; 8 | webpackConfig.module.rules.push({ 9 | test: /\.(png|jpg)$/, 10 | use: "arraybuffer-loader" 11 | }); 12 | // BEGIN typescript support 13 | webpackConfig.module.rules.unshift({ 14 | test: /\.js$/, 15 | use: [{ 16 | loader: "babel-loader", 17 | options: { 18 | "presets": [ 19 | ["@babel/preset-env", { 20 | "useBuiltIns": false, 21 | "targets": { 22 | "firefox": "70" 23 | } 24 | }] 25 | ], 26 | "plugins": [ 27 | "@babel/plugin-proposal-class-properties", 28 | "@babel/plugin-proposal-object-rest-spread" 29 | ] 30 | } 31 | }] 32 | }); 33 | const tsRule = webpackConfig.module.rules.find(rule => rule.test && /\.ts/.test(rule.test.toString())); 34 | tsRule.use.options.configFile = tsRule.use.options.configFile.replace("tsconfig.web.json", "tsconfig.web.test.json"); 35 | // END typescript support 36 | 37 | module.exports = config => config.set({ 38 | 39 | basePath: __dirname, 40 | 41 | browsers: CI ? ["ChromeHeadless", "FirefoxHeadless"] : ["ChromeHeadless"], 42 | 43 | captureTimeout: 60000, 44 | 45 | colors: true, 46 | 47 | // coverageReporter: { 48 | // dir: "build/coverage/", 49 | // reporters: [ 50 | // { type: "html" }, 51 | // { type: "text" }, 52 | // { type: "text-summary" } 53 | // ] 54 | // }, 55 | 56 | exclude: [], 57 | 58 | files: [ 59 | { pattern: "source/index.web.ts" }, 60 | { pattern: "test/web/index.js" }, 61 | { pattern: "test/web/**/*.spec.js" } 62 | ], 63 | 64 | frameworks: ["mocha", "sinon", "webpack"], 65 | 66 | // logLevel: config.LOG_DEBUG, 67 | 68 | plugins: [ 69 | require("karma-webpack"), 70 | require("karma-chrome-launcher"), 71 | require("karma-firefox-launcher"), 72 | require("karma-mocha"), 73 | require("karma-sinon"), 74 | require("karma-spec-reporter") 75 | ], 76 | 77 | preprocessors: { 78 | "*.ts": ["webpack"], 79 | "test/web/index.js": ["webpack"], 80 | "test/web/**/*.spec.js": ["webpack"] 81 | }, 82 | 83 | reporters: ["spec", "progress"], 84 | 85 | singleRun: true, 86 | 87 | webpack: webpackConfig 88 | 89 | }); 90 | -------------------------------------------------------------------------------- /test/integration/formatA.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | Credentials, 4 | MemoryDatasource, 5 | Vault, 6 | VaultFormatA, 7 | setDefaultFormat 8 | } from "../../dist/node/index.js"; 9 | 10 | describe("Format A", function () { 11 | function reduceCommandsToPadIDs(output, cmd) { 12 | const [method, id] = cmd.split(" "); 13 | if (method.toLowerCase() === "pad") { 14 | output.push(id); 15 | } 16 | return output; 17 | } 18 | 19 | beforeEach(function () { 20 | setDefaultFormat(VaultFormatA); 21 | }); 22 | 23 | afterEach(function () { 24 | setDefaultFormat(); 25 | }); 26 | 27 | it("retains PAD IDs between loads", async function () { 28 | let vault = new Vault(); 29 | vault.createGroup("General").createGroup("Test").createGroup("Sub"); 30 | const memStore = `formatATest-${Math.random()}`; 31 | const mds = new MemoryDatasource( 32 | Credentials.fromDatasource( 33 | { 34 | property: memStore 35 | }, 36 | "test" 37 | ) 38 | ); 39 | const idSet1 = vault.format.getHistory().reduce(reduceCommandsToPadIDs, []); 40 | await mds.save(vault.format.getHistory(), Credentials.fromPassword("test")); 41 | const { history } = await mds.load(Credentials.fromPassword("test")); 42 | vault = Vault.createFromHistory(history, VaultFormatA); 43 | const idSet2 = vault.format.getHistory().reduce(reduceCommandsToPadIDs, []); 44 | expect(idSet1).to.have.length.above(0); 45 | expect(idSet1).to.deep.equal(idSet2); 46 | }); 47 | 48 | it("retains PAD IDs between saves", async function () { 49 | let vault = new Vault(); 50 | vault.createGroup("General").createGroup("Test").createGroup("Sub"); 51 | const memStore = `formatATest-${Math.random()}`; 52 | const mds = new MemoryDatasource( 53 | Credentials.fromDatasource( 54 | { 55 | property: memStore 56 | }, 57 | "test" 58 | ) 59 | ); 60 | const idSet1 = vault.format.getHistory().reduce(reduceCommandsToPadIDs, []); 61 | vault.createGroup("Another"); 62 | await mds.save(vault.format.getHistory(), Credentials.fromPassword("test")); 63 | const { history } = await mds.load(Credentials.fromPassword("test")); 64 | vault = Vault.createFromHistory(history, VaultFormatA); 65 | const idSet2 = vault.format.getHistory().reduce(reduceCommandsToPadIDs, []); 66 | const matchingSet = idSet2.slice(0, idSet1.length); 67 | expect(matchingSet).to.deep.equal(idSet1); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/integration/vaultRefs.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | Credentials, 4 | VaultFormatA, 5 | VaultFormatB, 6 | VaultManager, 7 | VaultSource, 8 | setDefaultFormat 9 | } from "../../dist/node/index.js"; 10 | 11 | describe("Vault", function () { 12 | [ 13 | ["Format A", VaultFormatA], 14 | ["Format B", VaultFormatB] 15 | ].forEach(([format, Format]) => { 16 | describe(`using ${format}`, function () { 17 | beforeEach(async function () { 18 | setDefaultFormat(Format); 19 | this.vaultManager = new VaultManager({ 20 | autoUpdate: false 21 | }); 22 | const creds = Credentials.fromDatasource( 23 | { 24 | type: "memory", 25 | property: `test:${Math.floor(Math.random() * 1000000)}` 26 | }, 27 | "test" 28 | ); 29 | const credsStr = await creds.toSecureString(); 30 | this.source = new VaultSource("Refs test", "memory", credsStr); 31 | await this.vaultManager.addSource(this.source); 32 | await this.source.unlock(Credentials.fromPassword("test"), { 33 | initialiseRemote: true 34 | }); 35 | this.vault = this.source.vault; 36 | }); 37 | 38 | afterEach(function () { 39 | setDefaultFormat(); 40 | }); 41 | 42 | it("updates own format reference on update", async function () { 43 | const originalVault = this.vault; 44 | const initialFormat = originalVault; 45 | await this.source.mergeFromRemote(); 46 | expect(this.source.vault).to.equal(originalVault); 47 | expect(originalVault.format).to.not.equal(initialFormat); 48 | }); 49 | 50 | it("updates group references on update", async function () { 51 | const myGroup = this.vault.createGroup("Test"); 52 | await this.source.mergeFromRemote(); 53 | const refMyGroup = this.vault.findGroupsByTitle("Test")[0]; 54 | expect(refMyGroup._source).to.equal(myGroup._source); 55 | }); 56 | 57 | it("updates entry references on update", async function () { 58 | const myGroup = this.vault.createGroup("Test"); 59 | const myEntry = myGroup.createEntry("Test"); 60 | await this.source.mergeFromRemote(); 61 | const refMyEntry = this.vault.findEntriesByProperty("title", "Test")[0]; 62 | expect(refMyEntry._source).to.equal(myEntry._source); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /source/tools/encoding.ts: -------------------------------------------------------------------------------- 1 | import { generateUUID } from "./uuid.js"; 2 | import { getSharedAppEnv } from "../env/appEnv.js"; 3 | 4 | const ENCODED_STRING_PATTERN = /^utf8\+base64:(|[a-zA-Z0-9+\/=]+)$/; 5 | export const ENCODED_STRING_PREFIX = "utf8+base64:"; 6 | 7 | /** 8 | * Decode a base64 string to a typed array 9 | * @param b64 The base64 string 10 | * @returns A typed array 11 | */ 12 | export function base64ToBytes(b64: string): Uint8Array { 13 | const decode = getSharedAppEnv().getProperty("encoding/v1/base64ToBytes"); 14 | return decode(b64); 15 | } 16 | 17 | /** 18 | * Encode a typed array to base64 19 | * @param uint8Arr The array of bytes to encode 20 | * @returns A base64 encoded string 21 | */ 22 | export function bytesToBase64(uint8Arr: Uint8Array): string { 23 | const encode = getSharedAppEnv().getProperty("encoding/v1/bytesToBase64"); 24 | return encode(uint8Arr); 25 | } 26 | 27 | /** 28 | * Decode a base64 string 29 | * @param b64 The base64 string 30 | * @returns The decoded string 31 | */ 32 | export function decodeBase64String(b64: string): string { 33 | const decode = getSharedAppEnv().getProperty("encoding/v1/base64ToText"); 34 | return decode(b64); 35 | } 36 | 37 | /** 38 | * Decode an encoded property value 39 | * @param value The encoded value 40 | * @returns The decoded value 41 | */ 42 | export function decodeStringValue(value: string): string { 43 | if (isEncoded(value) !== true) { 44 | throw new Error("Cannot decode: provided value is not encoded"); 45 | } 46 | const newValue = value.substr(ENCODED_STRING_PREFIX.length); 47 | const decode = getSharedAppEnv().getProperty("encoding/v1/base64ToText"); 48 | return decode(newValue); 49 | } 50 | 51 | /** 52 | * Encode a base64 string 53 | * @param text The raw text to encode 54 | * @returns A base64 encoded string 55 | */ 56 | export function encodeBase64String(text: string): string { 57 | const encode = getSharedAppEnv().getProperty("encoding/v1/textToBase64"); 58 | return encode(text); 59 | } 60 | 61 | /** 62 | * Encode a raw value into safe storage form 63 | * Uses base64 for encoding 64 | * @param value The raw value to encode 65 | * @returns The encoded result 66 | */ 67 | export function encodeStringValue(value: string): string { 68 | const encode = getSharedAppEnv().getProperty("encoding/v1/textToBase64"); 69 | return `${ENCODED_STRING_PREFIX}${encode(value)}`; 70 | } 71 | 72 | /** 73 | * Get a unique identifier (UUID v4) 74 | * @returns A unique identifier 75 | */ 76 | export function getUniqueID(): string { 77 | return generateUUID(); 78 | } 79 | 80 | /** 81 | * Check if a string value is encoded 82 | * @param text The value to check 83 | * @returns True if the text is encoded 84 | */ 85 | export function isEncoded(text: string): boolean { 86 | return ENCODED_STRING_PATTERN.test(text); 87 | } 88 | -------------------------------------------------------------------------------- /source/io/formatB/history.ts: -------------------------------------------------------------------------------- 1 | import { getTimestamp } from "../../tools/date.js"; 2 | import { EntryPropertyType, FormatBValue, FormatBValueHistoryItem } from "../../types.js"; 3 | 4 | export const MAX_ATTRIBUTE_HISTORY = 3; 5 | export const MAX_PROPERTY_HISTORY = 5; 6 | 7 | export function cloneValue(value: FormatBValue) { 8 | return { 9 | value: value.value, 10 | created: value.created, 11 | updated: value.updated, 12 | history: value.history.map((item) => Object.assign({}, item)) 13 | }; 14 | } 15 | 16 | function filterDuplicateHistoryItems( 17 | items: Array 18 | ): Array { 19 | return items.filter( 20 | (thisItem, thisIndex, itemArr) => 21 | itemArr.findIndex( 22 | (sub) => sub.value === thisItem.value && sub.updated === thisItem.updated 23 | ) === thisIndex 24 | ); 25 | } 26 | 27 | export function mergeValues( 28 | value1: FormatBValue, 29 | value2: FormatBValue, 30 | type: EntryPropertyType 31 | ): FormatBValue { 32 | const mostRecentValue = 33 | value1.updated > value2.updated || value1.updated === value2.updated ? value1 : value2; 34 | const olderValue = value1 === mostRecentValue ? value2 : value1; 35 | const newHistory = sortValueHistory( 36 | filterDuplicateHistoryItems([ 37 | valueToHistoryItem(olderValue), 38 | ...value1.history, 39 | ...value2.history 40 | ]) 41 | ); 42 | return { 43 | value: mostRecentValue.value, 44 | created: mostRecentValue.created, 45 | updated: mostRecentValue.updated, 46 | history: trimValueHistory(newHistory, type) 47 | }; 48 | } 49 | 50 | export function newRawValue(value: string): FormatBValue { 51 | const ts = getTimestamp(); 52 | return { 53 | value, 54 | created: ts, 55 | updated: ts, 56 | history: [] 57 | }; 58 | } 59 | 60 | export function sortValueHistory( 61 | history: Array 62 | ): Array { 63 | return history.sort((a, b) => { 64 | if (a.updated > b.updated) { 65 | return -1; 66 | } else if (b.updated > a.updated) { 67 | return 1; 68 | } 69 | return 0; 70 | }); 71 | } 72 | 73 | export function trimValueHistory( 74 | history: Array, 75 | type: EntryPropertyType 76 | ): Array { 77 | const max = type === EntryPropertyType.Attribute ? MAX_ATTRIBUTE_HISTORY : MAX_PROPERTY_HISTORY; 78 | const sorted = sortValueHistory(history); 79 | return sorted.slice(0, max); 80 | } 81 | 82 | export function valueToHistoryItem(value: FormatBValue): FormatBValueHistoryItem { 83 | return { 84 | value: value.value, 85 | updated: value.updated 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /source/core/VaultItem.ts: -------------------------------------------------------------------------------- 1 | import { Vault } from "./Vault.js"; 2 | import { VaultPermission } from "../types.js"; 3 | 4 | /** 5 | * Base vault member class (for Entry, Group etc.) 6 | */ 7 | export class VaultItem { 8 | _boundUpdateRefs: () => void; 9 | 10 | _source: any; 11 | 12 | /** 13 | * Reference to the containing vault 14 | * @protected 15 | */ 16 | _vault: Vault = null; 17 | 18 | /** 19 | * Constructor for the vault member base class 20 | * @param {Vault} vault Vault reference 21 | * @param {Object} source Remote source object reference 22 | */ 23 | constructor(vault: Vault, source: any) { 24 | this._vault = vault; 25 | this._source = source; 26 | this._source.permissions = this._source.permissions || [ 27 | VaultPermission.Manage, 28 | VaultPermission.Read, 29 | VaultPermission.Write 30 | ]; 31 | } 32 | 33 | /** 34 | * The ID of the entry or group 35 | * @readonly 36 | */ 37 | get id(): string { 38 | return this._vault.format.getItemID(this._source); 39 | } 40 | 41 | /** 42 | * The current granted permissions 43 | */ 44 | get permissions(): Array { 45 | return [...this._source.permissions]; 46 | } 47 | 48 | /** 49 | * The vault this item belongs to 50 | * @readonly 51 | */ 52 | get vault(): Vault { 53 | return this._vault; 54 | } 55 | 56 | /** 57 | * Grant a new permission to the member 58 | * @param perm The permission to grant 59 | */ 60 | grantPermission(perm: string) { 61 | if (!this.hasPermission(perm)) { 62 | this._source.permissions.push(perm); 63 | } 64 | } 65 | 66 | /** 67 | * Check if the member has a permission 68 | * @param perm The permission to check for 69 | */ 70 | hasPermission(perm: string): boolean { 71 | return this._source.permissions.includes(perm); 72 | } 73 | 74 | /** 75 | * Revoke all permissions 76 | */ 77 | revokeAllPermissions() { 78 | this._source.permissions = []; 79 | } 80 | 81 | /** 82 | * Revoke a single permission 83 | * @param perm The permission to revoke 84 | */ 85 | revokePermission(perm: string) { 86 | this._source.permissions = this._source.permissions.filter((current) => current !== perm); 87 | } 88 | 89 | /** 90 | * Clean up all of the data in the vault item 91 | * @protected 92 | */ 93 | _cleanUp() { 94 | this._vault = null; 95 | this._source = null; 96 | } 97 | 98 | /** 99 | * Update source references 100 | * @protected 101 | */ 102 | _updateRefs() { 103 | // No-op for vault item 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /source/io/formatB/compare.ts: -------------------------------------------------------------------------------- 1 | import { sortValueHistory } from "./history.js"; 2 | import { FormatBKeyValueObject, FormatBValue, FormatBVault, History } from "../../types.js"; 3 | 4 | export function historiesDiffer(historyA: History, historyB: History) { 5 | const vaultA = inflateHistory(historyA); 6 | const vaultB = inflateHistory(historyB); 7 | if (vaultA.e.length !== vaultB.e.length) return true; 8 | if (vaultA.g.length !== vaultB.g.length) return true; 9 | if (propertiesDiffer(vaultA.a, vaultB.a)) return true; 10 | return [ 11 | [vaultA, vaultB], 12 | [vaultB, vaultA] 13 | ].some(([item1, item2]: Array) => { 14 | const diffEntries = item1.e.some((entry1) => { 15 | // Check entries 16 | const matching = item2.e.find((entry2) => entry2.id === entry1.id); 17 | if (!matching) return true; 18 | if (entry1.g !== matching.g) return true; 19 | // Check entry properties 20 | return propertiesDiffer(entry1.p, matching.p) || propertiesDiffer(entry1.a, matching.a); 21 | }); 22 | if (diffEntries && item1.e.length > 0) return true; 23 | const diffGroups = item1.g.some((group1) => { 24 | const matching = item2.g.find((group2) => group2.id === group1.id); 25 | if (!matching) return true; 26 | if (group1.g !== matching.g) return true; 27 | // Check properties 28 | if (group1.t !== matching.t) return true; 29 | return propertiesDiffer(group1.a, matching.a); 30 | }); 31 | if (diffGroups && item1.g.length > 0) return true; 32 | return false; 33 | }); 34 | } 35 | 36 | function inflateHistory(history: History): FormatBVault { 37 | return JSON.parse(history[0]) as FormatBVault; 38 | } 39 | 40 | export function propertiesDiffer( 41 | props1: FormatBKeyValueObject, 42 | props2: FormatBKeyValueObject 43 | ): boolean { 44 | const allKeys = [...new Set([...Object.keys(props1), ...Object.keys(props2)])]; 45 | if (allKeys.length === 0) return false; 46 | return allKeys.some((key) => valuesDiffer(props1[key], props2[key])); 47 | } 48 | 49 | export function valuesDiffer(value1: FormatBValue, value2: FormatBValue): boolean { 50 | if ((!value1 && value2) || (value1 && !value2)) return true; 51 | for (const prop of ["value", "created", "updated"]) { 52 | if (value1[prop] !== value2[prop]) return true; 53 | } 54 | if (value1.history.length !== value2.history.length) return true; 55 | if (value1.history.length === 0) return false; 56 | const hist1 = sortValueHistory([...value1.history]); 57 | const hist2 = sortValueHistory([...value2.history]); 58 | for (const ind in hist1) { 59 | if (hist1[ind].value !== hist2[ind].value || hist1[ind].updated !== hist2[ind].updated) 60 | return true; 61 | } 62 | return false; 63 | } 64 | -------------------------------------------------------------------------------- /source/search/VaultSourceEntrySearch.ts: -------------------------------------------------------------------------------- 1 | import { SearchResult, SearcherFactory } from "./BaseSearch.js"; 2 | import { VaultSource } from "../core/VaultSource.js"; 3 | import { Vault } from "../core/Vault.js"; 4 | import { StorageInterface } from "../storage/StorageInterface.js"; 5 | import { VaultEntrySearch } from "./VaultEntrySearch.js"; 6 | import { VaultSourceStatus } from "../types.js"; 7 | 8 | export class VaultSourceEntrySearch extends VaultEntrySearch { 9 | _sources: Array; 10 | 11 | constructor( 12 | sources: Array, 13 | memory?: StorageInterface, 14 | searcherFactory?: SearcherFactory 15 | ) { 16 | const vaults: Array = sources.reduce((output, source) => { 17 | if (source.status === VaultSourceStatus.Unlocked) { 18 | return [...output, source.vault]; 19 | } 20 | return output; 21 | }, []); 22 | super(vaults, memory, searcherFactory); 23 | this._sources = [...sources]; 24 | } 25 | 26 | /** 27 | * Last search results 28 | * @deprecated Use `getResults` instead 29 | */ 30 | get results(): Array { 31 | return this.getResults(); 32 | } 33 | 34 | /** 35 | * Get last search results 36 | * @returns An array of results 37 | */ 38 | getResults(): Array { 39 | return super.getResults().map((res) => { 40 | const output = res; 41 | const source = this._sources.find((src) => src?.vault?.id === output.vaultID); 42 | if (source) { 43 | output.sourceID = source.id; 44 | } 45 | return output; 46 | }); 47 | } 48 | 49 | /** 50 | * Search for entries by term 51 | * @param term The term to search for 52 | * @returns An array of search results 53 | */ 54 | searchByTerm(term: string): Array { 55 | const results = super.searchByTerm(term); 56 | return results.map((res) => { 57 | const output = res; 58 | const source = this._sources.find((src) => src?.vault?.id === output.vaultID); 59 | if (source) { 60 | output.sourceID = source.id; 61 | } 62 | return output; 63 | }); 64 | } 65 | 66 | /** 67 | * Search for entries by URL 68 | * @param url The URL to search with 69 | * @returns An array of search results 70 | */ 71 | searchByURL(url: string): Array { 72 | const results = super.searchByURL(url); 73 | return results.map((res) => { 74 | const output = res; 75 | const source = this._sources.find((src) => src?.vault?.id === output.vaultID); 76 | if (source) { 77 | output.sourceID = source.id; 78 | } 79 | return output; 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ENTRY_FACADES.md: -------------------------------------------------------------------------------- 1 | # Entry facades 2 | 3 | Entries can come in different types (website logins, SSH keys, credit cards etc.), and facades help facilitate dynamic rendering of input fields for various Buttercup applications. A facade is an object that looks like the following: 4 | 5 | ```javascript 6 | { 7 | type: "website", 8 | fields: [ 9 | { 10 | title: "Title", 11 | field: "property", 12 | property: "title", 13 | value: "My sample entry", 14 | valueType: "text", 15 | secret: false, 16 | multiline: false, 17 | formatting: false 18 | }, 19 | { 20 | title: "Username", 21 | field: "property", 22 | property: "username", 23 | value: "user@system.org", 24 | valueType: "text", 25 | secret: false, 26 | multiline: false, 27 | formatting: false 28 | }, 29 | { 30 | title: "Password", 31 | field: "property", 32 | property: "password", 33 | value: "sI83.B19-z$", 34 | valueType: "password", 35 | secret: true, 36 | multiline: false, 37 | formatting: false 38 | }, 39 | { 40 | title: "URL", 41 | field: "meta", 42 | property: "url", 43 | value: "https://signin.system.org", 44 | valueType: "text", 45 | secret: false, 46 | multiline: false, 47 | formatting: false 48 | } 49 | ] 50 | } 51 | ``` 52 | 53 | This facade object allows other applications to easily and predictably edit entries in a dynamic nature. These facades can be generated on entries by using code like the following: 54 | 55 | ```javascript 56 | const { createEntryFacade } = require("buttercup"); 57 | 58 | const facade = createEntryFacade(myEntry); 59 | ``` 60 | 61 | After editing a facade, it can be applied to an entry by using code like the following: 62 | 63 | ```javascript 64 | const { consumeEntryFacade } = require("buttercup"); 65 | 66 | consumeEntryFacade(myEntry, facade); 67 | ``` 68 | 69 | ## Property Value Types 70 | 71 | All Buttercup entry **properties** have _value types_. A value type defines what kind of value the UI should believe the property stores, and this can change how the user is able to interact with the property value. 72 | 73 | _Attributes cannot have value types._ 74 | 75 | While generating a facade for an entry, you may notice some ambiguity between a property's `valueType` value and the corresponding attribute field also storing the property's value type. This is internal to how Buttercup functions, and a helper function is made available to help you **set a new value type for a property within a facade**: 76 | 77 | ```typescript 78 | import { setEntryFacadePropertyValueType } from "buttercup"; 79 | 80 | setEntryFacadePropertyValueType(facade, "secret-property", "password"); 81 | ``` 82 | -------------------------------------------------------------------------------- /source/web/LocalStorageDatasource.ts: -------------------------------------------------------------------------------- 1 | import { TextDatasource } from "../datasources/TextDatasource.js"; 2 | import { fireInstantiationHandlers, registerDatasource } from "../datasources/register.js"; 3 | import { Credentials } from "../credentials/Credentials.js"; 4 | import { getCredentials } from "../credentials/memory/credentials.js"; 5 | import LocalStorageInterface from "./LocalStorageInterface.js"; 6 | import { 7 | CredentialsPayload, 8 | DatasourceConfiguration, 9 | DatasourceLoadedData, 10 | EncryptedContent, 11 | History 12 | } from "../types.js"; 13 | 14 | const TYPE = "localstorage"; 15 | 16 | /** 17 | * Local Storage datasource 18 | * @augments TextDatasource 19 | * @memberof module:Buttercup 20 | */ 21 | export default class LocalStorageDatasource extends TextDatasource { 22 | _property: string; 23 | _storage: LocalStorageInterface; 24 | 25 | /** 26 | * Constructor for the datasource 27 | * @param credentials The credentials instance with which to 28 | * use to configure the datasource 29 | */ 30 | constructor(credentials: Credentials) { 31 | super(credentials); 32 | const { data: credentialData } = getCredentials(credentials.id) as CredentialsPayload; 33 | const { datasource: datasourceConfig } = credentialData; 34 | const { property } = datasourceConfig as DatasourceConfiguration; 35 | this._property = property; 36 | this._storage = new LocalStorageInterface(); 37 | this.type = TYPE; 38 | fireInstantiationHandlers(TYPE, this); 39 | } 40 | 41 | /** 42 | * Load from a local-storage property 43 | * @param credentials The credentials for decryption 44 | * @returns A promise resolving with vault history 45 | * @memberof LocalStorageDatasource 46 | */ 47 | async load(credentials: Credentials): Promise { 48 | const readVault = await this._storage.getValue("vault"); 49 | if (!readVault) { 50 | throw new Error("No vault in storage"); 51 | } 52 | this.setContent(readVault); 53 | return super.load(credentials); 54 | } 55 | 56 | /** 57 | * Save vault history to local-storage 58 | * @param history The vault history to save 59 | * @param credentials The credentials to save with 60 | * @returns A promise that resolves when saving is complete 61 | * @memberof LocalStorageDatasource 62 | */ 63 | save(history: History, credentials: Credentials): Promise { 64 | return super.save(history, credentials).then(async (encrypted) => { 65 | await this._storage.setValue("vault", encrypted); 66 | return encrypted; 67 | }); 68 | } 69 | 70 | /** 71 | * Whether or not the datasource supports attachments 72 | * @memberof LocalStorageDatasource 73 | */ 74 | supportsAttachments(): boolean { 75 | return false; 76 | } 77 | } 78 | 79 | registerDatasource(TYPE, LocalStorageDatasource); 80 | -------------------------------------------------------------------------------- /test/integration/fileVaultManagement.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { fileSync } from "tmp"; 3 | import { Credentials, VaultManager, VaultSource } from "../../dist/node/index.js"; 4 | 5 | describe("VaultManager", function () { 6 | describe("with file datasource", function () { 7 | beforeEach(async function () { 8 | this.vaultManager = new VaultManager({ 9 | autoUpdate: false 10 | }); 11 | // source 1 12 | this.tmp1 = fileSync(); 13 | const creds = Credentials.fromDatasource( 14 | { 15 | type: "file", 16 | path: this.tmp1.name 17 | }, 18 | "test" 19 | ); 20 | const credStr = await creds.toSecureString(); 21 | this.vaultSource1 = new VaultSource("test", "file", credStr); 22 | await this.vaultManager.addSource(this.vaultSource1); 23 | await this.vaultSource1.unlock(Credentials.fromPassword("test"), { 24 | initialiseRemote: true 25 | }); 26 | // source 2 27 | this.tmp2 = fileSync(); 28 | const creds2 = Credentials.fromDatasource( 29 | { 30 | type: "file", 31 | path: this.tmp2.name 32 | }, 33 | "test" 34 | ); 35 | const credStr2 = await creds2.toSecureString(); 36 | this.vaultSource2 = new VaultSource("test2", "file", credStr2); 37 | await this.vaultManager.addSource(this.vaultSource2); 38 | await this.vaultSource2.unlock(Credentials.fromPassword("test"), { 39 | initialiseRemote: true 40 | }); 41 | }); 42 | 43 | afterEach(function () { 44 | this.tmp1.removeCallback(); 45 | this.tmp2.removeCallback(); 46 | }); 47 | 48 | it("can change password", async function () { 49 | await this.vaultSource1.changeMasterPassword("test", "test2"); 50 | expect(this.vaultSource1.status).to.equal(VaultSource.STATUS_UNLOCKED); 51 | await this.vaultSource1.lock(); 52 | await this.vaultSource1.unlock(Credentials.fromPassword("test2")); 53 | expect(this.vaultSource1.status).to.equal(VaultSource.STATUS_UNLOCKED); 54 | }); 55 | 56 | describe("sources", function () { 57 | it("stores sources initially in the order they were added", function () { 58 | const [source1, source2] = this.vaultManager.sources; 59 | expect(source1.id).to.equal(this.vaultSource1.id); 60 | expect(source2.id).to.equal(this.vaultSource2.id); 61 | }); 62 | }); 63 | 64 | describe("reorderSource", function () { 65 | it("changes the order of sources", async function () { 66 | await this.vaultManager.reorderSource(this.vaultSource2.id, 0); 67 | const [source1, source2] = this.vaultManager.sources; 68 | expect(source1.id).to.equal(this.vaultSource2.id); 69 | expect(source2.id).to.equal(this.vaultSource1.id); 70 | }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/integration/saving.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { 3 | Credentials, 4 | VaultFormatA, 5 | VaultFormatB, 6 | VaultManager, 7 | VaultSource, 8 | setDefaultFormat 9 | } from "../../dist/node/index.js"; 10 | 11 | describe("Vault", function () { 12 | [ 13 | ["Format A", VaultFormatA], 14 | ["Format B", VaultFormatB] 15 | ].forEach(([format, Format]) => { 16 | describe(`using ${format}`, function () { 17 | beforeEach(async function () { 18 | setDefaultFormat(Format); 19 | this.vaultManager = new VaultManager({ 20 | autoUpdate: false 21 | }); 22 | const creds = Credentials.fromDatasource( 23 | { 24 | type: "memory", 25 | property: `test:${Math.floor(Math.random() * 1000000)}` 26 | }, 27 | "test" 28 | ); 29 | const credsStr = await creds.toSecureString(); 30 | this.source = new VaultSource("Refs test", "memory", credsStr); 31 | await this.vaultManager.addSource(this.source); 32 | await this.source.unlock(Credentials.fromPassword("test"), { 33 | initialiseRemote: true 34 | }); 35 | this.vault = this.source.vault; 36 | }); 37 | 38 | afterEach(function () { 39 | setDefaultFormat(); 40 | }); 41 | 42 | it("can empty trashed entries", async function () { 43 | const group = this.vault.createGroup("Main"); 44 | const entry = group.createEntry("Test"); 45 | entry.delete(); 46 | await this.source.save(); 47 | // Now empty trash 48 | this.vault.emptyTrash(); 49 | await this.source.save(); 50 | expect(this.vault.getTrashGroup().getEntries()).to.have.lengthOf( 51 | 0, 52 | "Should have 0 entries" 53 | ); 54 | expect(this.vault.getTrashGroup().getGroups()).to.have.lengthOf( 55 | 0, 56 | "Should have 0 groups" 57 | ); 58 | }); 59 | 60 | it("can empty trashed entries", async function () { 61 | const group = this.vault.createGroup("Main"); 62 | group.createEntry("Test 1"); 63 | group.createEntry("Test 2"); 64 | await this.source.save(); 65 | // Now delete 66 | group.delete(); 67 | await this.source.save(); 68 | // Now empty trash 69 | this.vault.emptyTrash(); 70 | await this.source.save(); 71 | expect(this.vault.getTrashGroup().getEntries()).to.have.lengthOf( 72 | 0, 73 | "Should have 0 entries" 74 | ); 75 | expect(this.vault.getTrashGroup().getGroups()).to.have.lengthOf( 76 | 0, 77 | "Should have 0 groups" 78 | ); 79 | }); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /test/integration/search/helpers.js: -------------------------------------------------------------------------------- 1 | import { 2 | Credentials, 3 | Entry, 4 | EntryType, 5 | Group, 6 | Vault, 7 | VaultManager, 8 | VaultSource 9 | } from "../../../dist/node/index.js"; 10 | 11 | export async function createSampleManager() { 12 | const manager = new VaultManager({ 13 | autoUpdate: false 14 | }); 15 | const creds = Credentials.fromDatasource( 16 | { 17 | type: "memory", 18 | property: `test:${Math.floor(Math.random() * 1000000)}` 19 | }, 20 | "test" 21 | ); 22 | const credsStr = await creds.toSecureString(); 23 | const source = new VaultSource("Refs test", "memory", credsStr); 24 | await manager.addSource(source); 25 | await source.unlock(Credentials.fromPassword("test"), { 26 | initialiseRemote: true 27 | }); 28 | createSampleVault(source.vault); 29 | await source.save(); 30 | return [manager, source]; 31 | } 32 | 33 | export function createSampleVault(vault = new Vault()) { 34 | const groupA = vault.createGroup("Email"); 35 | groupA 36 | .createEntry("Personal Mail") 37 | .setProperty("username", "green.monkey@fastmail.com") 38 | .setProperty("password", "df98Sm2.109x{91") 39 | .setProperty("url", "https://fastmail.com") 40 | .setAttribute(Entry.Attributes.FacadeType, EntryType.Website); 41 | groupA 42 | .createEntry("Work") 43 | .setProperty("username", "j.crowley@gmov.edu.au") 44 | .setProperty("password", "#f05c.*skU3") 45 | .setProperty("URL", "gmov.edu.au/portal/auth") 46 | .addTags("job"); 47 | groupA 48 | .createEntry("Work logs") 49 | .setProperty("username", "j.crowley@gmov.edu.au") 50 | .setProperty("password", "#f05c.*skU3") 51 | .setProperty("URL", "https://logs.gmov.edu.au/sys30/atc.php") 52 | .addTags("job"); 53 | const groupB = vault.createGroup("Bank"); 54 | groupB 55 | .createEntry("MyBank") 56 | .setProperty("username", "324654356346") 57 | .setProperty("PIN", "1234") 58 | .setAttribute(Entry.Attributes.FacadeType, EntryType.Login) 59 | .addTags("finance", "banking"); 60 | groupB 61 | .createEntry("Insurance") 62 | .setProperty("username", "testing-user") 63 | .setProperty("URL", "http://test.org/portal-int/login.aspx") 64 | .addTags("finance"); 65 | const groupC = vault.createGroup("General"); 66 | groupC 67 | .createEntry("Clipart") 68 | .setProperty("username", "gmonkey123") 69 | .setProperty("password", "test93045") 70 | .setProperty("Url", "clipart.com"); 71 | groupC 72 | .createEntry("Wordpress") 73 | .setProperty("username", "gmonkey1234") 74 | .setProperty("password", "passw0rd") 75 | .setProperty("Url", "https://wordpress.com/") 76 | .setProperty("Login URL", "https://wordpress.com/account/login.php"); 77 | const trashGroup = vault.createGroup("Trash"); 78 | trashGroup 79 | .createEntry("Ebay") 80 | .setProperty("username", "gmk123@hotmail.com") 81 | .setProperty("password", "passw0rd") 82 | .setProperty("Url", "https://ebay.com/"); 83 | trashGroup.setAttribute(Group.Attribute.Role, "trash"); 84 | return vault; 85 | } 86 | -------------------------------------------------------------------------------- /VAULT_FORMAT.md: -------------------------------------------------------------------------------- 1 | # Vault Format 2 | 3 | Buttercup uses special format _vaults_ to store secret groups and entries. This document details the active vault formats that Buttercup uses in its current software. 4 | 5 | ## Format B 6 | 7 | > ? -> Current 8 | 9 | The second format, utilising a JSON structure for vault data: 10 | 11 | ```json 12 | { 13 | "id": "fb31b4a6-1e54-4460-ae03-5441a8083be5", 14 | "a": {}, 15 | "g": [ 16 | { 17 | "id": "c5f76882-6d5d-4348-b3fd-222608670af0", 18 | "t": "General", 19 | "a": {}, 20 | "g": "0" 21 | }, 22 | { 23 | "id": "e494bd0a-4c91-48c0-85c7-59ff8387dd87", 24 | "t": "Websites", 25 | "a": {}, 26 | "g": "c5f76882-6d5d-4348-b3fd-222608670af0" 27 | } 28 | ], 29 | "e": [ 30 | { 31 | "id": "9f051bef-e6c5-4b85-a135-5ff7ddfc4cb2", 32 | "g": "e494bd0a-4c91-48c0-85c7-59ff8387dd87", 33 | "a": {}, 34 | "p": { 35 | "title": { 36 | "value": "My Entry", 37 | "created": 1598298143533, 38 | "updated": 1598298143533, 39 | "history": [] 40 | }, 41 | "username": { 42 | "value": "user@test.com", 43 | "created": 1598298178114, 44 | "updated": 1598298178114, 45 | "history": [] 46 | }, 47 | "password": { 48 | "value": "passw0rd", 49 | "created": 1598298153538, 50 | "updated": 1598298178114, 51 | "history": [ 52 | { 53 | "value": "old-password", 54 | "updated": 1598298177010 55 | } 56 | ] 57 | }, 58 | "deleted item": { 59 | "value": "some value", 60 | "created": 1598298362682, 61 | "updated": 1598298362682, 62 | "deleted": 1598298411608, 63 | "history": [] 64 | } 65 | } 66 | } 67 | ], 68 | "c": "2020-08-24T19:40:12.609Z" 69 | } 70 | ``` 71 | 72 | ## Format A 73 | 74 | > September 2015 -> 2020 75 | 76 | The first format for Butttercup vaults. Uses a line-by-line delta structure to _modify_ the vault. Supported step-by-step updates to the vault.: 77 | 78 | ``` 79 | aid df553df1-9096-4e97-b321-5cda5a761922 80 | cmm "Buttercup archive created (2017-1-7)" 81 | fmt "buttercup/a" 82 | cgr 0 ddb5bed5-00d5-4b5d-96a2-1a36511ac538 83 | tgr ddb5bed5-00d5-4b5d-96a2-1a36511ac538 "test-group-main" 84 | pad 6b78b1bc-66f9-4f67-ac9b-92862c8bb1f7 85 | cen ddb5bed5-00d5-4b5d-96a2-1a36511ac538 ce86c59b-0290-476a-8121-8a6e5381c528 86 | sep ce86c59b-0290-476a-8121-8a6e5381c528 title "test-entry-main" 87 | pad 46fb579e-561f-434c-a9f3-7464f49f63a9 88 | sep ce86c59b-0290-476a-8121-8a6e5381c528 username "user123\\@test.@D" 89 | pad 4da334e8-c9e5-464d-9a91-393188972bbb 90 | sep ce86c59b-0290-476a-8121-8a6e5381c528 password "* \u0002\u0006\u0000\u0003\u0007\u0004\u0006\u0000͡! " 91 | pad 764cad08-2bc8-4ea3-a728-f38ac9aa2f78 92 | sem ce86c59b-0290-476a-8121-8a6e5381c528 "test-meta" "test-value 8" 93 | pad a1673764-c34b-481e-ae73-b09a56f85ba6 94 | ``` 95 | -------------------------------------------------------------------------------- /source/io/formatA/Flattener.ts: -------------------------------------------------------------------------------- 1 | import { describeVaultDataset } from "./describe.js"; 2 | import { VaultFormatA } from "../VaultFormatA.js"; 3 | import { FormatAVault } from "../../types.js"; 4 | 5 | /** 6 | * Check if a command should be preserved (not flattened) 7 | * @param command The command to check 8 | * @returns True if the command is to be kept 9 | * @private 10 | * @static 11 | * @memberof Flattener 12 | */ 13 | function mustBePreserved(command: string): boolean { 14 | const commandName = command.substr(0, 3); 15 | // Note: "fmt" and "aid" are generated automatically and do not need to be preserved 16 | return ["cmm"].indexOf(commandName) >= 0; 17 | } 18 | 19 | /** 20 | * Flattener class for flattening archive history sets 21 | */ 22 | export class Flattener { 23 | /** 24 | * Minimum history lines before flattening can occur 25 | * @static 26 | * @memberof Flattener 27 | */ 28 | static FLATTENING_MIN_LINES = 6000; 29 | /** 30 | * Number of lines to preserve (most recent) 31 | * @static 32 | * @memberof Flattener 33 | */ 34 | static PRESERVE_LINES = 5000; 35 | 36 | format: VaultFormatA; 37 | 38 | constructor(format: VaultFormatA) { 39 | this.format = format; 40 | } 41 | 42 | /** 43 | * Check if the dataset can be flattened 44 | * @returns True if it can be flattened 45 | * @memberof Flattener 46 | */ 47 | canBeFlattened(): boolean { 48 | return this.format.history.length >= Flattener.FLATTENING_MIN_LINES; 49 | } 50 | 51 | /** 52 | * Flatten a dataset 53 | * @param force Force flattening even if it is detected to be unnecessary 54 | * @returns True if flattening occurred, false otherwise 55 | * @memberof Flattener 56 | */ 57 | flatten(force: boolean = false): boolean { 58 | const history = this.format.history; 59 | const preservedLines = []; 60 | const tempFormat = new VaultFormatA(); 61 | let availableLines = history.length - Flattener.PRESERVE_LINES; 62 | // check if possible to flatten 63 | if (availableLines <= 0 || !this.canBeFlattened()) { 64 | if (!force) { 65 | return false; 66 | } 67 | availableLines = history.length; 68 | } 69 | // execute early history 70 | let currentCommand; 71 | for (let i = 0; i < availableLines; i += 1) { 72 | currentCommand = history[i]; 73 | if (mustBePreserved(currentCommand)) { 74 | preservedLines.push(currentCommand); 75 | } 76 | tempFormat.execute(currentCommand); 77 | } 78 | // describe the archive at its current state 79 | const cleanHistory = describeVaultDataset(tempFormat.source as FormatAVault, "0"); 80 | // prepare to replay 81 | const newHistory = [ 82 | ...preservedLines, // preserved commands that cannot be stripped 83 | ...cleanHistory, // the newly flattened description commands 84 | ...history.slice(availableLines) // the existing history minus the flattened portion 85 | ]; 86 | // clear the system 87 | this.format.erase(); 88 | // replay all history (expensive) 89 | this.format.execute(newHistory); 90 | // newHistory.forEach(this.format.execute.bind(this._westley)); 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/unit/facades/vault.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Vault, createGroupFacade, createVaultFacade } from "../../../dist/node/index.js"; 3 | 4 | describe("facades/vault", function () { 5 | beforeEach(function () { 6 | this.vault = new Vault(); 7 | this.vault.setAttribute("ATTR_1", "one").setAttribute("ATTR_2", "two"); 8 | const topGroup = this.vault.createGroup("top"); 9 | const bottomGroup = topGroup.createGroup("one").createGroup("two"); 10 | topGroup.createGroup("three"); 11 | topGroup 12 | .createEntry("Entry A") 13 | .setProperty("username", "user@example.com") 14 | .setProperty("password", "passw0rd"); 15 | bottomGroup 16 | .createEntry("Entry B") 17 | .setProperty("username", "user2@example.com") 18 | .setProperty("password", "pa55w0rd"); 19 | this.group = topGroup; 20 | topGroup.setAttribute("test_attr", "1234"); 21 | topGroup.setAttribute("test_attr_2", "5678"); 22 | }); 23 | 24 | describe("createVaultFacade", function () { 25 | it("outputs the correct facade type", function () { 26 | const { type } = createVaultFacade(this.vault); 27 | expect(type).to.equal("vault"); 28 | }); 29 | 30 | it("sets the vault ID", function () { 31 | const { id } = createVaultFacade(this.vault); 32 | expect(id).to.equal(this.vault.id); 33 | }); 34 | 35 | it("outputs attributes", function () { 36 | const { attributes } = createVaultFacade(this.vault); 37 | expect(attributes).to.deep.equal({ 38 | ATTR_1: "one", 39 | ATTR_2: "two" 40 | }); 41 | }); 42 | 43 | it("outputs all groups", function () { 44 | const { groups } = createVaultFacade(this.vault); 45 | const groupNames = groups.map((group) => group.title); 46 | expect(groupNames).to.include.members(["top", "one", "two", "three"]); 47 | }); 48 | 49 | it("outputs all entries", function () { 50 | const { entries } = createVaultFacade(this.vault); 51 | expect(entries).to.have.lengthOf(2); 52 | }); 53 | 54 | it("outputs a tag", function () { 55 | const { _tag } = createVaultFacade(this.vault); 56 | expect(_tag).to.be.a("string").that.has.length.above(0); 57 | }); 58 | }); 59 | 60 | describe("createGroupFacade", function () { 61 | it("outputs the ID", function () { 62 | const facade = createGroupFacade(this.group); 63 | expect(facade).to.have.property("id", this.group.id); 64 | }); 65 | 66 | it("outputs the title", function () { 67 | const facade = createGroupFacade(this.group); 68 | expect(facade).to.have.property("title", this.group.getTitle()); 69 | }); 70 | 71 | it("sets the type", function () { 72 | const facade = createGroupFacade(this.group); 73 | expect(facade).to.have.property("type", "group"); 74 | }); 75 | 76 | it("outputs the group attributes", function () { 77 | const facade = createGroupFacade(this.group); 78 | expect(facade).to.have.property("attributes").that.deep.equals({ 79 | test_attr: "1234", 80 | test_attr_2: "5678" 81 | }); 82 | }); 83 | }); 84 | }); 85 | --------------------------------------------------------------------------------