├── .nvmrc
├── .gitattributes
├── jsconfig.json
├── resources
├── Key32.key
├── Key64.key
├── demo.kdbx
├── Argon2.kdbx
├── Key32.kdbx
├── Key64.kdbx
├── KeyV2.kdbx
├── binkey.kdbx
├── binkey.key
├── AesChaCha.kdbx
├── Argon2id.kdbx
├── EmptyPass.kdbx
├── KDBX4.1.kdbx
├── YubiKey3.kdbx
├── YubiKey4.kdbx
├── cyrillic.kdbx
├── demohard.kdbx
├── AesKdfKdbx4.kdbx
├── KeyWithBom.kdbx
├── Argon2ChaCha.kdbx
├── NoPassWithKeyFile.kdbx
├── EmptyPassWithKeyFile.kdbx
├── KeyWithBom.key
├── demo.key
├── EmptyPassWithKeyFile.key
├── NoPassWithKeyFile.key
└── KeyV2.keyx
├── format
├── KDBX-HexFiend.png
├── README.md
└── Kdbx.tcl
├── conf
├── tsconfig.build-debug.json
├── tsconfig.build-prod.json
├── tsconfig.base.json
├── webpack.tests.config.ts
└── webpack.config.ts
├── .npmignore
├── .nycrc.json
├── .prettierrc
├── scripts
├── .eslintrc.json
├── save-perf-test.ts
├── make-big-files.ts
├── dump-header.ts
├── kdbx-to-xml.ts
└── kdbx-size-profiler.ts
├── tsconfig.json
├── lib
├── errors
│ └── kdbx-error.ts
├── format
│ ├── kdbx-context.ts
│ ├── kdbx-deleted-object.ts
│ ├── kdbx-uuid.ts
│ ├── kdbx-custom-data.ts
│ ├── kdbx-times.ts
│ ├── kdbx-binaries.ts
│ └── kdbx-credentials.ts
├── utils
│ ├── int64.ts
│ ├── byte-utils.ts
│ ├── binary-stream.ts
│ └── var-dictionary.ts
├── crypto
│ ├── protect-salt-generator.ts
│ ├── key-encryptor-aes.ts
│ ├── hashed-block-transform.ts
│ ├── protected-value.ts
│ ├── chacha20.ts
│ ├── key-encryptor-kdf.ts
│ ├── hmac-block-transform.ts
│ └── crypto-engine.ts
├── defs
│ ├── consts.ts
│ └── xml-names.ts
└── index.ts
├── .github
├── FUNDING.yml
└── workflows
│ └── CI.yaml
├── test
├── browser-unit-tests.html
├── errors
│ └── kdbx-error.spec.ts
├── crypto
│ ├── chacha20.spec.ts
│ ├── key-encryptor-aes.spec.ts
│ ├── hashed-block-transform.spec.ts
│ ├── hmac-block-transform.spec.ts
│ ├── protected-salt-generator.spec.ts
│ ├── salsa20.spec.ts
│ └── protected-value.spec.ts
├── utils
│ ├── int64.spec.ts
│ ├── byte-utils.spec.ts
│ └── binary-stream.spec.ts
├── test-support
│ ├── argon2.ts
│ ├── test-resources.ts
│ └── subtle-mock-node.ts
└── format
│ ├── kdbx-custom-data.spec.ts
│ ├── kdbx-uuid.spec.ts
│ ├── kdbx-binaries.spec.ts
│ └── kdbx-credentials.spec.ts
├── .editorconfig
├── LICENSE
├── .all-contributorsrc
├── .gitignore
├── package.json
├── release-notes.md
└── eslint.config.cjs
/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.18.0
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["*.js"]
3 | }
--------------------------------------------------------------------------------
/resources/Key32.key:
--------------------------------------------------------------------------------
1 | 12345678901234567890123456789012
--------------------------------------------------------------------------------
/resources/Key64.key:
--------------------------------------------------------------------------------
1 | 1234567890123456789012345678901234567890123456789012345678901234
--------------------------------------------------------------------------------
/resources/demo.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/demo.kdbx
--------------------------------------------------------------------------------
/resources/Argon2.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/Argon2.kdbx
--------------------------------------------------------------------------------
/resources/Key32.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/Key32.kdbx
--------------------------------------------------------------------------------
/resources/Key64.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/Key64.kdbx
--------------------------------------------------------------------------------
/resources/KeyV2.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/KeyV2.kdbx
--------------------------------------------------------------------------------
/resources/binkey.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/binkey.kdbx
--------------------------------------------------------------------------------
/resources/binkey.key:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/binkey.key
--------------------------------------------------------------------------------
/format/KDBX-HexFiend.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/format/KDBX-HexFiend.png
--------------------------------------------------------------------------------
/resources/AesChaCha.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/AesChaCha.kdbx
--------------------------------------------------------------------------------
/resources/Argon2id.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/Argon2id.kdbx
--------------------------------------------------------------------------------
/resources/EmptyPass.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/EmptyPass.kdbx
--------------------------------------------------------------------------------
/resources/KDBX4.1.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/KDBX4.1.kdbx
--------------------------------------------------------------------------------
/resources/YubiKey3.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/YubiKey3.kdbx
--------------------------------------------------------------------------------
/resources/YubiKey4.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/YubiKey4.kdbx
--------------------------------------------------------------------------------
/resources/cyrillic.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/cyrillic.kdbx
--------------------------------------------------------------------------------
/resources/demohard.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/demohard.kdbx
--------------------------------------------------------------------------------
/resources/AesKdfKdbx4.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/AesKdfKdbx4.kdbx
--------------------------------------------------------------------------------
/resources/KeyWithBom.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/KeyWithBom.kdbx
--------------------------------------------------------------------------------
/resources/Argon2ChaCha.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/Argon2ChaCha.kdbx
--------------------------------------------------------------------------------
/resources/NoPassWithKeyFile.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/NoPassWithKeyFile.kdbx
--------------------------------------------------------------------------------
/resources/EmptyPassWithKeyFile.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/keeweb/kdbxweb/HEAD/resources/EmptyPassWithKeyFile.kdbx
--------------------------------------------------------------------------------
/conf/tsconfig.build-debug.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "sourceMap": true
5 | }
6 | }
--------------------------------------------------------------------------------
/conf/tsconfig.build-prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.base.json",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "declarationDir": "../dist/types"
6 | }
7 | }
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !dist/kdbxweb.js
3 | !dist/kdbxweb.min.js
4 | !dist/types/**/*.d.ts
5 | !LICENSE
6 | !README.md
7 | !release-notes.md
8 | !format/Kdbx.tcl
9 | !lib/**/*.ts
10 |
--------------------------------------------------------------------------------
/.nycrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@istanbuljs/nyc-config-typescript",
3 | "all": true,
4 | "include": [
5 | "lib/**/*.ts"
6 | ],
7 | "exclude": [
8 | "*.js"
9 | ]
10 | }
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "singleQuote": true,
4 | "printWidth": 100,
5 | "trailingComma": "none",
6 | "quoteProps": "preserve",
7 | "endOfLine": "auto"
8 | }
9 |
--------------------------------------------------------------------------------
/scripts/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "project": ["../tsconfig.json"]
4 | },
5 | "rules": {
6 | "no-console": "off",
7 | "@typescript-eslint/no-explicit-any": "off"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/resources/KeyWithBom.key:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.00
5 |
6 |
7 | eiFL/HpBR0qjVrA/9GfX4HPyF4hNbP4fKyHowERq5jI=
8 |
9 |
10 |
--------------------------------------------------------------------------------
/resources/demo.key:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.00
5 |
6 |
7 | AtY2GR2pVt6aWz2ugfxfSQWjRId9l0JWe/LEMJWVJ1k=
8 |
9 |
10 |
--------------------------------------------------------------------------------
/resources/EmptyPassWithKeyFile.key:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.00
5 |
6 |
7 | 35szxiw2dcHtFlpmjhoWIDZ+gXO0VbI5nNY0gCyeL/o=
8 |
9 |
10 |
--------------------------------------------------------------------------------
/resources/NoPassWithKeyFile.key:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1.00
5 |
6 |
7 | WYVxXQxQ88KLv4QdSOKU1LIZ0nJZDVjGmgqIT2RWudU=
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./conf/tsconfig.base.json",
3 | "compilerOptions": {
4 | "types": ["node", "mocha"],
5 | "esModuleInterop": true,
6 | "resolveJsonModule": true
7 | },
8 | "include": ["test/**/*.ts", "scripts/**/*.ts", "conf/**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/conf/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "commonjs",
5 | "preserveConstEnums": true,
6 | "strict": true,
7 | "baseUrl": "../lib",
8 | "noEmitOnError": true,
9 | "types": ["node"],
10 | "outDir": "../dist"
11 | },
12 | "include": ["../lib/**/*.ts"]
13 | }
--------------------------------------------------------------------------------
/lib/errors/kdbx-error.ts:
--------------------------------------------------------------------------------
1 | class KdbxError extends Error {
2 | public readonly code: string;
3 |
4 | constructor(code: string, message?: string) {
5 | super('Error ' + code + (message ? ': ' + message : ''));
6 |
7 | this.name = 'KdbxError';
8 | this.code = code;
9 | }
10 | }
11 |
12 | export { KdbxError };
13 |
--------------------------------------------------------------------------------
/resources/KeyV2.keyx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 2.0
5 |
6 |
7 |
8 | A7007945 D07D54BA 28DF6434 1B4500FC
9 | 9750DFB1 D36ADA2D 9C32DC19 4C7AB01B
10 |
11 |
12 |
--------------------------------------------------------------------------------
/format/README.md:
--------------------------------------------------------------------------------
1 | This is a template for [HexFiend](https://github.com/ridiculousfish/HexFiend).
2 |
3 | To install:
4 |
5 | ```sh
6 | ln -s format/Kdbx.tcl ~/Library/Application\ Support/com.ridiculousfish.HexFiend/Templates/Kdbx.tcl
7 | ```
8 |
9 | Alternatively, just copy Kdbx.tcl to `~/Library/Application\ Support/com.ridiculousfish.HexFiend/Templates/Kdbx.tcl`.
10 |
11 | You should see something like this;
12 |
13 | 
14 |
--------------------------------------------------------------------------------
/lib/format/kdbx-context.ts:
--------------------------------------------------------------------------------
1 | import * as XmlUtils from './../utils/xml-utils';
2 | import { Kdbx } from './kdbx';
3 |
4 | export class KdbxContext {
5 | readonly kdbx: Kdbx;
6 | exportXml: boolean;
7 |
8 | constructor(opts: { kdbx: Kdbx; exportXml?: boolean }) {
9 | this.kdbx = opts.kdbx;
10 | this.exportXml = !!opts.exportXml;
11 | }
12 |
13 | setXmlDate(node: Node, dt: Date | undefined): void {
14 | const isBinary = this.kdbx.versionMajor >= 4 && !this.exportXml;
15 | XmlUtils.setDate(node, dt, isBinary);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: antelle
4 | patreon: # Replace with a single Patreon username
5 | open_collective: keeweb
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # for example PayPal links
13 |
--------------------------------------------------------------------------------
/test/browser-unit-tests.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | kdbxweb Browser Unit Tests with Mocha
6 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # #
2 | # @file .editorconfig
3 | # @author Aetherinox
4 | # @ref http://editorconfig.org
5 | # #
6 |
7 | # #
8 | # Is top-most EditorConfig file
9 | # #
10 |
11 | root = true
12 |
13 | # #
14 | # All Files
15 | # #
16 |
17 | [*]
18 | indent_style = space
19 | indent_size = 4
20 | end_of_line = lf
21 | charset = utf-8
22 | trim_trailing_whitespace = true
23 | insert_final_newline = true
24 |
25 | # #
26 | # Markdown Files
27 | # #
28 |
29 | [*.md]
30 | trim_trailing_whitespace = false
31 |
32 | # #
33 | # Other
34 | # #
35 |
36 | [{*.nsh,*.yml,*.yaml,*.json}]
37 | indent_style = space
38 | indent_size = 2
--------------------------------------------------------------------------------
/scripts/save-perf-test.ts:
--------------------------------------------------------------------------------
1 | import { Credentials, CryptoEngine, Kdbx, ProtectedValue } from '../lib';
2 | import { argon2 } from '../test/test-support/argon2';
3 |
4 | CryptoEngine.setArgon2Impl(argon2);
5 |
6 | const credentials = new Credentials(ProtectedValue.fromString(''));
7 |
8 | const db = Kdbx.create(credentials, 'test');
9 | db.upgrade();
10 |
11 | const time = process.hrtime();
12 | db.save()
13 | .then(() => {
14 | const diff = process.hrtime(time);
15 | const NS_PER_SEC = 1e9;
16 | const seconds = (diff[0] + diff[1] / NS_PER_SEC).toFixed(3);
17 | console.log(`Done in ${seconds} seconds`);
18 | })
19 | .catch((e) => console.error(e));
20 |
--------------------------------------------------------------------------------
/test/errors/kdbx-error.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { KdbxError } from '../../lib';
3 |
4 | describe('KdbxError', () => {
5 | it('creates error without message', () => {
6 | const err = new KdbxError('1');
7 | expect(err.name).to.be('KdbxError');
8 | expect(err.code).to.be('1');
9 | expect(err.message).to.be('Error 1');
10 | expect(err.toString()).to.be('KdbxError: Error 1');
11 | });
12 |
13 | it('creates error with message', () => {
14 | const err = new KdbxError('2', 'msg');
15 | expect(err.name).to.be('KdbxError');
16 | expect(err.code).to.be('2');
17 | expect(err.message).to.be('Error 2: msg');
18 | expect(err.toString()).to.be('KdbxError: Error 2: msg');
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/test/crypto/chacha20.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils, ChaCha20 } from '../../lib';
3 |
4 | describe('ChaCha20', () => {
5 | it('transforms data', () => {
6 | const key = new Uint8Array(32);
7 | const nonce = new Uint8Array(32);
8 |
9 | const chacha20 = new ChaCha20(key, nonce);
10 | expect(ByteUtils.bytesToHex(chacha20.getBytes(32))).to.be(
11 | '76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7'
12 | );
13 | expect(ByteUtils.bytesToHex(chacha20.getBytes(32))).to.be(
14 | 'da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586'
15 | );
16 | // @ts-ignore
17 | chacha20._input[12] = 0xffffffff;
18 | expect(ByteUtils.bytesToHex(chacha20.getBytes(16))).to.be(
19 | 'ace4cd09e294d1912d4ad205d06f95d9'
20 | );
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/lib/utils/int64.ts:
--------------------------------------------------------------------------------
1 | class Int64 {
2 | public readonly lo: number;
3 | public readonly hi: number;
4 |
5 | constructor(lo = 0, hi = 0) {
6 | this.lo = lo;
7 | this.hi = hi;
8 | }
9 |
10 | get value(): number {
11 | if (this.hi) {
12 | if (this.hi >= 0x200000) {
13 | throw new Error('too large number');
14 | }
15 | return this.hi * 0x100000000 + this.lo;
16 | }
17 | return this.lo;
18 | }
19 |
20 | valueOf(): number {
21 | return this.value;
22 | }
23 |
24 | static from(value: number): Int64 {
25 | if (value > 0x1fffffffffffff) {
26 | throw new Error('too large number');
27 | }
28 | const lo = value >>> 0;
29 | const hi = ((value - lo) / 0x100000000) >>> 0;
30 | return new Int64(lo, hi);
31 | }
32 | }
33 |
34 | export { Int64 };
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (C) 2021-2025 Antelle
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/crypto/key-encryptor-aes.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils, KeyEncryptorAes } from '../../lib';
3 |
4 | describe('KeyEncryptorAes', () => {
5 | const data = ByteUtils.hexToBytes(
6 | '5d18f8a5ae0e7ea86f0ad817f0c0d40656ef1da6367d8a88508b3c13cec0d7af'
7 | );
8 | const key = ByteUtils.hexToBytes(
9 | 'ee66af917de0b0336e659fe6bd40a337d04e3c2b3635210fa16f28fb24d563ac'
10 | );
11 |
12 | it('decrypts one round', () => {
13 | return KeyEncryptorAes.encrypt(data, key, 1).then((res) => {
14 | expect(ByteUtils.bytesToHex(res)).to.be(
15 | '46e891c182a31d005a8990ac5d61bb2124ffe5927fa008a739a9b0d217c79717'
16 | );
17 | });
18 | });
19 |
20 | it('decrypts two rounds', () => {
21 | return KeyEncryptorAes.encrypt(data, key, 2).then((res) => {
22 | expect(ByteUtils.bytesToHex(res)).to.be(
23 | '1818f732cb1a933911ec90baed252d388980cd3665e1009705e5007aa48ad916'
24 | );
25 | });
26 | });
27 |
28 | it('decrypts many rounds', () => {
29 | return KeyEncryptorAes.encrypt(data, key, 10021).then((res) => {
30 | expect(ByteUtils.bytesToHex(res)).to.be(
31 | '64d62f7ec4a363ff0fbb4520163b478ef4d0d631b690a2e7daa6bc09bca092df'
32 | );
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "kdbxweb",
3 | "projectOwner": "kdbxweb",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": ["README.md"],
7 | "imageSize": 40,
8 | "commit": true,
9 | "commitConvention": "angular",
10 | "contributors": [
11 | {
12 | "login": "antelle",
13 | "name": "Antelle",
14 | "avatar_url": "https://avatars.githubusercontent.com/u/633557?v=4",
15 | "profile": "https://gitlab.com/antelle",
16 | "contributions": ["code", "projectManagement", "fundingFinding"]
17 | },
18 | {
19 | "login": "Aetherinox",
20 | "name": "Aetherinox",
21 | "avatar_url": "https://avatars.githubusercontent.com/u/118329232?v=4",
22 | "profile": "https://gitlab.com/Aetherinox",
23 | "contributions": ["code", "projectManagement", "fundingFinding"]
24 | },
25 | {
26 | "login": "HarlemSquirrel",
27 | "name": "HarlemSquirrel",
28 | "avatar_url": "https://avatars.githubusercontent.com/u/6445815?v=4",
29 | "profile": "https://gitlab.com/HarlemSquirrel",
30 | "contributions": ["code", "projectManagement"]
31 | }
32 | ],
33 | "contributorsPerLine": 7,
34 | "linkToUsage": false,
35 | "skipCi": true
36 | }
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # #
2 | # Windows image file caches
3 | # #
4 |
5 | Thumbs.db
6 | ehthumbs.db
7 |
8 | # #
9 | # Folder config file
10 | # #
11 |
12 | Desktop.ini
13 |
14 | # #
15 | # Recycle Bin used on file shares
16 | # #
17 |
18 | $RECYCLE.BIN/
19 |
20 | # #
21 | # Windows Installer files
22 | # #
23 |
24 | *.cab
25 | *.msi
26 | *.msm
27 | *.msp
28 |
29 |
30 | # #
31 | # Windows shortcuts
32 | # #
33 |
34 | *.lnk
35 |
36 | # #
37 | # Operating System Files
38 | # #
39 |
40 | .DS_Store
41 | .AppleDouble
42 |
43 | # #
44 | # Other
45 | # #
46 |
47 | .Spotlight-V100
48 | .Trashes
49 | *.log
50 | tmp/
51 | *.user
52 | bin/
53 | *.suo
54 | dist
55 | **/dist
56 | .opt-*
57 | workspace.xml
58 |
59 | # #
60 | # Directories potentially created on remote AFP share
61 | # #
62 |
63 | .AppleDB
64 | .AppleDesktop
65 | Network Trash Folder
66 | Temporary Items
67 | .apdisk
68 |
69 | # #
70 | # Tests and coverage
71 | # #
72 |
73 | .nyc_output
74 | *coverage
75 | .coverage*
76 | test/dist
77 |
78 | # #
79 | # Keeweb specific folders
80 | # #
81 |
82 | .dev
83 | .vscode
84 | .env
85 | .aetherx
86 | keys
87 |
88 | # #
89 | # Intellij
90 | # #
91 |
92 | .idea/
93 | *.iml
94 |
95 | # #
96 | # NodeJS
97 | # #
98 |
99 | **/node_modules/
100 | npm-debug.log
101 | .env
102 | .aws
103 |
104 | # #
105 | # Distribution
106 | # #
107 |
108 | dist/
109 |
--------------------------------------------------------------------------------
/lib/format/kdbx-deleted-object.ts:
--------------------------------------------------------------------------------
1 | import * as XmlUtils from '../utils/xml-utils';
2 | import * as XmlNames from '../defs/xml-names';
3 | import { KdbxUuid } from './kdbx-uuid';
4 | import { KdbxContext } from './kdbx-context';
5 |
6 | export class KdbxDeletedObject {
7 | uuid: KdbxUuid | undefined;
8 | deletionTime: Date | undefined;
9 |
10 | private readNode(node: Element): void {
11 | switch (node.tagName) {
12 | case XmlNames.Elem.Uuid:
13 | this.uuid = XmlUtils.getUuid(node);
14 | break;
15 | case XmlNames.Elem.DeletionTime:
16 | this.deletionTime = XmlUtils.getDate(node);
17 | break;
18 | }
19 | }
20 |
21 | write(parentNode: Node, ctx: KdbxContext): void {
22 | const node = XmlUtils.addChildNode(parentNode, XmlNames.Elem.DeletedObject);
23 | XmlUtils.setUuid(XmlUtils.addChildNode(node, XmlNames.Elem.Uuid), this.uuid);
24 | ctx.setXmlDate(XmlUtils.addChildNode(node, XmlNames.Elem.DeletionTime), this.deletionTime);
25 | }
26 |
27 | static read(xmlNode: Node): KdbxDeletedObject {
28 | const obj = new KdbxDeletedObject();
29 | for (let i = 0, cn = xmlNode.childNodes, len = cn.length; i < len; i++) {
30 | const childNode = cn[i];
31 | if (childNode.tagName) {
32 | obj.readNode(childNode);
33 | }
34 | }
35 | return obj;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/format/kdbx-uuid.ts:
--------------------------------------------------------------------------------
1 | import { base64ToBytes, bytesToBase64 } from '../utils/byte-utils';
2 | import { ErrorCodes } from '../defs/consts';
3 | import { KdbxError } from '../errors/kdbx-error';
4 | import * as CryptoEngine from '../crypto/crypto-engine';
5 |
6 | const UuidLength = 16;
7 | const EmptyUuidStr = 'AAAAAAAAAAAAAAAAAAAAAA==';
8 |
9 | export class KdbxUuid {
10 | readonly id: string;
11 | readonly empty: boolean;
12 |
13 | constructor(ab?: ArrayBuffer | string) {
14 | if (ab === undefined) {
15 | ab = new ArrayBuffer(UuidLength);
16 | } else if (typeof ab === 'string') {
17 | ab = base64ToBytes(ab);
18 | }
19 | if (ab.byteLength !== UuidLength) {
20 | throw new KdbxError(ErrorCodes.FileCorrupt, `bad UUID length: ${ab.byteLength}`);
21 | }
22 | this.id = bytesToBase64(ab);
23 | this.empty = this.id === EmptyUuidStr;
24 | }
25 |
26 | equals(other: KdbxUuid | string | null | undefined): boolean {
27 | return (other && other.toString() === this.toString()) || false;
28 | }
29 |
30 | get bytes(): ArrayBuffer {
31 | return this.toBytes();
32 | }
33 |
34 | static random(): KdbxUuid {
35 | return new KdbxUuid(CryptoEngine.random(UuidLength));
36 | }
37 |
38 | toString(): string {
39 | return this.id;
40 | }
41 |
42 | valueOf(): string {
43 | return this.id;
44 | }
45 |
46 | toBytes(): ArrayBuffer {
47 | return base64ToBytes(this.id);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/scripts/make-big-files.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import { Credentials, CryptoEngine, Kdbx, ProtectedValue } from '../lib';
3 | import { argon2 } from '../test/test-support/argon2';
4 |
5 | CryptoEngine.setArgon2Impl(argon2);
6 |
7 | const GroupsCount = 100;
8 | const EntriesCount = 10000;
9 |
10 | const fileName = `${GroupsCount}G-${EntriesCount}E`;
11 |
12 | const credentials = new Credentials(ProtectedValue.fromString(''));
13 | const db = Kdbx.create(credentials, fileName);
14 | const groups = [db.getDefaultGroup()];
15 | for (let i = 0; i < GroupsCount; i++) {
16 | const parentGroup = groups[Math.floor(Math.random() * groups.length)];
17 | const group = db.createGroup(parentGroup, `Group ${i}`);
18 | groups.push(group);
19 | }
20 |
21 | for (let i = 0; i < EntriesCount; i++) {
22 | const parentGroup = groups[Math.floor(Math.random() * groups.length)];
23 | const entry = db.createEntry(parentGroup);
24 | entry.fields.set('Title', `Entry ${i}`);
25 | if (Math.random() < 0.5) {
26 | entry.fields.set('UserName', `User ${i}`);
27 | }
28 | if (Math.random() < 0.5) {
29 | entry.fields.set('Password', ProtectedValue.fromString(`Password ${i}`));
30 | }
31 | if (Math.random() < 0.5) {
32 | entry.fields.set('URL', `http://website${i}.com`);
33 | }
34 | }
35 |
36 | db.save()
37 | .then((data) => {
38 | console.log('Done, generated', fileName + '.kdbx');
39 | fs.writeFileSync(fileName + '.kdbx', Buffer.from(data));
40 | })
41 | .catch((e) => console.error(e));
42 |
--------------------------------------------------------------------------------
/scripts/dump-header.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import {
3 | Kdbx,
4 | KdbxUuid,
5 | VarDictionary,
6 | Int64,
7 | ByteUtils,
8 | BinaryStream,
9 | KdbxContext,
10 | KdbxHeader
11 | } from '../lib';
12 |
13 | if (process.argv.length < 3) {
14 | console.log('Usage: npm run script:dump-header path/to-file.kdbx');
15 | process.exit(1);
16 | }
17 |
18 | const filePath = process.argv[2];
19 | const file = new Uint8Array(fs.readFileSync(filePath)).buffer;
20 |
21 | const kdbx = new Kdbx();
22 | const ctx = new KdbxContext({ kdbx });
23 | const stm = new BinaryStream(file);
24 | const header = KdbxHeader.read(stm, ctx);
25 |
26 | for (const [field, value] of Object.entries(header)) {
27 | console.log(`${field}:`, presentValue(value));
28 | }
29 |
30 | function presentValue(value: any): any {
31 | if (value instanceof ArrayBuffer) {
32 | return ByteUtils.bytesToBase64(value);
33 | } else if (value instanceof KdbxUuid) {
34 | return value.toString();
35 | } else if (value instanceof Int64) {
36 | return value.value;
37 | } else if (value instanceof VarDictionary) {
38 | const obj: { [name: string]: any } = {};
39 | for (const key of value.keys()) {
40 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
41 | obj[key] = presentValue(value.get(key));
42 | }
43 | return obj;
44 | } else {
45 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
46 | return value;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/crypto/hashed-block-transform.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils, Consts, HashedBlockTransform, KdbxError } from '../../lib';
3 |
4 | describe('HashedBlockTransform', () => {
5 | it('decrypts and encrypts data', () => {
6 | const src = new Uint8Array([1, 2, 3, 4, 5]);
7 | return HashedBlockTransform.encrypt(src.buffer).then((enc) => {
8 | return HashedBlockTransform.decrypt(enc).then((dec) => {
9 | dec = new Uint8Array(dec);
10 | expect(dec).to.be.eql(src);
11 | });
12 | });
13 | });
14 |
15 | it('decrypts several blocks', () => {
16 | const src = new Uint8Array(1024 * 1024 * 2 + 2);
17 | for (let i = 0; i < src.length; i++) {
18 | src[i] = i % 256;
19 | }
20 | return HashedBlockTransform.encrypt(src.buffer).then((enc) => {
21 | return HashedBlockTransform.decrypt(enc).then((dec) => {
22 | expect(ByteUtils.bytesToBase64(dec)).to.be(ByteUtils.bytesToBase64(src));
23 | });
24 | });
25 | });
26 |
27 | it('throws error for invalid hash block', () => {
28 | const src = new Uint8Array([1, 2, 3, 4, 5]);
29 | return HashedBlockTransform.encrypt(src.buffer).then((enc) => {
30 | new Uint8Array(enc)[4] = 0;
31 | return HashedBlockTransform.decrypt(enc)
32 | .then(() => {
33 | throw 'We should not get here';
34 | })
35 | .catch((e) => {
36 | expect(e).to.be.a(KdbxError);
37 | expect(e.code).to.be(Consts.ErrorCodes.FileCorrupt);
38 | });
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/test/utils/int64.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { Int64 } from '../../lib';
3 |
4 | describe('Int64', () => {
5 | it('creates empty int64', () => {
6 | const i = new Int64();
7 | expect(i.hi).to.be(0);
8 | expect(i.lo).to.be(0);
9 | expect(i.value).to.be(0);
10 | expect(i.valueOf()).to.be(0);
11 | });
12 |
13 | it('creates int64 with low part', () => {
14 | const i = new Int64(0x123);
15 | expect(i.hi).to.be(0);
16 | expect(i.lo).to.be(0x123);
17 | expect(i.value).to.be(0x123);
18 | expect(i.valueOf()).to.be(0x123);
19 | });
20 |
21 | it('creates int64 with low and high parts', () => {
22 | const i = new Int64(0x123, 0x456);
23 | expect(i.hi).to.be(0x456);
24 | expect(i.lo).to.be(0x123);
25 | expect(i.value).to.be(0x45600000123);
26 | expect(i.valueOf()).to.be(0x45600000123);
27 | });
28 |
29 | it('creates int64 with large value', () => {
30 | const i = Int64.from(0x45600000123);
31 | expect(i.hi).to.be(0x456);
32 | expect(i.lo).to.be(0x123);
33 | expect(i.value).to.be(0x45600000123);
34 | expect(i.valueOf()).to.be(0x45600000123);
35 | });
36 |
37 | it('throws error for too high number conversion', () => {
38 | const i = new Int64(0xffffffff, 0xffffffff);
39 | expect(() => i.value).to.throwException((e) => {
40 | expect(e.message).to.be('too large number');
41 | });
42 | });
43 |
44 | it('throws error for too high number creation', () => {
45 | expect(() => {
46 | // eslint-disable-next-line no-loss-of-precision
47 | Int64.from(0xffffffffffffff);
48 | }).to.throwException((e) => {
49 | expect(e.message).to.be('too large number');
50 | });
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/test/crypto/hmac-block-transform.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils, Consts, HmacBlockTransform, KdbxError } from '../../lib';
3 |
4 | describe('HmacBlockTransform', () => {
5 | const key = ByteUtils.arrayToBuffer(
6 | ByteUtils.hexToBytes('1f5c3ef76d43e72ee2c5216c36187c799b153cab3d0cb63a6f3ecccc2627f535')
7 | );
8 |
9 | it('decrypts and encrypts data', () => {
10 | const src = new Uint8Array([1, 2, 3, 4, 5]);
11 | return HmacBlockTransform.encrypt(src.buffer, key).then((enc) => {
12 | return HmacBlockTransform.decrypt(enc, key).then((dec) => {
13 | dec = new Uint8Array(dec);
14 | expect(dec).to.be.eql(src);
15 | });
16 | });
17 | });
18 |
19 | it('decrypts several blocks', () => {
20 | const src = new Uint8Array(1024 * 1024 * 2 + 2);
21 | for (let i = 0; i < src.length; i++) {
22 | src[i] = i % 256;
23 | }
24 | return HmacBlockTransform.encrypt(src.buffer, key).then((enc) => {
25 | return HmacBlockTransform.decrypt(enc, key).then((dec) => {
26 | expect(ByteUtils.bytesToBase64(dec)).to.be(ByteUtils.bytesToBase64(src));
27 | });
28 | });
29 | });
30 |
31 | it('throws error for invalid hash block', () => {
32 | const src = new Uint8Array([1, 2, 3, 4, 5]);
33 | return HmacBlockTransform.encrypt(src.buffer, key).then((enc) => {
34 | new Uint8Array(enc)[4] = 0;
35 | return HmacBlockTransform.decrypt(enc, key)
36 | .then(() => {
37 | throw 'We should not get here';
38 | })
39 | .catch((e) => {
40 | expect(e).to.be.a(KdbxError);
41 | expect(e.code).to.be(Consts.ErrorCodes.FileCorrupt);
42 | });
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/test/test-support/argon2.ts:
--------------------------------------------------------------------------------
1 | import { Argon2Type, Argon2Version } from '../../lib/crypto/crypto-engine';
2 |
3 | export function argon2(
4 | password: ArrayBuffer,
5 | salt: ArrayBuffer,
6 | memory: number,
7 | iterations: number,
8 | length: number,
9 | parallelism: number,
10 | type: Argon2Type,
11 | version: Argon2Version
12 | ): Promise {
13 | let Module = require('./argon2-asm.min');
14 | if (Module.default) {
15 | Module = Module.default;
16 | }
17 | const passwordLen = password.byteLength;
18 | password = Module.allocate(new Uint8Array(password), 'i8', Module.ALLOC_NORMAL);
19 | const saltLen = salt.byteLength;
20 | salt = Module.allocate(new Uint8Array(salt), 'i8', Module.ALLOC_NORMAL);
21 | const hash = Module.allocate(new Array(length), 'i8', Module.ALLOC_NORMAL);
22 | const encodedLen = 512;
23 | const encoded = Module.allocate(new Array(encodedLen), 'i8', Module.ALLOC_NORMAL);
24 | try {
25 | const res = Module._argon2_hash(
26 | iterations,
27 | memory,
28 | parallelism,
29 | password,
30 | passwordLen,
31 | salt,
32 | saltLen,
33 | hash,
34 | length,
35 | encoded,
36 | encodedLen,
37 | type,
38 | version
39 | );
40 | if (res) {
41 | return Promise.reject(`Argon2 error: ${res}`);
42 | }
43 | const hashArr = new Uint8Array(length);
44 | for (let i = 0; i < length; i++) {
45 | hashArr[i] = Module.HEAP8[hash + i];
46 | }
47 | Module._free(password);
48 | Module._free(salt);
49 | Module._free(hash);
50 | Module._free(encoded);
51 | return Promise.resolve(hashArr);
52 | } catch (e) {
53 | return Promise.reject(e);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/crypto/protect-salt-generator.ts:
--------------------------------------------------------------------------------
1 | import { Salsa20 } from './salsa20';
2 | import { ChaCha20 } from './chacha20';
3 | import { arrayToBuffer } from '../utils/byte-utils';
4 | import { CrsAlgorithm, ErrorCodes } from '../defs/consts';
5 | import { KdbxError } from '../errors/kdbx-error';
6 | import * as CryptoEngine from '../crypto/crypto-engine';
7 |
8 | const SalsaNonce = new Uint8Array([0xe8, 0x30, 0x09, 0x4b, 0x97, 0x20, 0x5d, 0x2a]);
9 |
10 | /**
11 | * Protect information used for decrypt and encrypt protected data fields
12 | * @constructor
13 | */
14 | export class ProtectSaltGenerator {
15 | private _algo: Salsa20 | ChaCha20;
16 |
17 | constructor(algo: Salsa20 | ChaCha20) {
18 | this._algo = algo;
19 | }
20 |
21 | getSalt(len: number): ArrayBuffer {
22 | return arrayToBuffer(this._algo.getBytes(len));
23 | }
24 |
25 | static create(
26 | key: ArrayBuffer | Uint8Array,
27 | crsAlgorithm: number
28 | ): Promise {
29 | switch (crsAlgorithm) {
30 | case CrsAlgorithm.Salsa20:
31 | return CryptoEngine.sha256(arrayToBuffer(key)).then((hash) => {
32 | const key = new Uint8Array(hash);
33 | const algo = new Salsa20(key, SalsaNonce);
34 | return new ProtectSaltGenerator(algo);
35 | });
36 | case CrsAlgorithm.ChaCha20:
37 | return CryptoEngine.sha512(arrayToBuffer(key)).then((hash) => {
38 | const key = new Uint8Array(hash, 0, 32);
39 | const nonce = new Uint8Array(hash, 32, 12);
40 | const algo = new ChaCha20(key, nonce);
41 | return new ProtectSaltGenerator(algo);
42 | });
43 | default:
44 | return Promise.reject(new KdbxError(ErrorCodes.Unsupported, 'crsAlgorithm'));
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/scripts/kdbx-to-xml.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import { Credentials, CryptoEngine, Kdbx, ProtectedValue, XmlUtils } from '../lib';
3 | import { argon2 } from '../test/test-support/argon2';
4 |
5 | CryptoEngine.setArgon2Impl(argon2);
6 |
7 | if (process.argv.length < 4) {
8 | console.log('Usage: npm run script:kdbx-to-xml path/to-file.kdbx password');
9 | console.log('To make an XML that can be imported, add "-- --importable" after password');
10 | process.exit(1);
11 | }
12 |
13 | const filePath = process.argv[2];
14 | const password = process.argv[3];
15 | const importable = process.argv.includes('--importable');
16 | const file = new Uint8Array(fs.readFileSync(filePath)).buffer;
17 | const cred = new Credentials(ProtectedValue.fromString(password));
18 |
19 | (async () => {
20 | const db = await Kdbx.load(file, cred, { preserveXml: true });
21 | if (!db.xml) {
22 | throw new Error('XML not read');
23 | }
24 | let xml: string;
25 | if (importable) {
26 | xml = await db.saveXml(true);
27 | } else {
28 | xml = XmlUtils.serialize(db.xml);
29 | }
30 | fs.writeFileSync(filePath + '.xml', xml);
31 | console.log('Done, written', filePath + '.xml');
32 | console.log(
33 | "WARNING: the XML contains raw passwords as well as other data, don't paste it anywhere!"
34 | );
35 | if (importable) {
36 | console.log('This XML can be imported in applications compatible with KeePass.');
37 | } else {
38 | console.log(
39 | "The generated XML is a raw XML from your database, if you import it, passwords won't match. " +
40 | 'If you would like to generate an XML file suitable for import, add "-- --importable" after your password: ' +
41 | 'npm run script:kdbx-to-xml path/to-file.kdbx password -- --importable'
42 | );
43 | }
44 | })().catch((e) => {
45 | console.error('Error', e);
46 | process.exit(2);
47 | });
48 |
--------------------------------------------------------------------------------
/test/test-support/test-resources.ts:
--------------------------------------------------------------------------------
1 | import { ByteUtils } from '../../lib';
2 |
3 | export const TestResources = {
4 | demoKdbx: readFile('demo.kdbx'),
5 | demoKey: readFile('demo.key'),
6 | demoXml: readFile('demo.xml'),
7 | cyrillicKdbx: readFile('cyrillic.kdbx'),
8 | binKeyKdbx: readFile('binkey.kdbx'),
9 | binKeyKey: readFile('binkey.key'),
10 | emptyPass: readFile('EmptyPass.kdbx'),
11 | emptyPassWithKeyFile: readFile('EmptyPassWithKeyFile.kdbx'),
12 | emptyPassWithKeyFileKey: readFile('EmptyPassWithKeyFile.key'),
13 | noPassWithKeyFile: readFile('NoPassWithKeyFile.kdbx'),
14 | noPassWithKeyFileKey: readFile('NoPassWithKeyFile.key'),
15 | key32: readFile('Key32.kdbx'),
16 | key32KeyFile: readFile('Key32.key'),
17 | key64: readFile('Key64.kdbx'),
18 | key64KeyFile: readFile('Key64.key'),
19 | keyWithBom: readFile('KeyWithBom.kdbx'),
20 | keyWithBomKeyFile: readFile('KeyWithBom.key'),
21 | keyV2: readFile('KeyV2.kdbx'),
22 | keyV2KeyFile: readFile('KeyV2.keyx'),
23 | argon2: readFile('Argon2.kdbx'),
24 | argon2id: readFile('Argon2id.kdbx'),
25 | argon2ChaCha: readFile('Argon2ChaCha.kdbx'),
26 | aesChaCha: readFile('AesChaCha.kdbx'),
27 | aesKdfKdbx4: readFile('AesKdfKdbx4.kdbx'),
28 | yubikey3: readFile('YubiKey3.kdbx'),
29 | yubikey4: readFile('YubiKey4.kdbx'),
30 | emptyUuidXml: readFile('empty-uuid.xml'),
31 | kdbx41: readFile('KDBX4.1.kdbx')
32 | };
33 |
34 | function readFile(name: string) {
35 | let content;
36 | try {
37 | content = require('base64-loader!../../resources/' + name);
38 | } catch (e) {
39 | content = readNodeFile('../../resources/' + name);
40 | }
41 | content = ByteUtils.arrayToBuffer(ByteUtils.base64ToBytes(content));
42 | return content;
43 | }
44 |
45 | function readNodeFile(filePath: string): Buffer {
46 | return require('fs').readFileSync(require('path').join(__dirname, filePath), 'base64');
47 | }
48 |
--------------------------------------------------------------------------------
/test/crypto/protected-salt-generator.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils, Consts, ProtectSaltGenerator } from '../../lib';
3 |
4 | describe('ProtectSaltGenerator', () => {
5 | it('generates random sequences with Salsa20', () => {
6 | return ProtectSaltGenerator.create(
7 | new Uint8Array([1, 2, 3]),
8 | Consts.CrsAlgorithm.Salsa20
9 | ).then((gen) => {
10 | let bytes = gen.getSalt(0);
11 | expect(bytes.byteLength).to.be(0);
12 | bytes = gen.getSalt(10);
13 | expect(ByteUtils.bytesToBase64(bytes)).to.be('q1l4McuyQYDcDg==');
14 | bytes = gen.getSalt(10);
15 | expect(ByteUtils.bytesToBase64(bytes)).to.be('LJTKXBjqlTS8cg==');
16 | bytes = gen.getSalt(20);
17 | expect(ByteUtils.bytesToBase64(bytes)).to.be('jKVBKKNUnieRr47Wxh0YTKn82Pw=');
18 | });
19 | });
20 |
21 | it('generates random sequences with ChaCha20', () => {
22 | return ProtectSaltGenerator.create(
23 | new Uint8Array([1, 2, 3]),
24 | Consts.CrsAlgorithm.ChaCha20
25 | ).then((gen) => {
26 | let bytes = gen.getSalt(0);
27 | expect(bytes.byteLength).to.be(0);
28 | bytes = gen.getSalt(10);
29 | expect(ByteUtils.bytesToBase64(bytes)).to.be('iUIv7m2BJN2ubQ==');
30 | bytes = gen.getSalt(10);
31 | expect(ByteUtils.bytesToBase64(bytes)).to.be('BILRgZKxaxbRzg==');
32 | bytes = gen.getSalt(20);
33 | expect(ByteUtils.bytesToBase64(bytes)).to.be('KUeBUGjNBYhAoJstSqnMXQwuD6E=');
34 | });
35 | });
36 |
37 | it('fails if the algorithm is not supported', () => {
38 | return ProtectSaltGenerator.create(new Uint8Array(0), 0)
39 | .then(() => {
40 | throw 'Not expected';
41 | })
42 | .catch((e) => {
43 | expect(e.message).to.contain('Unsupported: crsAlgorithm');
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/test/crypto/salsa20.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { Salsa20 } from '../../lib';
3 |
4 | describe('Salsa20', () => {
5 | it('transforms data', () => {
6 | const key = new Uint8Array(32);
7 | key[0] = 0x80;
8 |
9 | const nonce = new Uint8Array(8);
10 | let i;
11 |
12 | for (i = 1; i < 32; i++) {
13 | key[i] = 0;
14 | }
15 | for (i = 0; i < 8; i++) {
16 | nonce[i] = 0;
17 | }
18 |
19 | const good = [
20 | // 0..63
21 | 'e3be8fdd8beca2e3ea8ef9475b29a6e7' +
22 | '003951e1097a5c38d23b7a5fad9f6844' +
23 | 'b22c97559e2723c7cbbd3fe4fc8d9a07' +
24 | '44652a83e72a9c461876af4d7ef1a117',
25 | // 192..255
26 | '57be81f47b17d9ae7c4ff15429a73e10' +
27 | 'acf250ed3a90a93c711308a74c6216a9' +
28 | 'ed84cd126da7f28e8abf8bb63517e1ca' +
29 | '98e712f4fb2e1a6aed9fdc73291faa17',
30 | // 256..319
31 | '958211c4ba2ebd5838c635edb81f513a' +
32 | '91a294e194f1c039aeec657dce40aa7e' +
33 | '7c0af57cacefa40c9f14b71a4b3456a6' +
34 | '3e162ec7d8d10b8ffb1810d71001b618',
35 | // 448..511
36 | '696afcfd0cddcc83c7e77f11a649d79a' +
37 | 'cdc3354e9635ff137e929933a0bd6f53' +
38 | '77efa105a3a4266b7c0d089d08f1e855' +
39 | 'cc32b15b93784a36e56a76cc64bc8477',
40 |
41 | '028184aa3d60ee85d13e2f398e7569ec' +
42 | 'fccba6995436ab8891d5c20b6f3bca36' +
43 | 'edcea801715a729a4afe751d1d8fe069' +
44 | 'c24e8cfa16c4eb14f37f70ae923c0cb5'
45 | ];
46 |
47 | const state = new Salsa20(key, nonce);
48 | expect(state.getHexString(64)).to.be(good[0]);
49 | state.getBytes(128);
50 | expect(state.getHexString(64)).to.be(good[1]);
51 | expect(state.getHexString(64)).to.be(good[2]);
52 | state.getBytes(128);
53 | expect(state.getHexString(64)).to.be(good[3]);
54 | // @ts-ignore
55 | state._counterWords[0] = -1;
56 | state.getBytes(128);
57 | expect(state.getHexString(64)).to.be(good[4]);
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/conf/webpack.tests.config.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { walkSync } from '@nodelib/fs.walk';
3 |
4 | const files = walkSync('test', { entryFilter: (e) => e.name.endsWith('.ts') });
5 | const entry = files.map((f) => f.path.replace('test', '.'));
6 |
7 | /*
8 | resolve:
9 | path.join(__dirname, '../test'), tests
10 | path.join(__dirname, '../node_modules') expect.js
11 | */
12 |
13 | module.exports = {
14 | mode: 'production',
15 | context: path.join(__dirname, '../test'),
16 | entry,
17 | output: {
18 | path: path.join(__dirname, '../dist'),
19 | filename: 'kdbxweb.test.js',
20 | libraryTarget: 'umd',
21 | globalObject: 'this'
22 | },
23 | module: {
24 | rules: [
25 | {
26 | test: /\.ts$/,
27 | use: 'ts-loader',
28 | exclude: /node_modules/
29 | },
30 | {
31 | test: /argon2-asm/,
32 | loader: 'exports-loader',
33 | options: { type: 'module', exports: 'default Module' }
34 | }
35 | ]
36 | },
37 | resolve: {
38 | extensions: ['.ts', '.js'],
39 | modules: [path.join(__dirname, '../test'), path.join(__dirname, '../node_modules')],
40 | alias: {
41 | '@': path.resolve(__dirname, '../')
42 | },
43 | fallback: {
44 | console: false,
45 | process: false,
46 | Buffer: false,
47 | crypto: false,
48 | zlib: false
49 | }
50 | },
51 | node: {
52 | __filename: false,
53 | __dirname: false
54 | },
55 | externals: {
56 | fs: true,
57 | path: true,
58 | crypto: true,
59 | zlib: true,
60 | '@xmldom/xmldom': true
61 | },
62 | performance: {
63 | hints: false
64 | },
65 | stats: {
66 | builtAt: false,
67 | env: false,
68 | hash: false,
69 | colors: true,
70 | modules: true,
71 | reasons: true,
72 | children: true,
73 | warnings: false,
74 | errorDetails: false,
75 | errorStack: false,
76 | errorsCount: false,
77 | logging: false, // false, 'none' | 'error' | 'warn' | 'info' | 'log' | 'verbose'
78 | loggingTrace: false
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/scripts/kdbx-size-profiler.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import {
3 | Credentials,
4 | CryptoEngine,
5 | Kdbx,
6 | KdbxBinaries,
7 | KdbxEntry,
8 | KdbxGroup,
9 | ProtectedValue
10 | } from '../lib';
11 | import { argon2 } from '../test/test-support/argon2';
12 |
13 | CryptoEngine.setArgon2Impl(argon2);
14 |
15 | if (process.argv.length < 4) {
16 | console.log('Usage: npm run script:kdbx-size-profiler path/to-file.kdbx password');
17 | process.exit(1);
18 | }
19 |
20 | const filePath = process.argv[2];
21 | const password = process.argv[3];
22 | const file = new Uint8Array(fs.readFileSync(filePath)).buffer;
23 | const cred = new Credentials(ProtectedValue.fromString(password));
24 |
25 | (async () => {
26 | try {
27 | const db = await Kdbx.load(file, cred);
28 | const xml = await db.saveXml(false);
29 | console.log(`File size: ${file.byteLength} bytes`);
30 | console.log(`XML: ${xml.length} characters`);
31 |
32 | const binSize = [...db.binaries.getAll().values()]
33 | .map((b) => b.value.byteLength)
34 | .reduce((s, v) => s + v, 0);
35 | console.log(`Binaries: ${binSize} bytes`);
36 |
37 | const iconsSize = [...db.meta.customIcons.values()]
38 | .map((b) => b.data.byteLength)
39 | .reduce((s, v) => s + v, 0);
40 | console.log(`Custom icons: ${iconsSize} bytes`);
41 |
42 | for (const item of db.getDefaultGroup().allGroupsAndEntries()) {
43 | if (item instanceof KdbxGroup) {
44 | console.log(`Group: "${item.name}"`);
45 | } else {
46 | printEntry(item);
47 | for (const histEntry of item.history) {
48 | printEntry(histEntry, true);
49 | }
50 | }
51 | }
52 | } catch (e) {
53 | console.error('Error', e);
54 | process.exit(2);
55 | }
56 |
57 | function printEntry(entry: KdbxEntry, isHistory = false) {
58 | const fieldsSize = [...entry.fields.values()]
59 | .map((f) => (typeof f === 'string' ? f.length : f.byteLength))
60 | .reduce((s, v) => s + v, 0);
61 | const binSize = [...entry.binaries.values()]
62 | .map(
63 | (b) =>
64 | (KdbxBinaries.isKdbxBinaryWithHash(b) ? b.value.byteLength : b.byteLength) | 0
65 | )
66 | .reduce((s, v) => s + v, 0);
67 |
68 | const type = isHistory ? ' History item' : 'Entry';
69 | const title = entry.fields.get('Title') || '(no title)';
70 | let sizeStr = `${fieldsSize} bytes fields`;
71 | if (binSize) {
72 | sizeStr += `, ${binSize} bytes binaries`;
73 | }
74 | console.log(` ${type}: "${title}": ${sizeStr}`);
75 | }
76 | })().catch((e) => console.error(e));
77 |
--------------------------------------------------------------------------------
/conf/webpack.config.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as webpack from 'webpack';
3 | import TerserPlugin from 'terser-webpack-plugin';
4 |
5 | import * as pkg from '../package.json';
6 |
7 | const debug = process.argv.indexOf('--mode=development') > 0;
8 | const license = `opensource.org/licenses/${pkg.license}`;
9 | const copyright = `(c) ${new Date().getFullYear()} ${pkg.author}, ${license}`;
10 | const banner = `kdbxweb v${pkg.version}, ${copyright}`;
11 |
12 | module.exports = {
13 | context: path.join(__dirname, '../lib'),
14 | entry: './index.ts',
15 | output: {
16 | path: path.join(__dirname, '../dist'),
17 | filename: 'kdbxweb' + (debug ? '' : '.min') + '.js',
18 | library: 'kdbxweb',
19 | libraryTarget: 'umd',
20 | globalObject: 'this'
21 | },
22 | module: {
23 | rules: [
24 | {
25 | test: /\.ts$/,
26 | exclude: /node_modules/,
27 | use: {
28 | loader: 'ts-loader',
29 | options: {
30 | configFile: path.join(
31 | __dirname,
32 | `tsconfig.build-${debug ? 'debug' : 'prod'}.json`
33 | )
34 | }
35 | }
36 | }
37 | ]
38 | },
39 | resolve: {
40 | extensions: ['.ts', '.js'],
41 | modules: [path.join(__dirname, '../util'), path.join(__dirname, '../node_modules')],
42 | alias: {
43 | '@': path.resolve(__dirname, '../')
44 | },
45 | fallback: {
46 | console: false,
47 | process: false,
48 | Buffer: false,
49 | crypto: false,
50 | zlib: false
51 | }
52 | },
53 | plugins: [new webpack.BannerPlugin({ banner })],
54 | node: {
55 | __filename: false,
56 | __dirname: false
57 | },
58 | optimization: {
59 | minimize: !debug,
60 | minimizer: debug
61 | ? []
62 | : [
63 | new TerserPlugin({
64 | extractComments: false
65 | })
66 | ]
67 | },
68 | externals: {
69 | fs: true,
70 | path: true,
71 | crypto: true,
72 | zlib: true,
73 | '@xmldom/xmldom': true
74 | },
75 | performance: {
76 | hints: false
77 | },
78 | stats: {
79 | builtAt: false,
80 | env: false,
81 | hash: false,
82 | colors: true,
83 | modules: true,
84 | reasons: true,
85 | children: true,
86 | warnings: false,
87 | errorDetails: false,
88 | errorStack: false,
89 | errorsCount: false,
90 | logging: false, // false, 'none' | 'error' | 'warn' | 'info' | 'log' | 'verbose'
91 | loggingTrace: false
92 | }
93 | };
94 |
--------------------------------------------------------------------------------
/lib/format/kdbx-custom-data.ts:
--------------------------------------------------------------------------------
1 | import * as XmlUtils from '../utils/xml-utils';
2 | import * as XmlNames from '../defs/xml-names';
3 | import { KdbxContext } from './kdbx-context';
4 |
5 | export type KdbxCustomDataItem = { value: string | undefined; lastModified?: Date | undefined };
6 |
7 | export type KdbxCustomDataMap = Map;
8 |
9 | export class KdbxCustomData {
10 | static read(node: Node): KdbxCustomDataMap {
11 | const customData = new Map();
12 | for (let i = 0, cn = node.childNodes, len = cn.length; i < len; i++) {
13 | const childNode = cn[i];
14 | if (childNode.tagName === XmlNames.Elem.StringDictExItem) {
15 | this.readItem(childNode, customData);
16 | }
17 | }
18 | return customData;
19 | }
20 |
21 | static write(
22 | parentNode: Node,
23 | ctx: KdbxContext,
24 | customData: KdbxCustomDataMap | undefined
25 | ): void {
26 | if (!customData) {
27 | return;
28 | }
29 | const node = XmlUtils.addChildNode(parentNode, XmlNames.Elem.CustomData);
30 | for (const [key, item] of customData) {
31 | if (item?.value) {
32 | const itemNode = XmlUtils.addChildNode(node, XmlNames.Elem.StringDictExItem);
33 | XmlUtils.setText(XmlUtils.addChildNode(itemNode, XmlNames.Elem.Key), key);
34 | XmlUtils.setText(XmlUtils.addChildNode(itemNode, XmlNames.Elem.Value), item.value);
35 | if (item.lastModified && ctx.kdbx.versionIsAtLeast(4, 1)) {
36 | XmlUtils.setDate(
37 | XmlUtils.addChildNode(itemNode, XmlNames.Elem.LastModTime),
38 | item.lastModified
39 | );
40 | }
41 | }
42 | }
43 | }
44 |
45 | private static readItem(node: Element, customData: KdbxCustomDataMap): void {
46 | let key, value, lastModified;
47 | for (let i = 0, cn = node.childNodes, len = cn.length; i < len; i++) {
48 | const childNode = cn[i];
49 | switch (childNode.tagName) {
50 | case XmlNames.Elem.Key:
51 | key = XmlUtils.getText(childNode);
52 | break;
53 | case XmlNames.Elem.Value:
54 | value = XmlUtils.getText(childNode);
55 | break;
56 | case XmlNames.Elem.LastModTime:
57 | lastModified = XmlUtils.getDate(childNode);
58 | break;
59 | }
60 | }
61 | if (key && value !== undefined) {
62 | const item: KdbxCustomDataItem = { value };
63 | if (lastModified) {
64 | item.lastModified = lastModified;
65 | }
66 | customData.set(key, item);
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/test/test-support/subtle-mock-node.ts:
--------------------------------------------------------------------------------
1 | let SubtleMockNode;
2 |
3 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
4 |
5 | if (global.process && global.process.versions && global.process.versions.node) {
6 | const nodeCrypto = require('crypto');
7 |
8 | SubtleMockNode = {
9 | subtle: {
10 | importKey(format: any, keyData: any) {
11 | const key = new ArrayBuffer(keyData.byteLength);
12 | new Uint8Array(key).set(new Uint8Array(keyData));
13 | return Promise.resolve(key);
14 | },
15 | encrypt(algo: any, key: any, cleartext: any) {
16 | return new Promise((resolve) => {
17 | const cipher = nodeCrypto.createCipheriv(
18 | 'aes-256-cbc',
19 | Buffer.from(new Uint8Array(key)),
20 | Buffer.from(new Uint8Array(algo.iv))
21 | );
22 | let data = cipher.update(Buffer.from(new Uint8Array(cleartext)));
23 | data = new Uint8Array(Buffer.concat([data, cipher.final()])).buffer;
24 | resolve(data);
25 | });
26 | },
27 | decrypt(algo: any, key: any, cleartext: any) {
28 | return new Promise((resolve) => {
29 | const cipher = nodeCrypto.createDecipheriv(
30 | 'aes-256-cbc',
31 | Buffer.from(new Uint8Array(key)),
32 | Buffer.from(new Uint8Array(algo.iv))
33 | );
34 | let data = cipher.update(Buffer.from(new Uint8Array(cleartext)));
35 | data = new Uint8Array(Buffer.concat([data, cipher.final()])).buffer;
36 | resolve(data);
37 | });
38 | },
39 | digest(format: any, data: any) {
40 | return new Promise((resolve) => {
41 | resolve(
42 | nodeCrypto
43 | .createHash(format.name.replace('-', '').toLowerCase())
44 | .update(Buffer.from(data))
45 | .digest().buffer
46 | );
47 | });
48 | },
49 | sign(algo: any, key: any, data: any) {
50 | return new Promise((resolve) => {
51 | resolve(
52 | nodeCrypto
53 | .createHmac('sha256', Buffer.from(key))
54 | .update(Buffer.from(data))
55 | .digest().buffer
56 | );
57 | });
58 | }
59 | },
60 | getRandomValues(arr: any) {
61 | for (let i = 0; i < arr.length; i++) {
62 | arr[i] = Math.random() * 255;
63 | }
64 | }
65 | };
66 | }
67 |
68 | export { SubtleMockNode };
69 |
--------------------------------------------------------------------------------
/lib/crypto/key-encryptor-aes.ts:
--------------------------------------------------------------------------------
1 | import * as CryptoEngine from './crypto-engine';
2 | import { arrayToBuffer, zeroBuffer } from '../utils/byte-utils';
3 |
4 | const maxRoundsPreIteration = 10000;
5 | const aesBlockSize = 16;
6 | const credentialSize = 32;
7 |
8 | /*
9 | In order to simulate multiple rounds of ECB encryption, we do CBC encryption
10 | across a zero buffer of large length with the IV being the desired plaintext.
11 | The zero buffer does not contribute to the xor, so xoring the previous block
12 | with the next one simulates running ECB multiple times. We limit the maximum
13 | size of the zero buffer to prevent enormous memory usage.
14 | */
15 |
16 | export function encrypt(
17 | credentials: Uint8Array,
18 | key: Uint8Array | ArrayBuffer,
19 | rounds: number
20 | ): Promise {
21 | const algo = CryptoEngine.createAesCbc();
22 | return algo
23 | .importKey(arrayToBuffer(key))
24 | .then(() => {
25 | const resolvers = [];
26 | for (let idx = 0; idx < credentialSize; idx += aesBlockSize) {
27 | resolvers.push(
28 | encryptBlock(algo, credentials.subarray(idx, idx + aesBlockSize), rounds)
29 | );
30 | }
31 | return Promise.all(resolvers);
32 | })
33 | .then((results) => {
34 | const res = new Uint8Array(credentialSize);
35 | results.forEach((result, idx) => {
36 | const base = idx * aesBlockSize;
37 | for (let i = 0; i < aesBlockSize; ++i) {
38 | res[i + base] = result[i];
39 | }
40 | zeroBuffer(result);
41 | });
42 | return res;
43 | });
44 | }
45 |
46 | function encryptBlock(
47 | algo: CryptoEngine.AesCbc,
48 | iv: Uint8Array | ArrayBuffer,
49 | rounds: number
50 | ): Promise {
51 | let result = Promise.resolve(arrayToBuffer(iv));
52 | const buffer = new Uint8Array(aesBlockSize * Math.min(rounds, maxRoundsPreIteration));
53 |
54 | while (rounds > 0) {
55 | const currentRounds = Math.min(rounds, maxRoundsPreIteration);
56 | rounds -= currentRounds;
57 |
58 | const dataLen = aesBlockSize * currentRounds;
59 | const zeroData =
60 | buffer.length === dataLen ? buffer.buffer : arrayToBuffer(buffer.subarray(0, dataLen));
61 | result = encryptBlockBuffer(algo, result, zeroData);
62 | }
63 |
64 | return result.then((res) => {
65 | return new Uint8Array(res);
66 | });
67 | }
68 |
69 | function encryptBlockBuffer(
70 | algo: CryptoEngine.AesCbc,
71 | promisedIv: Promise,
72 | buffer: ArrayBuffer
73 | ): Promise {
74 | return promisedIv
75 | .then((iv) => {
76 | return algo.encrypt(buffer, iv);
77 | })
78 | .then((buf) => {
79 | const res = arrayToBuffer(
80 | new Uint8Array(buf).subarray(-2 * aesBlockSize, -aesBlockSize)
81 | );
82 | zeroBuffer(buf);
83 | return res;
84 | });
85 | }
86 |
--------------------------------------------------------------------------------
/lib/utils/byte-utils.ts:
--------------------------------------------------------------------------------
1 | const textEncoder = new TextEncoder();
2 | const textDecoder = new TextDecoder();
3 |
4 | type ArrayBufferOrArray = ArrayBuffer | Uint8Array;
5 |
6 | export function arrayBufferEquals(ab1: ArrayBuffer, ab2: ArrayBuffer): boolean {
7 | if (ab1.byteLength !== ab2.byteLength) {
8 | return false;
9 | }
10 | const arr1 = new Uint8Array(ab1);
11 | const arr2 = new Uint8Array(ab2);
12 | for (let i = 0, len = arr1.length; i < len; i++) {
13 | if (arr1[i] !== arr2[i]) {
14 | return false;
15 | }
16 | }
17 | return true;
18 | }
19 |
20 | export function bytesToString(arr: ArrayBufferOrArray): string {
21 | if (arr instanceof ArrayBuffer) {
22 | arr = new Uint8Array(arr);
23 | }
24 | return textDecoder.decode(arr);
25 | }
26 |
27 | export function stringToBytes(str: string): Uint8Array {
28 | return textEncoder.encode(str);
29 | }
30 |
31 | export function base64ToBytes(str: string): Uint8Array {
32 | if (typeof atob === 'function') {
33 | const byteStr = atob(str);
34 | const arr = new Uint8Array(byteStr.length);
35 | for (let i = 0; i < byteStr.length; i++) {
36 | arr[i] = byteStr.charCodeAt(i);
37 | }
38 | return arr;
39 | } else {
40 | const buffer = Buffer.from(str, 'base64');
41 | return new Uint8Array(buffer);
42 | }
43 | }
44 |
45 | export function bytesToBase64(arr: ArrayBufferOrArray): string {
46 | const intArr = arr instanceof ArrayBuffer ? new Uint8Array(arr) : arr;
47 | if (typeof btoa === 'function') {
48 | let str = '';
49 | for (let i = 0; i < intArr.length; i++) {
50 | str += String.fromCharCode(intArr[i]);
51 | }
52 | return btoa(str);
53 | } else {
54 | const buffer = Buffer.from(arr);
55 | return buffer.toString('base64');
56 | }
57 | }
58 |
59 | export function hexToBytes(hex: string): Uint8Array {
60 | const arr = new Uint8Array(Math.ceil(hex.length / 2));
61 | for (let i = 0; i < arr.length; i++) {
62 | arr[i] = parseInt(hex.substr(i * 2, 2), 16);
63 | }
64 | return arr;
65 | }
66 |
67 | export function bytesToHex(arr: ArrayBufferOrArray): string {
68 | const intArr = arr instanceof ArrayBuffer ? new Uint8Array(arr) : arr;
69 | let str = '';
70 | for (let i = 0; i < intArr.length; i++) {
71 | const byte = intArr[i].toString(16);
72 | if (byte.length === 1) {
73 | str += '0';
74 | }
75 | str += byte;
76 | }
77 | return str;
78 | }
79 |
80 | export function arrayToBuffer(arr: ArrayBufferOrArray): ArrayBuffer {
81 | if (arr instanceof ArrayBuffer) {
82 | return arr;
83 | }
84 | const ab = arr.buffer;
85 | if (arr.byteOffset === 0 && arr.byteLength === ab.byteLength) {
86 | return ab;
87 | }
88 | return arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
89 | }
90 |
91 | export function zeroBuffer(arr: ArrayBufferOrArray): void {
92 | const intArr = arr instanceof ArrayBuffer ? new Uint8Array(arr) : arr;
93 | intArr.fill(0);
94 | }
95 |
--------------------------------------------------------------------------------
/lib/defs/consts.ts:
--------------------------------------------------------------------------------
1 | export const Signatures = {
2 | FileMagic: 0x9aa2d903,
3 | Sig2Kdbx: 0xb54bfb67,
4 | Sig2Kdb: 0xb54bfb65
5 | } as const;
6 |
7 | export const ErrorCodes = {
8 | NotImplemented: 'NotImplemented',
9 | InvalidArg: 'InvalidArg',
10 | BadSignature: 'BadSignature',
11 | InvalidVersion: 'InvalidVersion',
12 | Unsupported: 'Unsupported',
13 | FileCorrupt: 'FileCorrupt',
14 | InvalidKey: 'InvalidKey',
15 | MergeError: 'MergeError',
16 | InvalidState: 'InvalidState'
17 | } as const;
18 |
19 | export const CompressionAlgorithm = {
20 | None: 0,
21 | GZip: 1
22 | } as const;
23 |
24 | export const CrsAlgorithm = {
25 | Null: 0,
26 | ArcFourVariant: 1,
27 | Salsa20: 2,
28 | ChaCha20: 3
29 | } as const;
30 |
31 | export const KdfId = {
32 | Argon2: '72Nt34wpREuR96mkA+MKDA==',
33 | Argon2d: '72Nt34wpREuR96mkA+MKDA==',
34 | Argon2id: 'nimLGVbbR3OyPfw+xvCh5g==',
35 | Aes: 'ydnzmmKKRGC/dA0IwYpP6g=='
36 | } as const;
37 |
38 | export const CipherId = {
39 | Aes: 'McHy5r9xQ1C+WAUhavxa/w==',
40 | ChaCha20: '1gOKK4tvTLWlJDOaMdu1mg=='
41 | } as const;
42 |
43 | export const AutoTypeObfuscationOptions = {
44 | None: 0,
45 | UseClipboard: 1
46 | } as const;
47 |
48 | export const Defaults = {
49 | KeyEncryptionRounds: 300000,
50 | MntncHistoryDays: 365,
51 | HistoryMaxItems: 10,
52 | HistoryMaxSize: 6 * 1024 * 1024,
53 | RecycleBinName: 'Recycle Bin'
54 | } as const;
55 |
56 | export const Icons = {
57 | Key: 0,
58 | World: 1,
59 | Warning: 2,
60 | NetworkServer: 3,
61 | MarkedDirectory: 4,
62 | UserCommunication: 5,
63 | Parts: 6,
64 | Notepad: 7,
65 | WorldSocket: 8,
66 | Identity: 9,
67 | PaperReady: 10,
68 | Digicam: 11,
69 | IRCommunication: 12,
70 | MultiKeys: 13,
71 | Energy: 14,
72 | Scanner: 15,
73 | WorldStar: 16,
74 | CDRom: 17,
75 | Monitor: 18,
76 | EMail: 19,
77 | Configuration: 20,
78 | ClipboardReady: 21,
79 | PaperNew: 22,
80 | Screen: 23,
81 | EnergyCareful: 24,
82 | EMailBox: 25,
83 | Disk: 26,
84 | Drive: 27,
85 | PaperQ: 28,
86 | TerminalEncrypted: 29,
87 | Console: 30,
88 | Printer: 31,
89 | ProgramIcons: 32,
90 | Run: 33,
91 | Settings: 34,
92 | WorldComputer: 35,
93 | Archive: 36,
94 | Homebanking: 37,
95 | DriveWindows: 38,
96 | Clock: 39,
97 | EMailSearch: 40,
98 | PaperFlag: 41,
99 | Memory: 42,
100 | TrashBin: 43,
101 | Note: 44,
102 | Expired: 45,
103 | Info: 46,
104 | Package: 47,
105 | Folder: 48,
106 | FolderOpen: 49,
107 | FolderPackage: 50,
108 | LockOpen: 51,
109 | PaperLocked: 52,
110 | Checked: 53,
111 | Pen: 54,
112 | Thumbnail: 55,
113 | Book: 56,
114 | List: 57,
115 | UserKey: 58,
116 | Tool: 59,
117 | Home: 60,
118 | Star: 61,
119 | Tux: 62,
120 | Feather: 63,
121 | Apple: 64,
122 | Wiki: 65,
123 | Money: 66,
124 | Certificate: 67,
125 | BlackBerry: 68
126 | } as const;
127 |
--------------------------------------------------------------------------------
/test/crypto/protected-value.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils, ProtectedValue } from '../../lib';
3 |
4 | describe('ProtectedValue', () => {
5 | const valueBytes = ByteUtils.stringToBytes('strvalue'),
6 | encValueBytes = ByteUtils.stringToBytes('strvalue'),
7 | saltBytes = new Uint8Array(valueBytes.length);
8 | for (let i = 0; i < saltBytes.length; i++) {
9 | saltBytes[i] = i;
10 | encValueBytes[i] ^= i;
11 | }
12 |
13 | it('decrypts salted value in string', () => {
14 | const value = new ProtectedValue(encValueBytes, saltBytes);
15 | expect(value.getText()).to.be('strvalue');
16 | });
17 |
18 | it('returns string in binary', () => {
19 | const value = new ProtectedValue(encValueBytes, saltBytes);
20 | expect(value.getBinary()).to.be.eql(valueBytes);
21 | });
22 |
23 | it('checks substring', () => {
24 | const value = new ProtectedValue(encValueBytes, saltBytes);
25 | expect(value.includes('test')).to.be(false);
26 | expect(value.includes('str')).to.be(true);
27 | expect(value.includes('val')).to.be(true);
28 | expect(value.includes('value')).to.be(true);
29 | expect(value.includes('')).to.be(false);
30 | });
31 |
32 | it('calculates SHA512 hash', () => {
33 | const value = new ProtectedValue(encValueBytes, saltBytes);
34 | return value.getHash().then((hash) => {
35 | expect(ByteUtils.bytesToHex(hash)).to.be(
36 | '1f5c3ef76d43e72ee2c5216c36187c799b153cab3d0cb63a6f3ecccc2627f535'
37 | );
38 | });
39 | });
40 |
41 | it('creates value from string', () => {
42 | const value = ProtectedValue.fromString('test');
43 | expect(value.getText()).to.be('test');
44 | });
45 |
46 | it('creates value from binary', () => {
47 | const value = ProtectedValue.fromBinary(ByteUtils.stringToBytes('test'));
48 | expect(value.getText()).to.be('test');
49 | });
50 |
51 | it('returns byte length', () => {
52 | const value = ProtectedValue.fromBinary(ByteUtils.stringToBytes('test'));
53 | expect(value.byteLength).to.be(4);
54 | });
55 |
56 | it('can change salt', () => {
57 | const value = ProtectedValue.fromString('test');
58 | expect(value.getText()).to.be('test');
59 | value.setSalt(new Uint8Array([1, 2, 3, 4]).buffer);
60 | expect(value.getText()).to.be('test');
61 | });
62 |
63 | it('returns protected value as base64 string', () => {
64 | const value = ProtectedValue.fromBinary(ByteUtils.stringToBytes('test'));
65 | value.setSalt(new Uint8Array([1, 2, 3, 4]).buffer);
66 | expect(value.toString()).to.be('dWdwcA==');
67 | });
68 |
69 | it('clones itself', () => {
70 | const value = ProtectedValue.fromString('test').clone();
71 | expect(value.getText()).to.be('test');
72 | });
73 |
74 | it('creates a value from base64', () => {
75 | const value = ProtectedValue.fromBase64('aGVsbG8=');
76 | expect(value.getText()).to.be('hello');
77 | });
78 |
79 | it('returns base64 of the value', () => {
80 | const value = ProtectedValue.fromString('hello');
81 | expect(value.toBase64()).to.be('aGVsbG8=');
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | import { ChaCha20 } from './crypto/chacha20';
2 | import * as CryptoEngine from './crypto/crypto-engine';
3 | import * as HashedBlockTransform from './crypto/hashed-block-transform';
4 | import * as HmacBlockTransform from './crypto/hmac-block-transform';
5 | import * as KeyEncryptorAes from './crypto/key-encryptor-aes';
6 | import * as KeyEncryptorKdf from './crypto/key-encryptor-kdf';
7 | import { ProtectSaltGenerator } from './crypto/protect-salt-generator';
8 | import { ProtectedValue } from './crypto/protected-value';
9 | import { Salsa20 } from './crypto/salsa20';
10 |
11 | import * as Consts from './defs/consts';
12 | import * as XmlNames from './defs/xml-names';
13 |
14 | import { KdbxError } from './errors/kdbx-error';
15 |
16 | import { Kdbx, KdbxEditState } from './format/kdbx';
17 | import {
18 | KdbxBinaries,
19 | KdbxBinary,
20 | KdbxBinaryIn,
21 | KdbxBinaryOrRef,
22 | KdbxBinaryRef,
23 | KdbxBinaryRefWithValue,
24 | KdbxBinaryWithHash
25 | } from './format/kdbx-binaries';
26 | import { KdbxContext } from './format/kdbx-context';
27 | import { KdbxChallengeResponseFn, KdbxCredentials } from './format/kdbx-credentials';
28 | import { KdbxCustomData, KdbxCustomDataMap, KdbxCustomDataItem } from './format/kdbx-custom-data';
29 | import { KdbxDeletedObject } from './format/kdbx-deleted-object';
30 | import {
31 | KdbxAutoTypeItem,
32 | KdbxEntry,
33 | KdbxEntryAutoType,
34 | KdbxEntryEditState,
35 | KdbxEntryField
36 | } from './format/kdbx-entry';
37 | import { KdbxFormat } from './format/kdbx-format';
38 | import { KdbxGroup } from './format/kdbx-group';
39 | import { KdbxHeader } from './format/kdbx-header';
40 | import {
41 | KdbxMemoryProtection,
42 | KdbxMeta,
43 | KdbxMetaEditState,
44 | KdbxCustomIcon
45 | } from './format/kdbx-meta';
46 | import { KdbxTimes } from './format/kdbx-times';
47 | import { KdbxUuid } from './format/kdbx-uuid';
48 |
49 | import { BinaryStream } from './utils/binary-stream';
50 | import * as ByteUtils from './utils/byte-utils';
51 | import { Int64 } from './utils/int64';
52 | import { VarDictionary } from './utils/var-dictionary';
53 | import * as XmlUtils from './utils/xml-utils';
54 |
55 | export {
56 | ChaCha20,
57 | CryptoEngine,
58 | HashedBlockTransform,
59 | HmacBlockTransform,
60 | KeyEncryptorAes,
61 | KeyEncryptorKdf,
62 | ProtectSaltGenerator,
63 | ProtectedValue,
64 | Salsa20,
65 | Consts,
66 | XmlNames,
67 | KdbxError,
68 | Kdbx,
69 | KdbxEditState,
70 | KdbxBinaries,
71 | KdbxBinaryRef,
72 | KdbxBinaryRefWithValue,
73 | KdbxBinaryWithHash,
74 | KdbxBinary,
75 | KdbxBinaryOrRef,
76 | KdbxBinaryIn,
77 | KdbxContext,
78 | KdbxCredentials,
79 | KdbxCredentials as Credentials,
80 | KdbxChallengeResponseFn,
81 | KdbxCustomData,
82 | KdbxCustomDataMap,
83 | KdbxCustomDataItem,
84 | KdbxDeletedObject,
85 | KdbxEntry,
86 | KdbxEntryEditState,
87 | KdbxEntryField,
88 | KdbxAutoTypeItem,
89 | KdbxEntryAutoType,
90 | KdbxFormat,
91 | KdbxGroup,
92 | KdbxHeader,
93 | KdbxMeta,
94 | KdbxMetaEditState,
95 | KdbxCustomIcon,
96 | KdbxMemoryProtection,
97 | KdbxTimes,
98 | KdbxUuid,
99 | BinaryStream,
100 | ByteUtils,
101 | Int64,
102 | VarDictionary,
103 | XmlUtils
104 | };
105 |
--------------------------------------------------------------------------------
/lib/defs/xml-names.ts:
--------------------------------------------------------------------------------
1 | export const Elem = {
2 | DocNode: 'KeePassFile',
3 |
4 | Meta: 'Meta',
5 | Root: 'Root',
6 | Group: 'Group',
7 | Entry: 'Entry',
8 |
9 | Generator: 'Generator',
10 | HeaderHash: 'HeaderHash',
11 | SettingsChanged: 'SettingsChanged',
12 | DbName: 'DatabaseName',
13 | DbNameChanged: 'DatabaseNameChanged',
14 | DbDesc: 'DatabaseDescription',
15 | DbDescChanged: 'DatabaseDescriptionChanged',
16 | DbDefaultUser: 'DefaultUserName',
17 | DbDefaultUserChanged: 'DefaultUserNameChanged',
18 | DbMntncHistoryDays: 'MaintenanceHistoryDays',
19 | DbColor: 'Color',
20 | DbKeyChanged: 'MasterKeyChanged',
21 | DbKeyChangeRec: 'MasterKeyChangeRec',
22 | DbKeyChangeForce: 'MasterKeyChangeForce',
23 | RecycleBinEnabled: 'RecycleBinEnabled',
24 | RecycleBinUuid: 'RecycleBinUUID',
25 | RecycleBinChanged: 'RecycleBinChanged',
26 | EntryTemplatesGroup: 'EntryTemplatesGroup',
27 | EntryTemplatesGroupChanged: 'EntryTemplatesGroupChanged',
28 | HistoryMaxItems: 'HistoryMaxItems',
29 | HistoryMaxSize: 'HistoryMaxSize',
30 | LastSelectedGroup: 'LastSelectedGroup',
31 | LastTopVisibleGroup: 'LastTopVisibleGroup',
32 |
33 | MemoryProt: 'MemoryProtection',
34 | ProtTitle: 'ProtectTitle',
35 | ProtUserName: 'ProtectUserName',
36 | ProtPassword: 'ProtectPassword',
37 | ProtUrl: 'ProtectURL',
38 | ProtNotes: 'ProtectNotes',
39 |
40 | CustomIcons: 'CustomIcons',
41 | CustomIconItem: 'Icon',
42 | CustomIconItemID: 'UUID',
43 | CustomIconItemData: 'Data',
44 | CustomIconItemName: 'Name',
45 |
46 | AutoType: 'AutoType',
47 | History: 'History',
48 |
49 | Name: 'Name',
50 | Notes: 'Notes',
51 | Uuid: 'UUID',
52 | Icon: 'IconID',
53 | CustomIconID: 'CustomIconUUID',
54 | FgColor: 'ForegroundColor',
55 | BgColor: 'BackgroundColor',
56 | OverrideUrl: 'OverrideURL',
57 | Times: 'Times',
58 | Tags: 'Tags',
59 | QualityCheck: 'QualityCheck',
60 | PreviousParentGroup: 'PreviousParentGroup',
61 |
62 | CreationTime: 'CreationTime',
63 | LastModTime: 'LastModificationTime',
64 | LastAccessTime: 'LastAccessTime',
65 | ExpiryTime: 'ExpiryTime',
66 | Expires: 'Expires',
67 | UsageCount: 'UsageCount',
68 | LocationChanged: 'LocationChanged',
69 |
70 | GroupDefaultAutoTypeSeq: 'DefaultAutoTypeSequence',
71 | EnableAutoType: 'EnableAutoType',
72 | EnableSearching: 'EnableSearching',
73 |
74 | String: 'String',
75 | Binary: 'Binary',
76 | Key: 'Key',
77 | Value: 'Value',
78 |
79 | AutoTypeEnabled: 'Enabled',
80 | AutoTypeObfuscation: 'DataTransferObfuscation',
81 | AutoTypeDefaultSeq: 'DefaultSequence',
82 | AutoTypeItem: 'Association',
83 | Window: 'Window',
84 | KeystrokeSequence: 'KeystrokeSequence',
85 |
86 | Binaries: 'Binaries',
87 |
88 | IsExpanded: 'IsExpanded',
89 | LastTopVisibleEntry: 'LastTopVisibleEntry',
90 |
91 | DeletedObjects: 'DeletedObjects',
92 | DeletedObject: 'DeletedObject',
93 | DeletionTime: 'DeletionTime',
94 |
95 | CustomData: 'CustomData',
96 | StringDictExItem: 'Item'
97 | } as const;
98 |
99 | export const Attr = {
100 | Id: 'ID',
101 | Ref: 'Ref',
102 | Protected: 'Protected',
103 | ProtectedInMemPlainXml: 'ProtectInMemory',
104 | Compressed: 'Compressed'
105 | } as const;
106 |
107 | export const Val = {
108 | False: 'False',
109 | True: 'True'
110 | } as const;
111 |
--------------------------------------------------------------------------------
/.github/workflows/CI.yaml:
--------------------------------------------------------------------------------
1 | name: '🔨 Build › CI'
2 | run-name: '🔨 Build › CI'
3 |
4 | on:
5 |
6 | # #
7 | # Trigger › Workflow Dispatch
8 | #
9 | # If any values are not provided, will use fallback env variable
10 | # #
11 |
12 | workflow_dispatch:
13 | inputs:
14 |
15 | # #
16 | # Name of the plugin to use when creating the release zip / exe filename
17 | # e.g: kdbxweb-v1.0.0.zip
18 | # #
19 |
20 | PLUGIN_NAME:
21 | description: "📦 Name of App"
22 | required: true
23 | default: 'kdbxweb'
24 | type: string
25 |
26 | # #
27 | # Trigger › Push
28 | # #
29 |
30 | push:
31 | branches:
32 | - master
33 | - main
34 | tags:
35 | - '*'
36 |
37 | # #
38 | # Trigger › Pull Requests
39 | # #
40 |
41 | pull_request:
42 |
43 | # #
44 | # Environment Vars
45 | #
46 | # PLUGIN_NAME This is the project name used in Cloudflare.
47 | # #
48 |
49 | env:
50 | PLUGIN_NAME: ${{ github.event.inputs.PLUGIN_NAME || 'kdbxweb' }}
51 | BOT_NAME_1: EuropaServ
52 | BOT_NAME_DEPENDABOT: dependabot[bot]
53 |
54 | # #
55 | # Jobs
56 | # #
57 |
58 | jobs:
59 | job-ci:
60 | runs-on: ubuntu-latest
61 | steps:
62 |
63 | # #
64 | # CI › Start
65 | # #
66 |
67 | - name: '✅ Start'
68 | id: task_ci_start
69 | run: |
70 | echo "Starting linter"
71 |
72 | # #
73 | # CI › Checkout
74 | # #
75 |
76 | - name: '✅ Checkout'
77 | id: task_ci_checkout
78 | uses: actions/checkout@v4
79 | with:
80 | fetch-depth: 0
81 |
82 | # #
83 | # CI › Setup Node
84 | # #
85 |
86 | - name: '⚙️ Setup Node'
87 | id: task_ci_node_setup
88 | uses: actions/setup-node@v4
89 | with:
90 | node-version-file: '.nvmrc'
91 | node-version: '18'
92 | registry-url: 'https://registry.npmjs.org'
93 |
94 | # #
95 | # CI › Node Clean Install
96 | # #
97 |
98 | - name: '🕛 NPM › Clean Install'
99 | id: task_ci_npm_install
100 | run: |
101 | npm ci
102 |
103 | # #
104 | # CI › Build
105 | # #
106 |
107 | - name: '🔨 NPM › Build'
108 | id: task_ci_npm_build
109 | run: npm start
110 |
111 | # #
112 | # CI › Tests › Coveralls
113 | # #
114 |
115 | - name: '🧪 Tests › Submit Coveralls'
116 | id: task_ci_tests_coverage
117 | uses: coverallsapp/github-action@master
118 | if: ${{ !startsWith(github.ref, 'refs/tags/') }}
119 | with:
120 | github-token: ${{ secrets.GITHUB_TOKEN }}
121 |
122 | # #
123 | # CI › NPM › Publish
124 | # #
125 |
126 | - name: '📦 NPM › Publish'
127 | id: task_ci_npm_publish
128 | if: ${{ startsWith(github.ref, 'refs/tags/') }}
129 | run: npm publish
130 | env:
131 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
132 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kdbxweb",
3 | "version": "2.2.0",
4 | "description": "Kdbx KeePass database reader for web",
5 | "main": "dist/kdbxweb.js",
6 | "private": false,
7 | "types": "dist/types/index.d.ts",
8 | "homepage": "https://keeweb.info",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/keeweb/kdbxweb.git"
12 | },
13 | "author": {
14 | "name": "Antelle",
15 | "email": "keeweb@keeweb.info",
16 | "url": "https://antelle.net"
17 | },
18 | "engines": {
19 | "node": "^18.18.0"
20 | },
21 | "license": "MIT",
22 | "readme": "README.md",
23 | "funding": {
24 | "type": "github",
25 | "url": "https://github.com/sponsors/antelle"
26 | },
27 | "scripts": {
28 | "test": "npm run tests:node",
29 | "start": "npm run lint && npm run build",
30 | "tsc": "tsc",
31 | "lint": "eslint lib test conf scripts",
32 | "build": "npm run clean && npm run tests:cover && npm run pack:tests && npm run pack:dist-debug && npm run pack:dist-prod",
33 | "clean": "rimraf dist .nyc_output coverage",
34 | "pack:dist-prod": "webpack --progress --mode=production --config conf/webpack.config.ts --mode production",
35 | "pack:dist-debug": "webpack --progress --mode=development --devtool source-map --config conf/webpack.config.ts --mode development",
36 | "webpack-stats": "webpack --mode=production --json --config conf/webpack.config.ts > dist/stats.json",
37 | "pack:tests": "webpack --progress --config conf/webpack.tests.config.ts",
38 | "tests:node": "mocha --require ts-node/register --recursive --reporter spec test/**/*.spec.ts",
39 | "tests:cover": "nyc --reporter=lcov npm run tests:node",
40 | "script:dump-header": "ts-node scripts/dump-header",
41 | "script:kdbx-size-profiler": "ts-node scripts/kdbx-size-profiler",
42 | "script:kdbx-to-xml": "ts-node scripts/kdbx-to-xml",
43 | "script:make-big-files": "ts-node scripts/make-big-files",
44 | "script:save-perf-test": "ts-node scripts/save-perf-test"
45 | },
46 | "keywords": [
47 | "kdbx",
48 | "keepass"
49 | ],
50 | "devDependencies": {
51 | "@istanbuljs/nyc-config-typescript": "^1.0.2",
52 | "@nodelib/fs.walk": "^1.2.8",
53 | "@types/expect.js": "^0.3.29",
54 | "@types/mocha": "^9.0.0",
55 | "@types/terser-webpack-plugin": "^5.0.3",
56 | "@typescript-eslint/eslint-plugin": "^8.18.1",
57 | "@typescript-eslint/parser": "^8.18.1",
58 | "base64-loader": "^1.0.0",
59 | "eslint": "^9.17.0",
60 | "eslint-config-prettier": "^9.1.0",
61 | "eslint-plugin-chai-friendly": "^1.0.1",
62 | "eslint-plugin-import": "^2.31.0",
63 | "eslint-plugin-n": "17.15.0",
64 | "eslint-plugin-prettier": "^5.2.1",
65 | "eslint-plugin-promise": "^7.2.1",
66 | "eslint-plugin-standard": "^4.1.0",
67 | "eslint-plugin-mocha": "^10.5.0",
68 | "expect.js": "^0.3.1",
69 | "exports-loader": "^3.0.0",
70 | "mocha": "^9.1.1",
71 | "nyc": "^15.1.0",
72 | "prettier": "^3.4.2",
73 | "prettier-eslint": "^16.3.0",
74 | "rimraf": "^3.0.2",
75 | "source-map-support": "^0.5.19",
76 | "stats-webpack-plugin": "^0.7.0",
77 | "terser-webpack-plugin": "^5.2.3",
78 | "ts-loader": "^9.2.5",
79 | "ts-node": "^10.2.1",
80 | "typescript": "^5.7.2",
81 | "webpack": "^5.97.1",
82 | "webpack-cli": "^6.0.1"
83 | },
84 | "dependencies": {
85 | "@xmldom/xmldom": "^0.8.10",
86 | "fflate": "^0.7.1"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/test/format/kdbx-custom-data.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { Kdbx, KdbxContext, KdbxCustomData, KdbxCustomDataItem, XmlUtils } from '../../lib';
3 |
4 | describe('KdbxCustomData', () => {
5 | const kdbx = new Kdbx();
6 | const ctx = new KdbxContext({ kdbx });
7 |
8 | it('reads custom data from xml', () => {
9 | const xml = XmlUtils.parse(
10 | '' +
11 | '- k1v1
' +
12 | '- k2v2
' +
13 | ''
14 | );
15 | const cd = KdbxCustomData.read(xml.documentElement);
16 | expect([...cd.entries()]).to.eql([
17 | ['k1', { value: 'v1' }],
18 | ['k2', { value: 'v2' }]
19 | ]);
20 | });
21 |
22 | it('reads empty custom data from empty xml', () => {
23 | const xml = XmlUtils.parse('');
24 | const cd = KdbxCustomData.read(xml.documentElement);
25 | expect(cd).to.eql({});
26 | });
27 |
28 | it('skips unknown tags', () => {
29 | const xml = XmlUtils.parse(
30 | '- kv
'
31 | );
32 | const cd = KdbxCustomData.read(xml.documentElement);
33 | expect([...cd.entries()]).to.eql([['k', { value: 'v' }]]);
34 | });
35 |
36 | it('skips empty keys', () => {
37 | const xml = XmlUtils.parse(
38 | '- v
'
39 | );
40 | const cd = KdbxCustomData.read(xml.documentElement);
41 | expect(cd).to.eql({});
42 | });
43 |
44 | it('writes custom data to xml', () => {
45 | const xml = XmlUtils.create('root');
46 | KdbxCustomData.write(
47 | xml.documentElement,
48 | ctx,
49 | new Map([
50 | ['k1', { value: 'v1' }],
51 | ['k2', { value: 'v2' }]
52 | ])
53 | );
54 | expect(XmlUtils.serialize((xml.documentElement))).to.eql(
55 | '' +
56 | '- k1v1
' +
57 | '- k2v2
' +
58 | ''
59 | );
60 | });
61 |
62 | it('writes empty custom data to xml', () => {
63 | const xml = XmlUtils.create('root');
64 | KdbxCustomData.write(xml.documentElement, ctx, new Map());
65 | expect(
66 | XmlUtils.serialize((xml.documentElement)).replace(/\s/g, '')
67 | ).to.eql('');
68 | });
69 |
70 | it('does not create tag for empty custom data', () => {
71 | const xml = XmlUtils.create('root');
72 | KdbxCustomData.write(xml.documentElement, ctx, undefined);
73 | expect(
74 | XmlUtils.serialize((xml.documentElement)).replace(/\s/g, '')
75 | ).to.eql('');
76 | });
77 |
78 | it('skips keys without values', () => {
79 | const xml = XmlUtils.create('root');
80 | KdbxCustomData.write(
81 | xml.documentElement,
82 | ctx,
83 | new Map([
84 | ['k1', { value: 'v1' }],
85 | ['k2', { value: '' }],
86 | ['k3', { value: undefined }]
87 | ])
88 | );
89 | expect(XmlUtils.serialize((xml.documentElement))).to.eql(
90 | '' +
91 | '- k1v1
' +
92 | ''
93 | );
94 | });
95 | });
96 |
--------------------------------------------------------------------------------
/lib/format/kdbx-times.ts:
--------------------------------------------------------------------------------
1 | import * as XmlNames from './../defs/xml-names';
2 | import * as XmlUtils from './../utils/xml-utils';
3 | import { KdbxContext } from './kdbx-context';
4 |
5 | export class KdbxTimes {
6 | creationTime: Date | undefined;
7 | lastModTime: Date | undefined;
8 | lastAccessTime: Date | undefined;
9 | expiryTime: Date | undefined;
10 | expires: boolean | null | undefined;
11 | usageCount: number | undefined;
12 | locationChanged: Date | undefined;
13 |
14 | private readNode(node: Element): void {
15 | switch (node.tagName) {
16 | case XmlNames.Elem.CreationTime:
17 | this.creationTime = XmlUtils.getDate(node);
18 | break;
19 | case XmlNames.Elem.LastModTime:
20 | this.lastModTime = XmlUtils.getDate(node);
21 | break;
22 | case XmlNames.Elem.LastAccessTime:
23 | this.lastAccessTime = XmlUtils.getDate(node);
24 | break;
25 | case XmlNames.Elem.ExpiryTime:
26 | this.expiryTime = XmlUtils.getDate(node);
27 | break;
28 | case XmlNames.Elem.Expires:
29 | this.expires = XmlUtils.getBoolean(node);
30 | break;
31 | case XmlNames.Elem.UsageCount:
32 | this.usageCount = XmlUtils.getNumber(node);
33 | break;
34 | case XmlNames.Elem.LocationChanged:
35 | this.locationChanged = XmlUtils.getDate(node);
36 | break;
37 | }
38 | }
39 |
40 | clone(): KdbxTimes {
41 | const clone = new KdbxTimes();
42 | clone.creationTime = this.creationTime;
43 | clone.lastModTime = this.lastModTime;
44 | clone.lastAccessTime = this.lastAccessTime;
45 | clone.expiryTime = this.expiryTime;
46 | clone.expires = this.expires;
47 | clone.usageCount = this.usageCount;
48 | clone.locationChanged = this.locationChanged;
49 | return clone;
50 | }
51 |
52 | update(): void {
53 | const now = new Date();
54 | this.lastModTime = now;
55 | this.lastAccessTime = now;
56 | }
57 |
58 | write(parentNode: Element, ctx: KdbxContext): void {
59 | const node = XmlUtils.addChildNode(parentNode, XmlNames.Elem.Times);
60 | ctx.setXmlDate(XmlUtils.addChildNode(node, XmlNames.Elem.CreationTime), this.creationTime);
61 | ctx.setXmlDate(XmlUtils.addChildNode(node, XmlNames.Elem.LastModTime), this.lastModTime);
62 | ctx.setXmlDate(
63 | XmlUtils.addChildNode(node, XmlNames.Elem.LastAccessTime),
64 | this.lastAccessTime
65 | );
66 | ctx.setXmlDate(XmlUtils.addChildNode(node, XmlNames.Elem.ExpiryTime), this.expiryTime);
67 | XmlUtils.setBoolean(XmlUtils.addChildNode(node, XmlNames.Elem.Expires), this.expires);
68 | XmlUtils.setNumber(XmlUtils.addChildNode(node, XmlNames.Elem.UsageCount), this.usageCount);
69 | ctx.setXmlDate(
70 | XmlUtils.addChildNode(node, XmlNames.Elem.LocationChanged),
71 | this.locationChanged
72 | );
73 | }
74 |
75 | static create(): KdbxTimes {
76 | const times = new KdbxTimes();
77 | const now = new Date();
78 | times.creationTime = now;
79 | times.lastModTime = now;
80 | times.lastAccessTime = now;
81 | times.expiryTime = now;
82 | times.expires = false;
83 | times.usageCount = 0;
84 | times.locationChanged = now;
85 | return times;
86 | }
87 |
88 | static read(xmlNode: Node): KdbxTimes {
89 | const obj = new KdbxTimes();
90 | for (let i = 0, cn = xmlNode.childNodes, len = cn.length; i < len; i++) {
91 | const childNode = cn[i];
92 | if (childNode.tagName) {
93 | obj.readNode(childNode);
94 | }
95 | }
96 | return obj;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/lib/crypto/hashed-block-transform.ts:
--------------------------------------------------------------------------------
1 | import { BinaryStream } from '../utils/binary-stream';
2 | import * as CryptoEngine from '../crypto/crypto-engine';
3 | import { KdbxError } from '../errors/kdbx-error';
4 | import { arrayBufferEquals } from '../utils/byte-utils';
5 | import { ErrorCodes } from '../defs/consts';
6 |
7 | const BlockSize = 1024 * 1024;
8 |
9 | export function decrypt(data: ArrayBuffer): Promise {
10 | return Promise.resolve().then(() => {
11 | const stm = new BinaryStream(data);
12 | const buffers: ArrayBuffer[] = [];
13 | let // blockIndex = 0,
14 | blockLength = 0,
15 | blockHash: ArrayBuffer,
16 | totalLength = 0;
17 |
18 | const next = (): Promise => {
19 | /* blockIndex = */ stm.getUint32(true);
20 | blockHash = stm.readBytes(32);
21 | blockLength = stm.getUint32(true);
22 | if (blockLength > 0) {
23 | totalLength += blockLength;
24 | const blockData = stm.readBytes(blockLength);
25 | return CryptoEngine.sha256(blockData).then((calculatedHash) => {
26 | if (!arrayBufferEquals(calculatedHash, blockHash)) {
27 | throw new KdbxError(ErrorCodes.FileCorrupt, 'invalid hash block');
28 | } else {
29 | buffers.push(blockData);
30 | return next();
31 | }
32 | });
33 | } else {
34 | const ret = new Uint8Array(totalLength);
35 | let offset = 0;
36 | for (let i = 0; i < buffers.length; i++) {
37 | ret.set(new Uint8Array(buffers[i]), offset);
38 | offset += buffers[i].byteLength;
39 | }
40 | return Promise.resolve(ret.buffer);
41 | }
42 | };
43 | return next();
44 | });
45 | }
46 |
47 | export function encrypt(data: ArrayBuffer): Promise {
48 | return Promise.resolve().then(() => {
49 | let bytesLeft = data.byteLength;
50 | let currentOffset = 0,
51 | blockIndex = 0,
52 | totalLength = 0;
53 | const buffers: ArrayBuffer[] = [];
54 |
55 | const next = (): Promise => {
56 | if (bytesLeft > 0) {
57 | const blockLength = Math.min(BlockSize, bytesLeft);
58 | bytesLeft -= blockLength;
59 |
60 | const blockData = data.slice(currentOffset, currentOffset + blockLength);
61 | return CryptoEngine.sha256(blockData).then((blockHash) => {
62 | const blockBuffer = new ArrayBuffer(4 + 32 + 4);
63 | const stm = new BinaryStream(blockBuffer);
64 | stm.setUint32(blockIndex, true);
65 | stm.writeBytes(blockHash);
66 | stm.setUint32(blockLength, true);
67 |
68 | buffers.push(blockBuffer);
69 | totalLength += blockBuffer.byteLength;
70 | buffers.push(blockData);
71 | totalLength += blockData.byteLength;
72 |
73 | blockIndex++;
74 | currentOffset += blockLength;
75 |
76 | return next();
77 | });
78 | } else {
79 | const endBlockData = new ArrayBuffer(4 + 32 + 4);
80 | const view = new DataView(endBlockData);
81 | view.setUint32(0, blockIndex, true);
82 | buffers.push(endBlockData);
83 | totalLength += endBlockData.byteLength;
84 |
85 | const ret = new Uint8Array(totalLength);
86 | let offset = 0;
87 | for (let i = 0; i < buffers.length; i++) {
88 | ret.set(new Uint8Array(buffers[i]), offset);
89 | offset += buffers[i].byteLength;
90 | }
91 | return Promise.resolve(ret.buffer);
92 | }
93 | };
94 | return next();
95 | });
96 | }
97 |
--------------------------------------------------------------------------------
/lib/crypto/protected-value.ts:
--------------------------------------------------------------------------------
1 | import * as CryptoEngine from './crypto-engine';
2 | import {
3 | arrayToBuffer,
4 | base64ToBytes,
5 | bytesToBase64,
6 | bytesToString,
7 | stringToBytes,
8 | zeroBuffer
9 | } from '../utils/byte-utils';
10 |
11 | export class ProtectedValue {
12 | readonly value: Uint8Array;
13 | readonly salt: Uint8Array;
14 |
15 | constructor(value: ArrayBuffer, salt: ArrayBuffer) {
16 | this.value = new Uint8Array(value);
17 | this.salt = new Uint8Array(salt);
18 | }
19 |
20 | toString(): string {
21 | return bytesToBase64(this.value);
22 | }
23 |
24 | static fromString(str: string): ProtectedValue {
25 | const bytes = stringToBytes(str),
26 | salt = CryptoEngine.random(bytes.length);
27 | for (let i = 0, len = bytes.length; i < len; i++) {
28 | bytes[i] ^= salt[i];
29 | }
30 | return new ProtectedValue(arrayToBuffer(bytes), arrayToBuffer(salt));
31 | }
32 |
33 | toBase64(): string {
34 | const binary = this.getBinary();
35 | const base64 = bytesToBase64(binary);
36 | zeroBuffer(binary);
37 | return base64;
38 | }
39 |
40 | static fromBase64(base64: string): ProtectedValue {
41 | const bytes = base64ToBytes(base64);
42 | return ProtectedValue.fromBinary(bytes);
43 | }
44 |
45 | /**
46 | * Keep in mind that you're passing the ownership of this array, the contents will be destroyed
47 | */
48 | static fromBinary(binary: ArrayBuffer): ProtectedValue {
49 | const bytes = new Uint8Array(binary),
50 | salt = CryptoEngine.random(bytes.length);
51 | for (let i = 0, len = bytes.length; i < len; i++) {
52 | bytes[i] ^= salt[i];
53 | }
54 | return new ProtectedValue(arrayToBuffer(bytes), arrayToBuffer(salt));
55 | }
56 |
57 | includes(str: string): boolean {
58 | if (str.length === 0) {
59 | return false;
60 | }
61 | const source = this.value,
62 | salt = this.salt,
63 | search = stringToBytes(str),
64 | sourceLen = source.length,
65 | searchLen = search.length,
66 | maxPos = sourceLen - searchLen;
67 | src: for (let sourceIx = 0; sourceIx <= maxPos; sourceIx++) {
68 | for (let searchIx = 0; searchIx < searchLen; searchIx++) {
69 | if (
70 | (source[sourceIx + searchIx] ^ salt[sourceIx + searchIx]) !==
71 | search[searchIx]
72 | ) {
73 | continue src;
74 | }
75 | }
76 | return true;
77 | }
78 | return false;
79 | }
80 |
81 | getHash(): Promise {
82 | const binary = arrayToBuffer(this.getBinary());
83 | return CryptoEngine.sha256(binary).then((hash) => {
84 | zeroBuffer(binary);
85 | return hash;
86 | });
87 | }
88 |
89 | getText(): string {
90 | return bytesToString(this.getBinary());
91 | }
92 |
93 | getBinary(): Uint8Array {
94 | const value = this.value,
95 | salt = this.salt;
96 | const bytes = new Uint8Array(value.byteLength);
97 | for (let i = bytes.length - 1; i >= 0; i--) {
98 | bytes[i] = value[i] ^ salt[i];
99 | }
100 | return bytes;
101 | }
102 |
103 | setSalt(newSalt: ArrayBuffer): void {
104 | const newSaltArr = new Uint8Array(newSalt);
105 | const value = this.value,
106 | salt = this.salt;
107 | for (let i = 0, len = value.length; i < len; i++) {
108 | value[i] = value[i] ^ salt[i] ^ newSaltArr[i];
109 | salt[i] = newSaltArr[i];
110 | }
111 | }
112 |
113 | clone(): ProtectedValue {
114 | return new ProtectedValue(this.value, this.salt);
115 | }
116 |
117 | get byteLength(): number {
118 | return this.value.byteLength;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/lib/crypto/chacha20.ts:
--------------------------------------------------------------------------------
1 | export class ChaCha20 {
2 | private readonly _sigmaWords = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574];
3 | private readonly _block = new Uint8Array(64);
4 | private _blockUsed = 64;
5 | private readonly _x = new Uint32Array(16);
6 | private readonly _input: Uint32Array;
7 |
8 | constructor(key: Uint8Array, nonce: Uint8Array) {
9 | const input = new Uint32Array(16);
10 |
11 | input[0] = this._sigmaWords[0];
12 | input[1] = this._sigmaWords[1];
13 | input[2] = this._sigmaWords[2];
14 | input[3] = this._sigmaWords[3];
15 | input[4] = u8to32le(key, 0);
16 | input[5] = u8to32le(key, 4);
17 | input[6] = u8to32le(key, 8);
18 | input[7] = u8to32le(key, 12);
19 | input[8] = u8to32le(key, 16);
20 | input[9] = u8to32le(key, 20);
21 | input[10] = u8to32le(key, 24);
22 | input[11] = u8to32le(key, 28);
23 | input[12] = 0; // counter
24 |
25 | if (nonce.length === 12) {
26 | input[13] = u8to32le(nonce, 0);
27 | input[14] = u8to32le(nonce, 4);
28 | input[15] = u8to32le(nonce, 8);
29 | } else {
30 | input[13] = 0;
31 | input[14] = u8to32le(nonce, 0);
32 | input[15] = u8to32le(nonce, 4);
33 | }
34 |
35 | this._input = input;
36 | }
37 |
38 | getBytes(numberOfBytes: number): Uint8Array {
39 | const out = new Uint8Array(numberOfBytes);
40 | for (let i = 0; i < numberOfBytes; i++) {
41 | if (this._blockUsed === 64) {
42 | this.generateBlock();
43 | this._blockUsed = 0;
44 | }
45 | out[i] = this._block[this._blockUsed];
46 | this._blockUsed++;
47 | }
48 | return out;
49 | }
50 |
51 | private generateBlock(): void {
52 | const input = this._input;
53 | const x = this._x;
54 | const block = this._block;
55 |
56 | x.set(input);
57 | for (let i = 20; i > 0; i -= 2) {
58 | quarterRound(x, 0, 4, 8, 12);
59 | quarterRound(x, 1, 5, 9, 13);
60 | quarterRound(x, 2, 6, 10, 14);
61 | quarterRound(x, 3, 7, 11, 15);
62 | quarterRound(x, 0, 5, 10, 15);
63 | quarterRound(x, 1, 6, 11, 12);
64 | quarterRound(x, 2, 7, 8, 13);
65 | quarterRound(x, 3, 4, 9, 14);
66 | }
67 | for (let i = 16; i--; ) {
68 | x[i] += input[i];
69 | }
70 | for (let i = 16; i--; ) {
71 | u32to8le(block, 4 * i, x[i]);
72 | }
73 |
74 | input[12] += 1;
75 | if (!input[12]) {
76 | input[13] += 1;
77 | }
78 | }
79 |
80 | public encrypt(data: Uint8Array): Uint8Array {
81 | const length = data.length;
82 | const res = new Uint8Array(length);
83 | let pos = 0;
84 | const block = this._block;
85 | while (pos < length) {
86 | this.generateBlock();
87 | const blockLength = Math.min(length - pos, 64);
88 | for (let i = 0; i < blockLength; i++) {
89 | res[pos] = data[pos] ^ block[i];
90 | pos++;
91 | }
92 | }
93 | return res;
94 | }
95 | }
96 |
97 | function quarterRound(x: Uint32Array, a: number, b: number, c: number, d: number): void {
98 | x[a] += x[b];
99 | x[d] = rotate(x[d] ^ x[a], 16);
100 | x[c] += x[d];
101 | x[b] = rotate(x[b] ^ x[c], 12);
102 | x[a] += x[b];
103 | x[d] = rotate(x[d] ^ x[a], 8);
104 | x[c] += x[d];
105 | x[b] = rotate(x[b] ^ x[c], 7);
106 | }
107 |
108 | function u8to32le(x: Uint8Array, i: number): number {
109 | return x[i] | (x[i + 1] << 8) | (x[i + 2] << 16) | (x[i + 3] << 24);
110 | }
111 |
112 | function u32to8le(x: Uint8Array, i: number, u: number): void {
113 | x[i] = u;
114 | u >>>= 8;
115 | x[i + 1] = u;
116 | u >>>= 8;
117 | x[i + 2] = u;
118 | u >>>= 8;
119 | x[i + 3] = u;
120 | }
121 |
122 | function rotate(v: number, c: number): number {
123 | return (v << c) | (v >>> (32 - c));
124 | }
125 |
--------------------------------------------------------------------------------
/test/format/kdbx-uuid.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { KdbxUuid } from '../../lib';
3 |
4 | describe('KdbxUuid', () => {
5 | it('creates uuid from 16 bytes ArrayBuffer', () => {
6 | const uuid = new KdbxUuid(
7 | new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6]).buffer
8 | );
9 | expect(uuid.id).to.be('AQIDBAUGBwgJCgECAwQFBg==');
10 | });
11 |
12 | it('creates uuid from 16 bytes array', () => {
13 | const uuid = new KdbxUuid(
14 | new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6])
15 | );
16 | expect(uuid.id).to.be('AQIDBAUGBwgJCgECAwQFBg==');
17 | });
18 |
19 | it('creates uuid base64 string', () => {
20 | const uuid = new KdbxUuid('AQIDBAUGBwgJCgECAwQFBg==');
21 | expect(uuid.id).to.be('AQIDBAUGBwgJCgECAwQFBg==');
22 | });
23 |
24 | it('throws an error for less than 16 bytes', () => {
25 | try {
26 | const uuid = new KdbxUuid(new Uint16Array([123]).buffer);
27 | throw new Error(`Expected an error to be thrown, got UUID instead: ${uuid}`);
28 | } catch (e) {
29 | expect((e as Error).message).to.contain('FileCorrupt: bad UUID length: 2');
30 | }
31 | });
32 |
33 | it('creates empty uuid from undefined', () => {
34 | const uuid = new KdbxUuid(undefined);
35 | expect(uuid.id).to.be('AAAAAAAAAAAAAAAAAAAAAA==');
36 | expect(uuid.empty).to.be(true);
37 | });
38 |
39 | it('returns uuid in toString method', () => {
40 | const uuid = new KdbxUuid(
41 | new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6]).buffer
42 | );
43 | expect(uuid.toString()).to.be('AQIDBAUGBwgJCgECAwQFBg==');
44 | });
45 |
46 | it('returns uuid in valueOf method', () => {
47 | const uuid = new KdbxUuid(
48 | new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6]).buffer
49 | );
50 | expect(uuid.valueOf()).to.be('AQIDBAUGBwgJCgECAwQFBg==');
51 | });
52 |
53 | it('creates empty uuid from no arg', () => {
54 | const uuid = new KdbxUuid();
55 | expect(uuid.toString()).to.be('AAAAAAAAAAAAAAAAAAAAAA==');
56 | expect(uuid.empty).to.be(true);
57 | });
58 |
59 | it('sets empty property for empty uuid', () => {
60 | const uuid = new KdbxUuid(new Uint8Array(16).buffer);
61 | expect(uuid.toString()).to.be('AAAAAAAAAAAAAAAAAAAAAA==');
62 | expect(uuid.empty).to.be(true);
63 | });
64 |
65 | it('returns bytes in toBytes method', () => {
66 | const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6]);
67 | const uuid = new KdbxUuid(bytes.buffer);
68 | expect(uuid.toBytes()).to.be.eql(bytes);
69 | });
70 |
71 | it('returns bytes in bytes property', () => {
72 | const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6]);
73 | const uuid = new KdbxUuid(bytes.buffer);
74 | expect(uuid.bytes).to.be.eql(bytes);
75 | });
76 |
77 | it('returns bytes in toBytes method for empty value', () => {
78 | const uuid = new KdbxUuid();
79 | expect(uuid.toBytes()).to.be.eql([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
80 | });
81 |
82 | it('generates random uuid', () => {
83 | const uuid = KdbxUuid.random();
84 | expect(uuid).to.be.a(KdbxUuid);
85 | expect(uuid.toString()).not.to.be('AAAAAAAAAAAAAAAAAAAAAA==');
86 | });
87 |
88 | it('checks equality', () => {
89 | const uuid = new KdbxUuid(
90 | new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6]).buffer
91 | );
92 | expect(uuid.equals('AQIDBAUGBwgJCgECAwQFBg==')).to.be(true);
93 | expect(uuid.equals(new KdbxUuid('AQIDBAUGBwgJCgECAwQFBg=='))).to.be(true);
94 | expect(uuid.equals(undefined)).to.be(false);
95 | expect(uuid.equals(null)).to.be(false);
96 | expect(uuid.equals('')).to.be(false);
97 | expect(uuid.equals('???')).to.be(false);
98 | expect(uuid.equals(new KdbxUuid())).to.be(false);
99 | });
100 | });
101 |
--------------------------------------------------------------------------------
/lib/crypto/key-encryptor-kdf.ts:
--------------------------------------------------------------------------------
1 | import * as CryptoEngine from '../crypto/crypto-engine';
2 | import * as KeyEncryptorAes from './key-encryptor-aes';
3 | import { VarDictionary, VarDictionaryAnyValue } from '../utils/var-dictionary';
4 | import { KdbxError } from '../errors/kdbx-error';
5 | import { ErrorCodes, KdfId } from '../defs/consts';
6 | import { bytesToBase64, zeroBuffer } from '../utils/byte-utils';
7 | import { Argon2Type } from './crypto-engine';
8 | import { Int64 } from '../utils/int64';
9 |
10 | export function encrypt(key: ArrayBuffer, kdfParams: VarDictionary): Promise {
11 | const uuid = kdfParams.get('$UUID');
12 | if (!uuid || !(uuid instanceof ArrayBuffer)) {
13 | return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'no kdf uuid'));
14 | }
15 | const kdfUuid = bytesToBase64(uuid);
16 | switch (kdfUuid) {
17 | case KdfId.Argon2d:
18 | return encryptArgon2(key, kdfParams, CryptoEngine.Argon2TypeArgon2d);
19 | case KdfId.Argon2id:
20 | return encryptArgon2(key, kdfParams, CryptoEngine.Argon2TypeArgon2id);
21 | case KdfId.Aes:
22 | return encryptAes(key, kdfParams);
23 | default:
24 | return Promise.reject(new KdbxError(ErrorCodes.Unsupported, 'bad kdf'));
25 | }
26 | }
27 |
28 | function encryptArgon2(
29 | key: ArrayBuffer,
30 | kdfParams: VarDictionary,
31 | argon2type: Argon2Type
32 | ): Promise {
33 | const salt = kdfParams.get('S');
34 | if (!(salt instanceof ArrayBuffer) || salt.byteLength !== 32) {
35 | return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad argon2 salt'));
36 | }
37 |
38 | const parallelism = toNumber(kdfParams.get('P'));
39 | if (typeof parallelism !== 'number' || parallelism < 1) {
40 | return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad argon2 parallelism'));
41 | }
42 |
43 | const iterations = toNumber(kdfParams.get('I'));
44 | if (typeof iterations !== 'number' || iterations < 1) {
45 | return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad argon2 iterations'));
46 | }
47 |
48 | const memory = toNumber(kdfParams.get('M'));
49 | if (typeof memory !== 'number' || memory < 1 || memory % 1024 !== 0) {
50 | return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad argon2 memory'));
51 | }
52 |
53 | const version = kdfParams.get('V');
54 | if (version !== 0x13 && version !== 0x10) {
55 | return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad argon2 version'));
56 | }
57 |
58 | const secretKey = kdfParams.get('K');
59 | if (secretKey) {
60 | return Promise.reject(new KdbxError(ErrorCodes.Unsupported, 'argon2 secret key'));
61 | }
62 |
63 | const assocData = kdfParams.get('A');
64 | if (assocData) {
65 | return Promise.reject(new KdbxError(ErrorCodes.Unsupported, 'argon2 assoc data'));
66 | }
67 |
68 | return CryptoEngine.argon2(
69 | key,
70 | salt,
71 | memory / 1024,
72 | iterations,
73 | 32,
74 | parallelism,
75 | argon2type,
76 | version
77 | );
78 | }
79 |
80 | function encryptAes(key: ArrayBuffer, kdfParams: VarDictionary) {
81 | const salt = kdfParams.get('S');
82 | if (!(salt instanceof ArrayBuffer) || salt.byteLength !== 32) {
83 | return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad aes salt'));
84 | }
85 |
86 | const rounds = toNumber(kdfParams.get('R'));
87 | if (typeof rounds !== 'number' || rounds < 1) {
88 | return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad aes rounds'));
89 | }
90 |
91 | return KeyEncryptorAes.encrypt(new Uint8Array(key), new Uint8Array(salt), rounds).then(
92 | (key) => {
93 | return CryptoEngine.sha256(key).then((hash) => {
94 | zeroBuffer(key);
95 | return hash;
96 | });
97 | }
98 | );
99 | }
100 |
101 | function toNumber(number: VarDictionaryAnyValue): number | undefined {
102 | if (typeof number === 'number') {
103 | return number;
104 | } else if (number instanceof Int64) {
105 | return number.value;
106 | }
107 | return undefined;
108 | }
109 |
--------------------------------------------------------------------------------
/lib/format/kdbx-binaries.ts:
--------------------------------------------------------------------------------
1 | import * as CryptoEngine from './../crypto/crypto-engine';
2 | import { ProtectedValue } from '../crypto/protected-value';
3 | import { arrayToBuffer, bytesToHex } from '../utils/byte-utils';
4 |
5 | export type KdbxBinaryRef = { ref: string };
6 | export type KdbxBinaryRefWithValue = { ref: string; value: KdbxBinary };
7 | export type KdbxBinaryWithHash = { hash: string; value: KdbxBinary };
8 |
9 | export type KdbxBinary = ProtectedValue | ArrayBuffer;
10 | export type KdbxBinaryOrRef = KdbxBinary | KdbxBinaryRef;
11 | export type KdbxBinaryIn = KdbxBinary | Uint8Array;
12 |
13 | export class KdbxBinaries {
14 | // temporary map used during database loading
15 | private readonly _mapById = new Map();
16 | // in runtime, entries are addressed by hash
17 | private readonly _mapByHash = new Map();
18 | // kept to be able to find binaries by id as well
19 | private readonly _idToHash = new Map();
20 |
21 | computeHashes(): Promise {
22 | // this method is called after the file is loaded
23 | const promises = [...this._mapById].map(([id, binary]) =>
24 | KdbxBinaries.getBinaryHash(binary).then((hash) => {
25 | this._idToHash.set(id, hash);
26 | this._mapByHash.set(hash, binary);
27 | })
28 | );
29 | return Promise.all(promises).then(() => {
30 | // it won't be used anymore
31 | this._mapById.clear();
32 | });
33 | }
34 |
35 | private static getBinaryHash(binary: KdbxBinaryIn): Promise {
36 | let promise;
37 | if (binary instanceof ProtectedValue) {
38 | promise = binary.getHash();
39 | } else {
40 | binary = arrayToBuffer(binary);
41 | promise = CryptoEngine.sha256(binary);
42 | }
43 | return promise.then(bytesToHex);
44 | }
45 |
46 | add(value: KdbxBinaryIn): Promise {
47 | // called after load
48 | if (value instanceof Uint8Array) {
49 | value = arrayToBuffer(value);
50 | }
51 | return KdbxBinaries.getBinaryHash(value).then((hash) => {
52 | this._mapByHash.set(hash, value);
53 | return { hash, value };
54 | });
55 | }
56 |
57 | addWithNextId(value: KdbxBinaryIn): void {
58 | // called during load (v4), when building the id map
59 | const id = this._mapById.size.toString();
60 | this.addWithId(id, value);
61 | }
62 |
63 | addWithId(id: string, value: KdbxBinaryIn): void {
64 | // called during load (v3), when building the id map
65 | if (value instanceof Uint8Array) {
66 | value = arrayToBuffer(value);
67 | }
68 | this._mapById.set(id, value);
69 | }
70 |
71 | addWithHash(binary: KdbxBinaryWithHash): void {
72 | this._mapByHash.set(binary.hash, binary.value);
73 | }
74 |
75 | deleteWithHash(hash: string): void {
76 | this._mapByHash.delete(hash);
77 | }
78 |
79 | getByRef(binaryRef: KdbxBinaryRef): KdbxBinaryWithHash | undefined {
80 | const hash = this._idToHash.get(binaryRef.ref);
81 | if (!hash) {
82 | return undefined;
83 | }
84 | const value = this._mapByHash.get(hash);
85 | if (!value) {
86 | return undefined;
87 | }
88 | return { hash, value };
89 | }
90 |
91 | getRefByHash(hash: string): KdbxBinaryRef | undefined {
92 | const ref = [...this._mapByHash.keys()].indexOf(hash);
93 | if (ref < 0) {
94 | return undefined;
95 | }
96 | return { ref: ref.toString() };
97 | }
98 |
99 | getAll(): KdbxBinaryRefWithValue[] {
100 | return [...this._mapByHash.values()].map((value, index) => {
101 | return { ref: index.toString(), value };
102 | });
103 | }
104 |
105 | getAllWithHashes(): KdbxBinaryWithHash[] {
106 | return [...this._mapByHash].map(([hash, value]) => ({
107 | hash,
108 | value
109 | }));
110 | }
111 |
112 | getValueByHash(hash: string): KdbxBinary | undefined {
113 | return this._mapByHash.get(hash);
114 | }
115 |
116 | static isKdbxBinaryRef(binary: KdbxBinaryOrRef | undefined): binary is KdbxBinaryRef {
117 | return !!(binary as KdbxBinaryRef)?.ref;
118 | }
119 |
120 | static isKdbxBinaryWithHash(
121 | binary: KdbxBinaryOrRef | KdbxBinaryWithHash | undefined
122 | ): binary is KdbxBinaryWithHash {
123 | return !!(binary as KdbxBinaryWithHash)?.hash;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/lib/crypto/hmac-block-transform.ts:
--------------------------------------------------------------------------------
1 | import { Int64 } from '../utils/int64';
2 | import { arrayBufferEquals, arrayToBuffer, zeroBuffer } from '../utils/byte-utils';
3 | import * as CryptoEngine from '../crypto/crypto-engine';
4 | import { BinaryStream } from '../utils/binary-stream';
5 | import { KdbxError } from '../errors/kdbx-error';
6 | import { ErrorCodes } from '../defs/consts';
7 |
8 | const BlockSize = 1024 * 1024;
9 |
10 | export function getHmacKey(key: ArrayBuffer, blockIndex: Int64): Promise {
11 | const shaSrc = new Uint8Array(8 + key.byteLength);
12 | shaSrc.set(new Uint8Array(key), 8);
13 | const view = new DataView(shaSrc.buffer);
14 | view.setUint32(0, blockIndex.lo, true);
15 | view.setUint32(4, blockIndex.hi, true);
16 | return CryptoEngine.sha512(arrayToBuffer(shaSrc)).then((sha) => {
17 | zeroBuffer(shaSrc);
18 | return sha;
19 | });
20 | }
21 |
22 | function getBlockHmac(
23 | key: ArrayBuffer,
24 | blockIndex: number,
25 | blockLength: number,
26 | blockData: ArrayBuffer
27 | ): Promise {
28 | return getHmacKey(key, new Int64(blockIndex)).then((blockKey) => {
29 | const blockDataForHash = new Uint8Array(blockData.byteLength + 4 + 8);
30 | const blockDataForHashView = new DataView(blockDataForHash.buffer);
31 | blockDataForHash.set(new Uint8Array(blockData), 4 + 8);
32 | blockDataForHashView.setInt32(0, blockIndex, true);
33 | blockDataForHashView.setInt32(8, blockLength, true);
34 | return CryptoEngine.hmacSha256(blockKey, blockDataForHash.buffer);
35 | });
36 | }
37 |
38 | export function decrypt(data: ArrayBuffer, key: ArrayBuffer): Promise {
39 | const stm = new BinaryStream(data);
40 | return Promise.resolve().then(() => {
41 | const buffers: ArrayBuffer[] = [];
42 | let blockIndex = 0,
43 | blockLength = 0,
44 | blockHash: ArrayBuffer,
45 | totalLength = 0;
46 |
47 | const next = (): Promise => {
48 | blockHash = stm.readBytes(32);
49 | blockLength = stm.getUint32(true);
50 | if (blockLength > 0) {
51 | totalLength += blockLength;
52 | const blockData = stm.readBytes(blockLength);
53 | return getBlockHmac(key, blockIndex, blockLength, blockData).then(
54 | (calculatedBlockHash) => {
55 | if (!arrayBufferEquals(calculatedBlockHash, blockHash)) {
56 | throw new KdbxError(ErrorCodes.FileCorrupt, 'invalid hash block');
57 | } else {
58 | buffers.push(blockData);
59 | blockIndex++;
60 | return next();
61 | }
62 | }
63 | );
64 | } else {
65 | const ret = new Uint8Array(totalLength);
66 | let offset = 0;
67 | for (let i = 0; i < buffers.length; i++) {
68 | ret.set(new Uint8Array(buffers[i]), offset);
69 | offset += buffers[i].byteLength;
70 | }
71 | return Promise.resolve(ret.buffer);
72 | }
73 | };
74 | return next();
75 | });
76 | }
77 |
78 | export function encrypt(data: ArrayBuffer, key: ArrayBuffer): Promise {
79 | return Promise.resolve().then(() => {
80 | let bytesLeft = data.byteLength;
81 | let currentOffset = 0,
82 | blockIndex = 0,
83 | totalLength = 0;
84 | const buffers: ArrayBuffer[] = [];
85 |
86 | const next = (): Promise => {
87 | const blockLength = Math.min(BlockSize, bytesLeft);
88 | bytesLeft -= blockLength;
89 |
90 | const blockData = data.slice(currentOffset, currentOffset + blockLength);
91 | return getBlockHmac(key, blockIndex, blockLength, blockData).then((blockHash) => {
92 | const blockBuffer = new ArrayBuffer(32 + 4);
93 | const stm = new BinaryStream(blockBuffer);
94 | stm.writeBytes(blockHash);
95 | stm.setUint32(blockLength, true);
96 |
97 | buffers.push(blockBuffer);
98 | totalLength += blockBuffer.byteLength;
99 |
100 | if (blockData.byteLength > 0) {
101 | buffers.push(blockData);
102 | totalLength += blockData.byteLength;
103 | blockIndex++;
104 | currentOffset += blockLength;
105 | return next();
106 | } else {
107 | const ret = new Uint8Array(totalLength);
108 | let offset = 0;
109 | for (let i = 0; i < buffers.length; i++) {
110 | ret.set(new Uint8Array(buffers[i]), offset);
111 | offset += buffers[i].byteLength;
112 | }
113 | return ret.buffer;
114 | }
115 | });
116 | };
117 | return next();
118 | });
119 | }
120 |
--------------------------------------------------------------------------------
/test/utils/byte-utils.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils } from '../../lib';
3 |
4 | describe('ByteUtils', () => {
5 | describe('arrayBufferEquals', () => {
6 | it('returns true for equal ArrayBuffers', () => {
7 | const ab1 = new Int8Array([1, 2, 3]).buffer;
8 | const ab2 = new Int8Array([1, 2, 3]).buffer;
9 | expect(ByteUtils.arrayBufferEquals(ab1, ab2)).to.be(true);
10 | });
11 |
12 | it('returns false for ArrayBuffers of different length', () => {
13 | const ab1 = new Int8Array([1, 2, 3]).buffer;
14 | const ab2 = new Int8Array([1, 2, 3, 4]).buffer;
15 | expect(ByteUtils.arrayBufferEquals(ab1, ab2)).to.be(false);
16 | });
17 |
18 | it('returns false for different ArrayBuffers', () => {
19 | const ab1 = new Int8Array([1, 2, 3]).buffer;
20 | const ab2 = new Int8Array([3, 2, 1]).buffer;
21 | expect(ByteUtils.arrayBufferEquals(ab1, ab2)).to.be(false);
22 | });
23 | });
24 |
25 | const str = 'utf8стрƒΩ≈ç√∫˜µ≤æ∆©ƒ∂ß';
26 | const strBytes = new Uint8Array([
27 | 117, 116, 102, 56, 209, 129, 209, 130, 209, 128, 198, 146, 206, 169, 226, 137, 136, 195,
28 | 167, 226, 136, 154, 226, 136, 171, 203, 156, 194, 181, 226, 137, 164, 195, 166, 226, 136,
29 | 134, 194, 169, 198, 146, 226, 136, 130, 195, 159
30 | ]);
31 |
32 | describe('bytesToString', () => {
33 | it('converts Array to string', () => {
34 | expect(ByteUtils.bytesToString(strBytes)).to.be(str);
35 | });
36 |
37 | it('converts ArrayBuffer to string', () => {
38 | expect(ByteUtils.bytesToString(strBytes.buffer)).to.be(str);
39 | });
40 | });
41 |
42 | describe('stringToBytes', () => {
43 | it('converts string to Array', () => {
44 | expect(ByteUtils.stringToBytes(str)).to.be.eql(strBytes);
45 | });
46 | });
47 |
48 | const base64 = 'c3Ry0L/RgNC40LLQtdGC';
49 | const bytes = new Uint8Array([
50 | 115, 116, 114, 208, 191, 209, 128, 208, 184, 208, 178, 208, 181, 209, 130
51 | ]);
52 |
53 | describe('base64ToBytes', () => {
54 | it('converts base64-string to byte array', () => {
55 | expect(ByteUtils.base64ToBytes(base64)).to.be.eql(bytes);
56 | });
57 |
58 | it('converts base64-string to byte array using Buffer', () => {
59 | const atob = global.atob;
60 | // @ts-ignore
61 | global.atob = undefined;
62 | try {
63 | expect(ByteUtils.base64ToBytes(base64)).to.be.eql(bytes);
64 | } finally {
65 | global.atob = atob;
66 | }
67 | });
68 | });
69 |
70 | describe('bytesToBase64', () => {
71 | it('converts byte array to base64-string', () => {
72 | expect(ByteUtils.bytesToBase64(bytes)).to.be.eql(base64);
73 | });
74 |
75 | it('converts ArrayBuffer base64-string', () => {
76 | expect(ByteUtils.bytesToBase64(bytes.buffer)).to.be.eql(base64);
77 | });
78 |
79 | it('converts byte array to base64-string using Buffer', () => {
80 | const btoa = global.btoa;
81 | // @ts-ignore
82 | global.btoa = undefined;
83 | try {
84 | expect(ByteUtils.bytesToBase64(bytes)).to.be.eql(base64);
85 | } finally {
86 | global.btoa = btoa;
87 | }
88 | });
89 | });
90 |
91 | const hexString = '737472d0bfd180d0b8d0b2d0b5d101';
92 | const hexBytes = new Uint8Array([
93 | 115, 116, 114, 208, 191, 209, 128, 208, 184, 208, 178, 208, 181, 209, 1
94 | ]);
95 |
96 | describe('hexToBytes', () => {
97 | it('converts hex string to byte array', () => {
98 | expect(ByteUtils.hexToBytes(hexString)).to.be.eql(hexBytes);
99 | });
100 |
101 | it('converts hex string in uppercase to byte array', () => {
102 | expect(ByteUtils.hexToBytes(hexString.toUpperCase())).to.be.eql(hexBytes);
103 | });
104 | });
105 |
106 | describe('bytesToHex', () => {
107 | it('converts byte array to hex string', () => {
108 | expect(ByteUtils.bytesToHex(hexBytes)).to.be.eql(hexString);
109 | });
110 |
111 | it('converts ArrayBuffer to hex string', () => {
112 | expect(ByteUtils.bytesToHex(hexBytes.buffer)).to.be.eql(hexString);
113 | });
114 | });
115 |
116 | describe('zeroBuffer', () => {
117 | it('fills array with zeroes', () => {
118 | const arr = new Uint8Array([1, 2, 3]);
119 | ByteUtils.zeroBuffer(arr);
120 | expect(arr).to.be.eql([0, 0, 0]);
121 | });
122 |
123 | it('fills array buffer with zeroes', () => {
124 | const arr = new Uint8Array([1, 2, 3]);
125 | ByteUtils.zeroBuffer(arr.buffer);
126 | expect(arr).to.be.eql([0, 0, 0]);
127 | });
128 | });
129 |
130 | describe('arrayToBuffer', () => {
131 | it('converts array to buffer', () => {
132 | const ab = ByteUtils.arrayToBuffer(new Uint8Array(4));
133 | expect(ab).to.be.an(ArrayBuffer);
134 | expect(ab.byteLength).to.be(4);
135 | });
136 |
137 | it('converts buffer to buffer', () => {
138 | const ab = ByteUtils.arrayToBuffer(new Uint8Array(4).buffer);
139 | expect(ab).to.be.an(ArrayBuffer);
140 | expect(ab.byteLength).to.be(4);
141 | });
142 |
143 | it('makes sliced buffer from sliced array', () => {
144 | const srcAb = new ArrayBuffer(10);
145 | const arr = new Uint8Array(srcAb, 1, 4);
146 | arr[0] = 1;
147 | expect(arr.buffer.byteLength).to.be(10);
148 | const ab = ByteUtils.arrayToBuffer(arr);
149 | expect(ab).to.be.an(ArrayBuffer);
150 | expect(ab.byteLength).to.be(4);
151 | expect(new Uint8Array(ab)[0]).to.be(1);
152 | });
153 | });
154 | });
155 |
--------------------------------------------------------------------------------
/lib/utils/binary-stream.ts:
--------------------------------------------------------------------------------
1 | export class BinaryStream {
2 | private _arrayBuffer: ArrayBuffer;
3 | private _dataView: DataView;
4 | private _pos: number;
5 | private readonly _canExpand: boolean;
6 |
7 | constructor(arrayBuffer?: ArrayBuffer) {
8 | this._arrayBuffer = arrayBuffer || new ArrayBuffer(1024);
9 | this._dataView = new DataView(this._arrayBuffer);
10 | this._pos = 0;
11 | this._canExpand = !arrayBuffer;
12 | }
13 |
14 | get pos(): number {
15 | return this._pos;
16 | }
17 |
18 | get byteLength(): number {
19 | return this._arrayBuffer.byteLength;
20 | }
21 |
22 | readBytes(size: number): ArrayBuffer {
23 | const buffer = this._arrayBuffer.slice(this._pos, this._pos + size);
24 | this._pos += size;
25 | return buffer;
26 | }
27 |
28 | readBytesToEnd(): ArrayBuffer {
29 | const size = this._arrayBuffer.byteLength - this._pos;
30 | return this.readBytes(size);
31 | }
32 |
33 | readBytesNoAdvance(startPos: number, endPos: number): ArrayBuffer {
34 | return this._arrayBuffer.slice(startPos, endPos);
35 | }
36 |
37 | writeBytes(bytes: ArrayBuffer | Uint8Array): void {
38 | const arr = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes;
39 | this.checkCapacity(arr.length);
40 | new Uint8Array(this._arrayBuffer).set(arr, this._pos);
41 | this._pos += arr.length;
42 | }
43 |
44 | getWrittenBytes(): ArrayBuffer {
45 | return this._arrayBuffer.slice(0, this._pos);
46 | }
47 |
48 | private checkCapacity(addBytes: number): void {
49 | const available = this._arrayBuffer.byteLength - this._pos;
50 | if (this._canExpand && available < addBytes) {
51 | let newLen = this._arrayBuffer.byteLength;
52 | const requestedLen = this._pos + addBytes;
53 | while (newLen < requestedLen) {
54 | newLen *= 2;
55 | }
56 | const newData = new Uint8Array(newLen);
57 | newData.set(new Uint8Array(this._arrayBuffer));
58 | this._arrayBuffer = newData.buffer;
59 | this._dataView = new DataView(this._arrayBuffer);
60 | }
61 | }
62 |
63 | getInt8(): number {
64 | const value = this._dataView.getInt8(this._pos);
65 | this._pos += 1;
66 | return value;
67 | }
68 |
69 | setInt8(value: number): void {
70 | this.checkCapacity(1);
71 | this._dataView.setInt8(this._pos, value);
72 | this._pos += 1;
73 | }
74 |
75 | getUint8(): number {
76 | const value = this._dataView.getUint8(this._pos);
77 | this._pos += 1;
78 | return value;
79 | }
80 |
81 | setUint8(value: number): void {
82 | this.checkCapacity(1);
83 | this._dataView.setUint8(this._pos, value);
84 | this._pos += 1;
85 | }
86 |
87 | getInt16(littleEndian: boolean): number {
88 | const value = this._dataView.getInt16(this._pos, littleEndian);
89 | this._pos += 2;
90 | return value;
91 | }
92 |
93 | setInt16(value: number, littleEndian: boolean): void {
94 | this.checkCapacity(2);
95 | this._dataView.setInt16(this._pos, value, littleEndian);
96 | this._pos += 2;
97 | }
98 |
99 | getUint16(littleEndian: boolean): number {
100 | const value = this._dataView.getUint16(this._pos, littleEndian);
101 | this._pos += 2;
102 | return value;
103 | }
104 |
105 | setUint16(value: number, littleEndian: boolean): void {
106 | this.checkCapacity(2);
107 | this._dataView.setUint16(this._pos, value, littleEndian);
108 | this._pos += 2;
109 | }
110 |
111 | getInt32(littleEndian: boolean): number {
112 | const value = this._dataView.getInt32(this._pos, littleEndian);
113 | this._pos += 4;
114 | return value;
115 | }
116 |
117 | setInt32(value: number, littleEndian: boolean): void {
118 | this.checkCapacity(4);
119 | this._dataView.setInt32(this._pos, value, littleEndian);
120 | this._pos += 4;
121 | }
122 |
123 | getUint32(littleEndian: boolean): number {
124 | const value = this._dataView.getUint32(this._pos, littleEndian);
125 | this._pos += 4;
126 | return value;
127 | }
128 |
129 | setUint32(value: number, littleEndian: boolean): void {
130 | this.checkCapacity(4);
131 | this._dataView.setUint32(this._pos, value, littleEndian);
132 | this._pos += 4;
133 | }
134 |
135 | getFloat32(littleEndian: boolean): number {
136 | const value = this._dataView.getFloat32(this._pos, littleEndian);
137 | this._pos += 4;
138 | return value;
139 | }
140 |
141 | setFloat32(value: number, littleEndian: boolean): void {
142 | this.checkCapacity(4);
143 | this._dataView.setFloat32(this._pos, value, littleEndian);
144 | this._pos += 4;
145 | }
146 |
147 | getFloat64(littleEndian: boolean): number {
148 | const value = this._dataView.getFloat64(this._pos, littleEndian);
149 | this._pos += 8;
150 | return value;
151 | }
152 |
153 | setFloat64(value: number, littleEndian: boolean): void {
154 | this.checkCapacity(8);
155 | this._dataView.setFloat64(this._pos, value, littleEndian);
156 | this._pos += 8;
157 | }
158 |
159 | getUint64(littleEndian: boolean): number {
160 | let part1 = this.getUint32(littleEndian),
161 | part2 = this.getUint32(littleEndian);
162 | if (littleEndian) {
163 | part2 *= 0x100000000;
164 | } else {
165 | part1 *= 0x100000000;
166 | }
167 | return part1 + part2;
168 | }
169 |
170 | setUint64(value: number, littleEndian: boolean): void {
171 | if (littleEndian) {
172 | this.setUint32(value & 0xffffffff, true);
173 | this.setUint32(Math.floor(value / 0x100000000), true);
174 | } else {
175 | this.checkCapacity(8);
176 | this.setUint32(Math.floor(value / 0x100000000), false);
177 | this.setUint32(value & 0xffffffff, false);
178 | }
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/test/utils/binary-stream.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { BinaryStream } from '../../lib';
3 |
4 | describe('BinaryStream', () => {
5 | const arr = new Uint8Array(100);
6 | for (let i = 0; i < arr.length; i++) {
7 | arr[i] = i;
8 | }
9 | const view = new DataView(arr.buffer);
10 |
11 | it('provides basic int and float getters available in DataView', () => {
12 | const stm = new BinaryStream(arr.buffer);
13 | expect(stm.getUint8()).to.be(view.getUint8(0));
14 | expect(stm.getUint8()).to.be(view.getUint8(1));
15 | expect(stm.getInt8()).to.be(view.getInt8(2));
16 | expect(stm.getInt8()).to.be(view.getInt8(3));
17 | expect(stm.getUint16(false)).to.be(view.getUint16(4, false));
18 | expect(stm.getUint16(true)).to.be(view.getUint16(6, true));
19 | expect(stm.getInt16(false)).to.be(view.getUint16(8, false));
20 | expect(stm.getInt16(true)).to.be(view.getUint16(10, true));
21 | expect(stm.getUint32(false)).to.be(view.getUint32(12, false));
22 | expect(stm.getUint32(true)).to.be(view.getUint32(16, true));
23 | expect(stm.getInt32(false)).to.be(view.getUint32(20, false));
24 | expect(stm.getInt32(true)).to.be(view.getUint32(24, true));
25 | expect(stm.getFloat32(false)).to.be(view.getFloat32(28, false));
26 | expect(stm.getFloat32(true)).to.be(view.getFloat32(32, true));
27 | expect(stm.getFloat64(false)).to.be(view.getFloat64(36, false));
28 | expect(stm.getFloat64(true)).to.be(view.getFloat64(44, true));
29 | });
30 |
31 | it('gets uint64', () => {
32 | let stm = new BinaryStream(arr.buffer);
33 | expect(stm.getUint64(false)).to.be(0x0001020304050607);
34 | expect(stm.getUint8()).to.be(8);
35 | stm = new BinaryStream(arr.buffer);
36 | expect(stm.getUint64(true)).to.be(0x0706050403020100);
37 | expect(stm.getUint8()).to.be(8);
38 | });
39 |
40 | it('provides basic int and float setters available in DataView', () => {
41 | const tmpArr = new Uint8Array(100);
42 | const stm = new BinaryStream(tmpArr.buffer);
43 | stm.setUint8(view.getUint8(0));
44 | stm.setUint8(view.getUint8(1));
45 | stm.setInt8(view.getInt8(2));
46 | stm.setInt8(view.getInt8(3));
47 | stm.setUint16(view.getUint16(4, false), false);
48 | stm.setUint16(view.getUint16(6, true), true);
49 | stm.setInt16(view.getUint16(8, false), false);
50 | stm.setInt16(view.getUint16(10, true), true);
51 | stm.setUint32(view.getUint32(12, false), false);
52 | stm.setUint32(view.getUint32(16, true), true);
53 | stm.setInt32(view.getUint32(20, false), false);
54 | stm.setInt32(view.getUint32(24, true), true);
55 | stm.setFloat32(view.getFloat32(28, false), false);
56 | stm.setFloat32(view.getFloat32(32, true), true);
57 | stm.setFloat64(view.getFloat64(36, false), false);
58 | stm.setFloat64(view.getFloat64(44, true), true);
59 | expectArrayBuffersEqual(tmpArr.buffer.slice(0, 52), arr.buffer.slice(0, 52));
60 | });
61 |
62 | it('sets uint64', () => {
63 | let tmpArr = new Uint8Array(9);
64 | let stm = new BinaryStream(tmpArr.buffer);
65 | stm.setUint64(0x0001020304050607, false);
66 | stm.setUint8(8);
67 | expectArrayBuffersEqual(tmpArr.buffer, arr.buffer.slice(0, 9));
68 | tmpArr = new Uint8Array(9);
69 | stm = new BinaryStream(tmpArr.buffer);
70 | stm.setUint64(0x0706050403020100, true);
71 | stm.setUint8(8);
72 | expectArrayBuffersEqual(tmpArr.buffer, arr.buffer.slice(0, 9));
73 | });
74 |
75 | it('reads bytes after pos', () => {
76 | let stm = new BinaryStream(arr.buffer);
77 | let bytes = stm.readBytesToEnd();
78 | expectArrayBuffersEqual(bytes, arr.buffer);
79 | bytes = stm.readBytesToEnd();
80 | expect(bytes.byteLength).to.be(0);
81 |
82 | stm = new BinaryStream(arr.buffer);
83 | stm.getUint8();
84 | stm.getFloat64(false);
85 | bytes = stm.readBytesToEnd();
86 | expectArrayBuffersEqual(bytes, arr.buffer.slice(9));
87 | bytes = stm.readBytesToEnd();
88 | expect(bytes.byteLength).to.be(0);
89 |
90 | stm = new BinaryStream(arr.buffer);
91 | for (let i = 0; i < 100; i++) {
92 | stm.getUint8();
93 | }
94 | bytes = stm.readBytesToEnd();
95 | expect(bytes.byteLength).to.be(0);
96 | });
97 |
98 | it('reads number of bytes after pos', () => {
99 | let stm = new BinaryStream(arr.buffer);
100 | let bytes = stm.readBytes(100);
101 | expectArrayBuffersEqual(bytes, arr.buffer);
102 | bytes = stm.readBytesToEnd();
103 | expect(bytes.byteLength).to.be(0);
104 |
105 | stm = new BinaryStream(arr.buffer);
106 | stm.getUint8();
107 | stm.getFloat64(false);
108 | bytes = stm.readBytes(50);
109 | expectArrayBuffersEqual(bytes, arr.buffer.slice(9, 59));
110 | bytes = stm.readBytesToEnd();
111 | expect(bytes.byteLength).to.be(41);
112 |
113 | stm = new BinaryStream(arr.buffer);
114 | for (let i = 0; i < 100; i++) {
115 | stm.getUint8();
116 | }
117 | bytes = stm.readBytes(5);
118 | expect(bytes.byteLength).to.be(0);
119 | });
120 |
121 | it('returns position', () => {
122 | const stm = new BinaryStream(arr.buffer);
123 | expect(stm.pos).to.be(0);
124 | stm.getInt8();
125 | expect(stm.pos).to.be(1);
126 | stm.readBytesToEnd();
127 | expect(stm.pos).to.be(100);
128 | });
129 |
130 | it('returns byteLength', () => {
131 | const stm = new BinaryStream(arr.buffer);
132 | expect(stm.byteLength).to.be(arr.buffer.byteLength);
133 | });
134 |
135 | it('can read bytes without changing position', () => {
136 | const stm = new BinaryStream(arr.buffer);
137 | expect(stm.pos).to.be(0);
138 | const bytes = stm.readBytesNoAdvance(10, 12);
139 | expect(stm.pos).to.be(0);
140 | expect(new Uint8Array(bytes)).to.be.eql(new Uint8Array([10, 11]));
141 | });
142 |
143 | it('can expand length on write', () => {
144 | const stm = new BinaryStream(new Uint8Array(2).buffer);
145 | // @ts-ignore
146 | stm._canExpand = true;
147 | stm.writeBytes(new Uint8Array([0, 1, 2]));
148 | stm.setUint8(3);
149 | stm.writeBytes(new Uint8Array([4]).buffer);
150 | expect(new Uint8Array(stm.getWrittenBytes())).to.be.eql(new Uint8Array([0, 1, 2, 3, 4]));
151 | });
152 |
153 | it('creates buffer itself and expands it', () => {
154 | const stm = new BinaryStream();
155 | stm.writeBytes(new Uint8Array(1021));
156 | stm.writeBytes(new Uint8Array([0, 1, 2]));
157 | stm.setUint8(3);
158 | stm.writeBytes(new Uint8Array([4]).buffer);
159 | expect(stm.getWrittenBytes().byteLength).to.be.eql(1026);
160 | });
161 |
162 | function expectArrayBuffersEqual(ab1: ArrayBuffer, ab2: ArrayBuffer) {
163 | expect(new Uint8Array(ab1)).to.eql(new Uint8Array(ab2));
164 | }
165 | });
166 |
--------------------------------------------------------------------------------
/format/Kdbx.tcl:
--------------------------------------------------------------------------------
1 | # KDBX file template for HexFiend
2 | # https://github.com/keeweb/kdbxweb/blob/master/format/Kdbx.tcl
3 | # MIT license
4 | #
5 | # Format reference: https://keepass.info/help/kb/kdbx_4.html
6 | # HexFiend templates docs: https://github.com/ridiculousfish/HexFiend/tree/master/templates
7 |
8 | little_endian
9 |
10 | requires 0 "03D9A29A 67FB4BB5"
11 |
12 | set version 0
13 |
14 | section "Header" {
15 | uint32 -hex "Magic"
16 | uint32 -hex "Signature"
17 | uint16 "Version minor"
18 | set version [uint16 "Version major"]
19 |
20 | if {$version == 3 || $version == 4} {
21 | set field 1
22 | while {![end] && $field != 0} {
23 | set field [uint8]
24 | move -1
25 | if {$field == 0} {
26 | set field_desc "EndOfHeader"
27 | } elseif {$field == 1} {
28 | set field_desc "Comment"
29 | } elseif {$field == 2} {
30 | set field_desc "CipherID"
31 | } elseif {$field == 3} {
32 | set field_desc "CompressionFlags"
33 | } elseif {$field == 4} {
34 | set field_desc "MasterSeed"
35 | } elseif {$field == 5} {
36 | set field_desc "TransformSeed"
37 | } elseif {$field == 6} {
38 | set field_desc "TransformRounds"
39 | } elseif {$field == 7} {
40 | set field_desc "EncryptionIV"
41 | } elseif {$field == 8} {
42 | set field_desc "ProtectedStreamKey"
43 | } elseif {$field == 9} {
44 | set field_desc "StreamStartBytes"
45 | } elseif {$field == 10} {
46 | set field_desc "InnerRandomStreamID"
47 | } elseif {$field == 11} {
48 | set field_desc "KdfParameters"
49 | } elseif {$field == 12} {
50 | set field_desc "PublicCustomData"
51 | } else {
52 | set field_desc "Unknown"
53 | }
54 | section "Header field" {
55 | uint8 "TypeID"
56 | move -1
57 | entry "Type" $field_desc 1
58 | move 1
59 | if {$version < 4} {
60 | set size [uint16 "Data length"]
61 | } else {
62 | set size [uint32 "Data length"]
63 | }
64 | hex $size "Data"
65 | if {$field == 1} {
66 | move -$size
67 | ascii $size "Comment"
68 | } elseif {$field == 2} {
69 | if {$size == 16} {
70 | move -16
71 | uuid "Cipher UUID"
72 | }
73 | } elseif {$field == 3} {
74 | if {$size == 4} {
75 | move -4
76 | set compression_flags [uint32 "Compression algorithm ID"]
77 | move -4
78 | if {$compression_flags == 0} {
79 | entry "Compression algorithm" "None" 4
80 | } elseif {$compression_flags == 1} {
81 | entry "Compression algorithm" "Gzip" 4
82 | }
83 | move 4
84 | }
85 | } elseif {$field == 6} {
86 | if {$size == 8} {
87 | move -8
88 | uint64 "Transform rounds"
89 | }
90 | } elseif {$field == 11} {
91 | section "KDF parameters" {
92 | move -$size
93 | set dict_version [uint16 -hex "Version"]
94 | if {$dict_version == 0x100} {
95 | set param_type 1
96 | while {![end] && $param_type != 0} {
97 | section "Parameter" {
98 | set param_type [uint8 "TypeID"]
99 | if {$param_type == 0} {
100 | set param_desc "End"
101 | } elseif {$param_type == 0x04} {
102 | set param_desc "UInt32"
103 | } elseif {$param_type == 0x05} {
104 | set param_desc "UInt64"
105 | } elseif {$param_type == 0x08} {
106 | set param_desc "Bool"
107 | } elseif {$param_type == 0x0C} {
108 | set param_desc "Int32"
109 | } elseif {$param_type == 0x0D} {
110 | set param_desc "Int64"
111 | } elseif {$param_type == 0x18} {
112 | set param_desc "String"
113 | } elseif {$param_type == 0x42} {
114 | set param_desc "Bytes"
115 | } else {
116 | set param_desc "Unknown"
117 | }
118 | move -1
119 | entry "Type" $param_desc 1
120 | move 1
121 | if {$param_type != 0} {
122 | set key_length [uint32 "Key length"]
123 | ascii $key_length "Key"
124 | set value_length [uint32 "Value length"]
125 | hex $value_length "Value"
126 | if {$param_type == 0x04 && $value_length == 4} {
127 | move -4
128 | uint32 "UInt32"
129 | } elseif {$param_type == 0x05 && $value_length == 8} {
130 | move -8
131 | uint64 "UInt64"
132 | } elseif {$param_type == 0x08 && $value_length == 1} {
133 | move -1
134 | int8 "Bool"
135 | } elseif {$param_type == 0x0C && $value_length == 4} {
136 | move -4
137 | int32 "Int32"
138 | } elseif {$param_type == 0x0D && $value_length == 8} {
139 | move -8
140 | int64 "Int64"
141 | } elseif {$param_type == 0x18} {
142 | move -$value_length
143 | ascii $value_length "String"
144 | }
145 | }
146 | }
147 | }
148 | } else {
149 | move -2
150 | move $size
151 | }
152 | }
153 | }
154 | }
155 | }
156 | } else {
157 | entry "Error" "Unexpected version, supported: 3 or 4"
158 | }
159 | }
160 |
161 | if {$version == 3} {
162 | bytes eof "Encrypted data"
163 | } elseif {$version == 4} {
164 | hex 32 "Header SHA256"
165 | hex 32 "Header HMAC"
166 | bytes eof "Encrypted data"
167 | }
168 |
--------------------------------------------------------------------------------
/test/format/kdbx-binaries.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils, KdbxBinaries, ProtectedValue } from '../../lib';
3 |
4 | describe('KdbxBinaries', () => {
5 | const protectedBinary = ProtectedValue.fromBinary(new TextEncoder().encode('bin'));
6 | const protectedBinary2 = ProtectedValue.fromBinary(new TextEncoder().encode('another'));
7 | const hash = '51a1f05af85e342e3c849b47d387086476282d5f50dc240c19216d6edfb1eb5a';
8 | const hash2 = 'ae448ac86c4e8e4dec645729708ef41873ae79c6dff84eff73360989487f08e5';
9 |
10 | describe('add', () => {
11 | it('adds a ProtectedValue', async () => {
12 | const binaries = new KdbxBinaries();
13 | const bin = await binaries.add(protectedBinary);
14 | expect(bin).to.be.ok();
15 | expect(bin.hash).to.be(hash);
16 | expect(binaries.getAllWithHashes()).to.eql([{ hash, value: protectedBinary }]);
17 | });
18 |
19 | it('adds an ArrayBuffer', async () => {
20 | const binaries = new KdbxBinaries();
21 | const ab = ByteUtils.arrayToBuffer(protectedBinary.getBinary());
22 | const bin = await binaries.add(ab);
23 | expect(bin).to.be.ok();
24 | expect(bin.hash).to.be(hash);
25 | expect(binaries.getAllWithHashes()).to.eql([{ hash, value: ab }]);
26 | });
27 |
28 | it('adds an Uint8Array', async () => {
29 | const binaries = new KdbxBinaries();
30 | const arr = protectedBinary.getBinary();
31 | const bin = await binaries.add(arr);
32 | expect(bin).to.be.ok();
33 | expect(bin.hash).to.be(hash);
34 | expect(binaries.getAllWithHashes()).to.eql([{ hash, value: arr.buffer }]);
35 | });
36 | });
37 |
38 | describe('addWithNextId', () => {
39 | it('adds a binary and generates id', async () => {
40 | const binaries = new KdbxBinaries();
41 | binaries.addWithNextId(protectedBinary);
42 | binaries.addWithNextId(protectedBinary2);
43 |
44 | await binaries.computeHashes();
45 |
46 | const found1 = binaries.getByRef({ ref: '0' });
47 | expect(found1).to.be.ok();
48 | expect(found1!.hash).to.be(hash);
49 |
50 | const found2 = binaries.getByRef({ ref: '1' });
51 | expect(found2).to.be.ok();
52 | expect(found2!.hash).to.be(hash2);
53 |
54 | const notFound = binaries.getByRef({ ref: '2' });
55 | expect(notFound).to.be(undefined);
56 | });
57 | });
58 |
59 | describe('addWithId', () => {
60 | it('adds a binary with the specified id', async () => {
61 | const binaries = new KdbxBinaries();
62 | binaries.addWithId('0', protectedBinary);
63 | binaries.addWithId('0', protectedBinary2);
64 |
65 | await binaries.computeHashes();
66 |
67 | const found2 = binaries.getByRef({ ref: '0' });
68 | expect(found2).to.be.ok();
69 | expect(found2!.hash).to.be(hash2);
70 |
71 | const notFound = binaries.getByRef({ ref: '1' });
72 | expect(notFound).to.be(undefined);
73 | });
74 | });
75 |
76 | describe('addWithHash', () => {
77 | it('adds a binary with the specified hash', () => {
78 | const binaries = new KdbxBinaries();
79 | binaries.addWithHash({ hash, value: protectedBinary });
80 |
81 | expect(binaries.getAllWithHashes()).to.eql([{ hash, value: protectedBinary }]);
82 | });
83 | });
84 |
85 | describe('deleteWithHash', () => {
86 | it('adds a binary with the specified hash', () => {
87 | const binaries = new KdbxBinaries();
88 | binaries.addWithHash({ hash, value: protectedBinary });
89 | binaries.addWithHash({ hash: hash2, value: protectedBinary2 });
90 | binaries.deleteWithHash(hash2);
91 |
92 | expect(binaries.getAllWithHashes()).to.eql([{ hash, value: protectedBinary }]);
93 | });
94 | });
95 |
96 | describe('getByRef', () => {
97 | it('returns a binary by reference', async () => {
98 | const binaries = new KdbxBinaries();
99 | binaries.addWithNextId(protectedBinary);
100 | binaries.addWithNextId(protectedBinary2);
101 |
102 | await binaries.computeHashes();
103 |
104 | binaries.deleteWithHash(hash2);
105 |
106 | const found1 = binaries.getByRef({ ref: '0' });
107 | expect(found1).to.be.ok();
108 | expect(found1!.hash).to.be(hash);
109 |
110 | expect(binaries.getByRef({ ref: '1' })).to.be(undefined);
111 | expect(binaries.getByRef({ ref: '2' })).to.be(undefined);
112 | });
113 | });
114 |
115 | describe('get...', () => {
116 | it('gets a reference by hash', async () => {
117 | const binaries = new KdbxBinaries();
118 | binaries.addWithNextId(protectedBinary);
119 | binaries.addWithNextId(protectedBinary2);
120 |
121 | await binaries.computeHashes();
122 |
123 | const ref1 = binaries.getRefByHash(hash);
124 | expect(ref1).to.be.ok();
125 | expect(ref1?.ref).to.be('0');
126 |
127 | const ref2 = binaries.getRefByHash(hash2);
128 | expect(ref2).to.be.ok();
129 | expect(ref2?.ref).to.be('1');
130 |
131 | const refNotExisting = binaries.getRefByHash('boo');
132 | expect(refNotExisting).to.be(undefined);
133 |
134 | const all = binaries.getAll();
135 | expect(all).to.eql([
136 | { ref: '0', value: protectedBinary },
137 | { ref: '1', value: protectedBinary2 }
138 | ]);
139 |
140 | const allWithHashes = binaries.getAllWithHashes();
141 | expect(allWithHashes).to.eql([
142 | { hash, value: protectedBinary },
143 | { hash: hash2, value: protectedBinary2 }
144 | ]);
145 |
146 | expect(binaries.getValueByHash(hash)).to.be(protectedBinary);
147 | expect(binaries.getValueByHash(hash2)).to.be(protectedBinary2);
148 | expect(binaries.getValueByHash('boo')).to.be(undefined);
149 | });
150 | });
151 |
152 | describe('isKdbxBinaryRef', () => {
153 | it('returns true for KdbxBinaryRef', () => {
154 | const isRef = KdbxBinaries.isKdbxBinaryRef({ ref: '1' });
155 | expect(isRef).to.be(true);
156 | });
157 |
158 | it('returns false for a ProtectedValue', () => {
159 | const isRef = KdbxBinaries.isKdbxBinaryRef(protectedBinary);
160 | expect(isRef).to.be(false);
161 | });
162 |
163 | it('returns false for undefined', () => {
164 | const isRef = KdbxBinaries.isKdbxBinaryRef(undefined);
165 | expect(isRef).to.be(false);
166 | });
167 | });
168 |
169 | describe('isKdbxBinaryWithHash', () => {
170 | it('returns true for KdbxBinaryWithHash', () => {
171 | const isRef = KdbxBinaries.isKdbxBinaryWithHash({ ref: '1', hash });
172 | expect(isRef).to.be(true);
173 | });
174 |
175 | it('returns false for KdbxBinaryRef', () => {
176 | const isRef = KdbxBinaries.isKdbxBinaryWithHash({ ref: '1' });
177 | expect(isRef).to.be(false);
178 | });
179 |
180 | it('returns false for a ProtectedValue', () => {
181 | const isRef = KdbxBinaries.isKdbxBinaryWithHash(protectedBinary);
182 | expect(isRef).to.be(false);
183 | });
184 |
185 | it('returns false for undefined', () => {
186 | const isRef = KdbxBinaries.isKdbxBinaryWithHash(undefined);
187 | expect(isRef).to.be(false);
188 | });
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/release-notes.md:
--------------------------------------------------------------------------------
1 | Release notes
2 | -------------
3 | ##### v2.1.1 (2021-09-06)
4 | `-` updated dependencies
5 |
6 | ##### v2.1.0 (2021-06-17)
7 | `-` replaced `pako` with `fflate` to reduce bundle size
8 |
9 | ##### v2.0.5 (2021-05-25)
10 | `-` writable `passwordHash` and `keyFileHash` credentials properties
11 |
12 | ##### v2.0.4 (2021-05-19)
13 | `-` fixed saving KDBX3 files without compression
14 |
15 | ##### v2.0.3 (2021-05-14)
16 | `-` fixed a crash in Electron when KDBX3 contains large attachments
17 |
18 | ##### v2.0.2 (2021-05-10)
19 | `+` exposed `ProtectedValue.salt` and `ProtectedValue.value`
20 | `+` new static method: `ProtectedValue.fromBase64`
21 | `+` new instance method: `ProtectedValue::toBase64`
22 |
23 | ##### v2.0.1 (2021-05-08)
24 | `+` added `passwordHash` and `keyFileHash` to credentials
25 |
26 | ##### v2.0.0 (2021-05-08)
27 | `*` the library rewritten in TypeScript
28 | `*` dist files removed from the repo
29 | `*` browser support limited to two latest versions
30 | `*` `Random` removed, use `CryptoEngine.random` instead
31 | `*` creating `KdbxUuid` with bad values produces an error
32 | `*` all library modules added to exports
33 | `*` underscore removed from 'private' class methods
34 | `*` some object exports replaced with ES6 exports
35 | `*` binaries interface changed completely, see kdbx-binary.js
36 | `*` replaced `forEach` with `*allItems` `*allGroups` `*allEntries`
37 | `*` `object {}` => `Map<>`: `*.customData`, `meta.customIcons`, `entry.fields`, `entry.binaries`
38 | `*` KDBX 4.1 support
39 | `*` custom icons contain name and lastModified when possible
40 | `*` error for files with too high minor version
41 |
42 | ##### v1.14.4 (2021-03-19)
43 | `*` cleaning up kdbx.xml after save to save memory
44 |
45 | ##### v1.14.3 (2021-03-19)
46 | `*` cleaning up kdbx.xml after load to save memory
47 |
48 | ##### v1.14.2 (2021-02-01)
49 | `-` fixed setting KDF to Argon2id
50 |
51 | ##### v1.14.1 (2020-12-31)
52 | `-` fixed new lines removal in non-encoded fields
53 |
54 | ##### v1.14.0 (2020-12-30)
55 | `-` fixed parsing xml with bad characters
56 |
57 | ##### v1.13.0 (2020-12-09)
58 | `+` possibility to generate a V2 keyfile
59 | `*` createRandomKeyFile returns a promise
60 |
61 | ##### v1.12.1 (2020-12-07)
62 | `+` relaxed keyfile version checking
63 |
64 | ##### v1.12.0 (2020-12-07)
65 | `+` V2 keyfiles support
66 |
67 | ##### v1.11.0 (2020-12-03)
68 | `+` Argon2id support
69 |
70 | ##### v1.10.0 (2020-09-12)
71 | `-` fixed KeyEncryptionRounds header field data type
72 | `-` missing polyfill added for old Edge versions
73 |
74 | ##### v1.9.0 (2020-06-04)
75 | `*` removed text-encoding polyfill
76 | `+` development: eslint
77 | `+` development: prettier
78 |
79 | ##### v1.8.0 (2020-06-02)
80 | `*` default format changed to KDBX4
81 |
82 | ##### v1.7.1 (2020-05-31)
83 | `+` fixed empty icon not understood by other clients
84 |
85 | ##### v1.7.0 (2020-05-31)
86 | `+` fixed empty auto-type obfuscation setting not understood by other clients
87 |
88 | ##### v1.6.0 (2020-04-10)
89 | `+` challenge-response keys support
90 |
91 | ##### v1.5.8 (2020-03-15)
92 | `+` fixed historyMaxItems for 0 and -1
93 |
94 | ##### v1.5.7 (2019-12-01)
95 | `+` fixed minor version for v4 files
96 |
97 | ##### v1.5.6 (2019-10-26)
98 | `+` generating missing ids while reading files
99 |
100 | ##### v1.5.5 (2019-10-04)
101 | `-` fixed another bug in importing entries
102 |
103 | ##### v1.5.4 (2019-10-04)
104 | `-` fixed importing entries
105 |
106 | ##### v1.5.3 (2019-09-24)
107 | `-` fix #26: library usage issues in node.js
108 |
109 | ##### v1.5.2 (2019-09-22)
110 | `-` removed a leaked dependency
111 |
112 | ##### v1.5.1 (2019-09-22)
113 | `-` fixed a bug in importing attachments
114 |
115 | ##### v1.5.0 (2019-09-22)
116 | `+` importing entries from other files using `Kdbx.importEntry`
117 | `*` debug and release versions are now provided: kdbxweb.js and kdbxweb.min.js
118 |
119 | ##### v1.4.2 (2019-09-14)
120 | `+` default encryption rounds increased to 300000
121 |
122 | ##### v1.4.1 (2019-09-14)
123 | `+` setting file KDF with `kdbx.setKdf`
124 |
125 | ##### v1.4.0 (2019-09-14)
126 | `+` setting file version with `kdbx.setVersion`
127 |
128 | ##### v1.3.0 (2019-09-08)
129 | `-` pretty-printing xml option in `Kdbx::saveXml`
130 |
131 | ##### v1.2.7 (2019-03-06)
132 | `-` fixed header after upgrade to kdbx4
133 |
134 | ##### v1.2.6 (2018-12-19)
135 | `*` performance improvement
136 |
137 | ##### v1.2.5 (2018-11-10)
138 | `+` removed usages of obsolete Buffer() constructor
139 |
140 | ##### v1.2.4 (2018-07-13)
141 | `+` fixed large attachments error: keeweb/keeweb#922
142 |
143 | ##### v1.2.3 (2018-03-29)
144 | `+` throw an error if there's not enough data in a file
145 |
146 | ##### v1.2.2 (2018-03-03)
147 | `+` copyright year updated
148 |
149 | ##### v1.2.1 (2018-03-03)
150 | `+` support AES KDF in KDBX4
151 |
152 | ##### v1.2.0 (2018-03-03)
153 | `!` dropped IE support
154 |
155 | ##### v1.1.0 (2018-02-14)
156 | `+` support ChaCha2 in KDBX3
157 |
158 | ##### v1.0.2 (2017-09-29)
159 | `-` improved decoding performance, fix #17
160 |
161 | ##### v1.0.1 (2017-02-27)
162 | `-` fix opening db with empty binaries
163 |
164 | ##### v1.0.0 (2017-02-01)
165 | `+` KDBX4 support
166 | `!` API updated
167 |
168 | ##### v0.4.6 (2016-08-23)
169 | `-` fix keyfiles with bom
170 |
171 | ##### v0.4.5 (2016-08-21)
172 | `+` support raw 32-byte and hex 64-byte keyfiles
173 |
174 | ##### v0.4.4 (2016-08-16)
175 | `-` fix keyfiles with unicode characters
176 |
177 | ##### v0.4.3 (2016-07-30)
178 | `-` index bugfix for v4.0.2
179 |
180 | ##### v0.4.2 (2016-07-30)
181 | `+` target index argument in move function
182 |
183 | ##### v0.4.1 (2016-04-21)
184 | `-` fixed bug in Firefox
185 |
186 | ##### v0.4.0 (2016-04-21)
187 | `!` xmldom is now external dependency
188 | `-` updated xmldom to patched version without encoder bug
189 |
190 | ##### v0.3.11 (2016-04-10)
191 | `-` create recycle bin if it's enabled but not yet created
192 |
193 | ##### v0.3.10 (2016-04-03)
194 | `-` Fixed random keyfile generator
195 |
196 | ##### v0.3.8 (2016-03-04)
197 | `+` Expose Kdbx.Consts.Signatures
198 |
199 | ##### v0.3.7 (2016-03-04)
200 | `-` Preserve empty fields in entries
201 |
202 | ##### v0.3.6 (2016-03-01)
203 | `+` Kdbx.loadXml
204 |
205 | ##### v0.3.5 (2016-02-26)
206 | `+` Allow to open db with empty password and keyfile
207 | `+` Using secure random generator if it's available
208 |
209 | ##### v0.3.4 (2016-02-14)
210 | KdbxCredentials.createKeyFileWithHash
211 |
212 | ##### v0.3.3 (2015-12-17)
213 | Binaries management
214 |
215 | ##### v0.3.2 (2015-12-13)
216 | ASCII-only dist
217 |
218 | ##### v0.3.1 (2015-12-02)
219 | Version fix
220 |
221 | ##### v0.3.0 (2015-12-02)
222 | Merge
223 | `+` Kdbx.merge
224 | `+` Kdbx.[get,set,remove]LocalEditState
225 | `+` KdbxEntry.removeHistory
226 | `+` KdbxGroup.forEach now accepts thisArg
227 |
228 | ##### v0.2.6 (2015-11-22)
229 | Custom icons cleanup
230 |
231 | ##### v0.2.6 (2015-11-21)
232 | History cleanup method
233 |
234 | ##### v0.2.5 (2015-11-10)
235 | Fixed KeePassX compatibility bugs
236 |
237 | ##### v0.2.4 (2015-11-09)
238 | `+` Export Uuid
239 | `-` Fix entry history write bug
240 |
241 | ##### v0.2.3 (2015-11-07)
242 | Support DeletedObjects
243 |
244 | ##### v0.2.2 (2015-11-06)
245 | Build fix
246 | `-` fixed node.js install issues
247 |
248 | ##### v0.2.1 (2015-11-04)
249 | API conststency
250 | `+` entry.parentGroup, group.parentGroup
251 | `!` Kdbx.move, Kdbx.remove now doesn't require parent group
252 |
253 | ##### v0.2.0 (2015-11-04)
254 | WebCrypto support
255 | `!` Kdbx.load, Kdbx.save, Kdbx.saveXml are now async
256 |
257 | ##### v0.1.12 (2015-11-02)
258 | Ability to use binary keyfiles
259 |
260 | ##### v0.1.11 (2015-10-24)
261 | Allow to change password and keyfile
262 |
263 | ##### v0.1.9 (2015-10-22)
264 | Fixed loading in nodejs
265 |
266 | ##### v0.1.8 (2015-10-17)
267 | Save as XML
268 |
269 | ##### v0.1.7 (2015-10-17)
270 | Entry creation bug fixed
271 |
272 | ##### v0.1.6 (2015-10-11)
273 | Move/delete entries/groups
274 |
275 | ##### v0.1.5 (2015-10-11)
276 | Creation of groups and entries
277 |
278 | ##### v0.1.4 (2015-09-27)
279 | Entry copy method
280 |
281 | ##### v0.1.3 (2015-09-19)
282 | Loader bug fixed
283 |
284 | ##### v0.1.2 (2015-09-19)
285 | Key processing speedup
286 |
287 | ##### v0.1.1 (2015-09-06)
288 | More exports
289 |
290 | ##### v0.1.0 (2015-08-22)
291 | First public beta
292 |
--------------------------------------------------------------------------------
/lib/crypto/crypto-engine.ts:
--------------------------------------------------------------------------------
1 | /*
2 | @note as of node 19, webcrypto is now global.
3 | update script to work with node 19, until then, build with node 18
4 | - https://nodejs.org/id/blog/announcements/v19-release-announce#stable-webcrypto
5 |
6 | - The WebCrypto API is now stable (with the exception of the following algorithms:
7 | Ed25519, Ed448, X25519, and X448)
8 |
9 | - Use globalThis.crypto or require('node:crypto').webcrypto to access this module.
10 | */
11 |
12 | import { KdbxError } from '../errors/kdbx-error';
13 | import { ErrorCodes } from '../defs/consts';
14 | import { arrayToBuffer, hexToBytes } from '../utils/byte-utils';
15 | import { ChaCha20 } from './chacha20';
16 | import * as nodeCrypto from 'crypto';
17 |
18 | const EmptySha256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
19 | const EmptySha512 =
20 | 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce' +
21 | '47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e';
22 |
23 | // maxRandomQuota is the max number of random bytes you can asks for from the cryptoEngine
24 | // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
25 | const MaxRandomQuota = 65536;
26 |
27 | export function sha256(data: ArrayBuffer): Promise {
28 | if (!data.byteLength) {
29 | return Promise.resolve(arrayToBuffer(hexToBytes(EmptySha256)));
30 | }
31 | if (global.crypto?.subtle) {
32 | return global.crypto.subtle.digest({ name: 'SHA-256' }, data);
33 | } else {
34 | return new Promise((resolve) => {
35 | const sha = nodeCrypto.createHash('sha256');
36 | const hash = sha.update(Buffer.from(data)).digest();
37 | resolve(hash.buffer);
38 | });
39 | }
40 | }
41 |
42 | export function sha512(data: ArrayBuffer): Promise {
43 | if (!data.byteLength) {
44 | return Promise.resolve(arrayToBuffer(hexToBytes(EmptySha512)));
45 | }
46 | if (global.crypto?.subtle) {
47 | return global.crypto.subtle.digest({ name: 'SHA-512' }, data);
48 | } else {
49 | return new Promise((resolve) => {
50 | const sha = nodeCrypto.createHash('sha512');
51 | const hash = sha.update(Buffer.from(data)).digest();
52 | resolve(hash.buffer);
53 | });
54 | }
55 | }
56 |
57 | export function hmacSha256(key: ArrayBuffer, data: ArrayBuffer): Promise {
58 | if (global.crypto?.subtle) {
59 | const algo = { name: 'HMAC', hash: { name: 'SHA-256' } };
60 | return global.crypto.subtle
61 | .importKey('raw', key, algo, false, ['sign'])
62 | .then((subtleKey) => {
63 | return global.crypto.subtle.sign(algo, subtleKey, data);
64 | });
65 | } else {
66 | return new Promise((resolve) => {
67 | const hmac = nodeCrypto.createHmac('sha256', Buffer.from(key));
68 | const hash = hmac.update(Buffer.from(data)).digest();
69 | resolve(hash.buffer);
70 | });
71 | }
72 | }
73 |
74 | export abstract class AesCbc {
75 | abstract importKey(key: ArrayBuffer): Promise;
76 | abstract encrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise;
77 | abstract decrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise;
78 | }
79 |
80 | class AesCbcSubtle extends AesCbc {
81 | private _key: CryptoKey | undefined;
82 |
83 | private get key(): CryptoKey {
84 | if (!this._key) {
85 | throw new KdbxError(ErrorCodes.InvalidState, 'no key');
86 | }
87 | return this._key;
88 | }
89 |
90 | importKey(key: ArrayBuffer): Promise {
91 | return global.crypto.subtle
92 | .importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt'])
93 | .then((key) => {
94 | this._key = key;
95 | });
96 | }
97 |
98 | encrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise {
99 | return global.crypto.subtle.encrypt(
100 | { name: 'AES-CBC', iv },
101 | this.key,
102 | data
103 | ) as Promise;
104 | }
105 |
106 | decrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise {
107 | return global.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, this.key, data).catch(() => {
108 | throw new KdbxError(ErrorCodes.InvalidKey, 'invalid key');
109 | }) as Promise;
110 | }
111 | }
112 |
113 | class AesCbcNode extends AesCbc {
114 | private _key: ArrayBuffer | undefined;
115 |
116 | private get key(): ArrayBuffer {
117 | if (!this._key) {
118 | throw new KdbxError(ErrorCodes.InvalidState, 'no key');
119 | }
120 | return this._key;
121 | }
122 |
123 | importKey(key: ArrayBuffer): Promise {
124 | this._key = key;
125 | return Promise.resolve();
126 | }
127 |
128 | encrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise {
129 | return Promise.resolve().then(() => {
130 | const cipher = nodeCrypto.createCipheriv(
131 | 'aes-256-cbc',
132 | Buffer.from(this.key),
133 | Buffer.from(iv)
134 | );
135 | const block = cipher.update(Buffer.from(data));
136 | return arrayToBuffer(Buffer.concat([block, cipher.final()]));
137 | });
138 | }
139 |
140 | decrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise {
141 | return Promise.resolve()
142 | .then(() => {
143 | const cipher = nodeCrypto.createDecipheriv(
144 | 'aes-256-cbc',
145 | Buffer.from(this.key),
146 | Buffer.from(iv)
147 | );
148 | const block = cipher.update(Buffer.from(data));
149 | return arrayToBuffer(Buffer.concat([block, cipher.final()]));
150 | })
151 | .catch(() => {
152 | throw new KdbxError(ErrorCodes.InvalidKey, 'invalid key');
153 | });
154 | }
155 | }
156 |
157 | export function createAesCbc(): AesCbc {
158 | if (global.crypto?.subtle) {
159 | return new AesCbcSubtle();
160 | } else {
161 | return new AesCbcNode();
162 | }
163 | }
164 |
165 | function safeRandomWeb(len: number): Uint8Array {
166 | const randomBytes = new Uint8Array(len);
167 | while (len > 0) {
168 | let segmentSize = len % MaxRandomQuota;
169 | segmentSize = segmentSize > 0 ? segmentSize : MaxRandomQuota;
170 | const randomBytesSegment = new Uint8Array(segmentSize);
171 | global.crypto.getRandomValues(randomBytesSegment);
172 | len -= segmentSize;
173 | randomBytes.set(randomBytesSegment, len);
174 | }
175 | return randomBytes;
176 | }
177 |
178 | export function random(len: number): Uint8Array {
179 | if (global.crypto?.subtle) {
180 | return safeRandomWeb(len);
181 | } else {
182 | return new Uint8Array(nodeCrypto.randomBytes(len));
183 | }
184 | }
185 |
186 | export function chacha20(
187 | data: ArrayBuffer,
188 | key: ArrayBuffer,
189 | iv: ArrayBuffer
190 | ): Promise {
191 | return Promise.resolve().then(() => {
192 | const algo = new ChaCha20(new Uint8Array(key), new Uint8Array(iv));
193 | return arrayToBuffer(algo.encrypt(new Uint8Array(data)));
194 | });
195 | }
196 |
197 | export const Argon2TypeArgon2d = 0;
198 | export const Argon2TypeArgon2id = 2;
199 |
200 | export type Argon2Type = typeof Argon2TypeArgon2d | typeof Argon2TypeArgon2id;
201 | export type Argon2Version = 0x10 | 0x13;
202 |
203 | export type Argon2Fn = (
204 | password: ArrayBuffer,
205 | salt: ArrayBuffer,
206 | memory: number,
207 | iterations: number,
208 | length: number,
209 | parallelism: number,
210 | type: Argon2Type,
211 | version: Argon2Version
212 | ) => Promise;
213 |
214 | let argon2impl: Argon2Fn | undefined;
215 |
216 | export function argon2(
217 | password: ArrayBuffer,
218 | salt: ArrayBuffer,
219 | memory: number,
220 | iterations: number,
221 | length: number,
222 | parallelism: number,
223 | type: Argon2Type,
224 | version: Argon2Version
225 | ): Promise {
226 | if (argon2impl) {
227 | return argon2impl(
228 | password,
229 | salt,
230 | memory,
231 | iterations,
232 | length,
233 | parallelism,
234 | type,
235 | version
236 | ).then(arrayToBuffer);
237 | }
238 | return Promise.reject(new KdbxError(ErrorCodes.NotImplemented, 'argon2 not implemented'));
239 | }
240 |
241 | export function setArgon2Impl(impl: Argon2Fn): void {
242 | argon2impl = impl;
243 | }
244 |
--------------------------------------------------------------------------------
/eslint.config.cjs:
--------------------------------------------------------------------------------
1 | /*
2 | Eslint 9 Flat Config
3 |
4 | old eslint < 8 .rc files are no longer supported! do not place .eslintrc files in subfolders.
5 | eslint developers are currently working on an experimental feature to allow for sub-folder
6 | override rules
7 | @ref https://github.com/eslint/eslint/discussions/18574#discussioncomment-9729092
8 | https://eslint.org/docs/latest/use/configure/configuration-files#experimental-configuration-file-resolution
9 |
10 | eslint config migration docs
11 | @ref https://eslint.org/docs/latest/use/configure/migration-guide
12 | */
13 |
14 | const js = require('@eslint/js');
15 | const globals = require('globals');
16 |
17 | /*
18 | Parser
19 | */
20 |
21 | const parserTS = require('@typescript-eslint/parser');
22 |
23 | /*
24 | Plugins
25 | */
26 |
27 | const pluginChaiFriendly = require('eslint-plugin-chai-friendly');
28 | const pluginImport = require('eslint-plugin-import');
29 | const pluginNode = require('eslint-plugin-n');
30 | const pluginPrettier = require('eslint-plugin-prettier');
31 | const pluginPromise = require('eslint-plugin-promise');
32 | const mochaPlugin = require('eslint-plugin-mocha');
33 | const tsPlugin = require('@typescript-eslint/eslint-plugin');
34 |
35 | /*
36 | Globals
37 | */
38 |
39 | const customGlobals = {
40 | guid: 'readable',
41 | uuid: 'readable'
42 | };
43 |
44 | /*
45 | Compatibility
46 | */
47 |
48 | const { FlatCompat } = require('@eslint/eslintrc');
49 |
50 | const compat = new FlatCompat({
51 | baseDirectory: __dirname,
52 | recommendedConfig: js.configs.recommended,
53 | allConfig: js.configs.all
54 | });
55 |
56 | /*
57 | Eslint > Flat Config
58 | */
59 |
60 | module.exports = [{
61 | ignores: [
62 | '**/argon2-asm.min.js',
63 | '**/test-support',
64 | '**/eslint.config.cjs'
65 | ],
66 | }, ...compat.extends('eslint:recommended', 'plugin:prettier/recommended', 'plugin:chai-friendly/recommended'), {
67 | files: ['**/*.{ts,tsx}'],
68 | plugins: {
69 | 'chai-friendly': pluginChaiFriendly,
70 | 'import': pluginImport,
71 | 'mocha': mochaPlugin,
72 | 'n': pluginNode,
73 | 'prettier': pluginPrettier,
74 | 'promise': pluginPromise,
75 | '@typescript-eslint': tsPlugin
76 | },
77 |
78 | linterOptions: {
79 | reportUnusedDisableDirectives: false
80 | },
81 |
82 | languageOptions: {
83 | parser: parserTS,
84 | ecmaVersion: 13,
85 | parserOptions: {
86 | project: [
87 | 'tsconfig.json',
88 | 'jsconfig.json',
89 | ],
90 | tsconfigRootDir: __dirname,
91 | },
92 | globals: {
93 | ...customGlobals,
94 | ...globals.browser,
95 | ...globals.node,
96 | ...globals.jest,
97 | ...globals.jquery,
98 | ...globals.mocha,
99 | _: true,
100 | $: true
101 | },
102 | sourceType: 'module',
103 | },
104 | rules: {
105 |
106 | /*
107 | Turn off original and add back typescript version
108 | */
109 |
110 | "no-unused-vars": 'off',
111 | "@typescript-eslint/no-unused-vars": ["error"],
112 |
113 | "no-redeclare": 'off',
114 | "@typescript-eslint/no-redeclare": "error",
115 |
116 | 'array-callback-return': 'error',
117 | 'curly': 'error',
118 | 'eqeqeq': 'error',
119 | 'no-alert': 'error',
120 | 'no-array-constructor': 'error',
121 | 'no-console': 'off',
122 | 'no-debugger': 'error',
123 | 'no-dupe-class-members': 'error',
124 | 'no-duplicate-imports': 'error',
125 | 'no-empty': 'off',
126 | 'no-eval': 'error',
127 | 'no-mixed-operators': 'off',
128 | 'no-new-func': 'error',
129 | 'no-new-object': 'error',
130 | 'no-throw-literal': 'off',
131 | 'no-unneeded-ternary': 'error',
132 | 'no-unused-expressions': 'off',
133 | 'no-useless-constructor': 'error',
134 | 'no-useless-escape': 'off',
135 | 'no-var': 'error',
136 | 'object-curly-spacing': 'off',
137 | 'object-property-newline': 'off',
138 | 'object-shorthand': 'error',
139 | 'one-var': 'off',
140 | 'prefer-arrow-callback': 'error',
141 | 'prefer-const': 'error',
142 | 'prefer-promise-reject-errors': 'off',
143 | 'prefer-rest-params': 'error',
144 | 'prefer-spread': 'error',
145 | 'quote-props': 'off',
146 | 'semi': ['error', 'always'],
147 | 'space-before-function-paren': 'off',
148 | 'strict': ['error', 'never'],
149 | 'camelcase': [
150 | 'error',
151 | {
152 | 'properties': 'always'
153 | }
154 | ],
155 | 'no-restricted-syntax': [
156 | 'error',
157 | {
158 | 'selector': 'ExportDefaultDeclaration',
159 | 'message': 'Prefer named exports'
160 | }
161 | ],
162 |
163 | /*
164 | @plugin eslint-plugin-chai-friendly
165 | */
166 |
167 | 'chai-friendly/no-unused-expressions': 2,
168 |
169 | /*
170 | @plugin eslint-plugin-import
171 | */
172 |
173 | 'import/no-webpack-loader-syntax': 'off',
174 | 'import/no-relative-parent-imports': 'off',
175 | 'import/first': 'error',
176 | 'import/no-default-export': 'error',
177 |
178 | /*
179 | @plugin eslint-plugin-n
180 | @url https://github.com/eslint-community/eslint-plugin-n
181 | */
182 |
183 | 'n/no-callback-literal': 0,
184 | 'n/no-deprecated-api': 'error',
185 | 'n/no-exports-assign': 'error',
186 | 'n/no-extraneous-import': 'error',
187 | 'n/no-extraneous-require': [
188 | 'error',
189 | {
190 | 'allowModules': ['electron', 'electron-notarize'],
191 | 'resolvePaths': [],
192 | 'tryExtensions': []
193 | }
194 | ],
195 | 'n/no-hide-core-modules': 'off',
196 | 'n/no-missing-import': 'off',
197 | 'n/no-missing-require': 'off',
198 | 'n/no-mixed-requires': 'error',
199 | 'n/no-new-require': 'error',
200 | 'n/no-path-concat': 'error',
201 | 'n/no-process-env': 'off',
202 | 'n/no-process-exit': 'off',
203 | 'n/no-restricted-import': 'error',
204 | 'n/no-restricted-require': 'error',
205 | 'n/no-sync': 'off',
206 | 'n/no-unpublished-bin': 'error',
207 | 'n/no-unpublished-import': 'error',
208 | 'n/no-unpublished-require': 'error',
209 | 'n/no-unsupported-features/es-builtins': 'error',
210 | 'n/no-unsupported-features/es-syntax': 'error',
211 | 'n/no-unsupported-features/node-builtins': 'off',
212 | 'n/prefer-global/buffer': 'error',
213 | 'n/prefer-global/console': 'error',
214 | 'n/prefer-global/process': 'error',
215 | 'n/prefer-global/text-decoder': 'error',
216 | 'n/prefer-global/text-encoder': 'error',
217 | 'n/prefer-global/url': 'error',
218 | 'n/prefer-global/url-search-params': 'error',
219 | 'n/prefer-node-protocol': 'off',
220 | 'n/prefer-promises/dns': 'off',
221 | 'n/prefer-promises/fs': 'off',
222 | 'n/process-exit-as-throw': 'error',
223 |
224 | /*
225 | @plugin eslint-plugin-prettier
226 |
227 | prettier parser options:
228 | - https://prettier.io/docs/en/options.html
229 | */
230 |
231 | 'prettier/prettier': [
232 | 'error',
233 | {
234 | experimentalTernaries: false,
235 | printWidth: 100,
236 | tabWidth: 4,
237 | useTabs: false,
238 | semi: true,
239 | singleQuote: true,
240 | quoteProps: 'preserve',
241 | jsxSingleQuote: true,
242 | trailingComma: 'none',
243 | bracketSpacing: true,
244 | bracketSameLine: false,
245 | arrowParens: 'always',
246 | proseWrap: 'preserve',
247 | htmlWhitespaceSensitivity: 'ignore',
248 | endOfLine: 'auto',
249 | parser: 'typescript',
250 | embeddedLanguageFormatting: 'auto',
251 | singleAttributePerLine: true
252 | }
253 | ]
254 | }
255 | },
256 | {
257 | files: ['test/**/*.ts'],
258 | languageOptions: {
259 | ecmaVersion: 11,
260 | parserOptions: {
261 | project: [
262 | 'tsconfig.json'
263 | ]
264 | },
265 | },
266 | rules: {
267 | '@typescript-eslint/no-unsafe-call': 'off',
268 | '@typescript-eslint/no-unsafe-member-access': 'off',
269 | '@typescript-eslint/ban-ts-comment': 'off',
270 | '@typescript-eslint/no-non-null-assertion': 'off',
271 | '@typescript-eslint/no-unsafe-assignment': 'off',
272 | '@typescript-eslint/no-unsafe-return': 'off',
273 | '@typescript-eslint/no-var-requires': 'off',
274 | '@typescript-eslint/no-explicit-any': 'off'
275 | }
276 | }
277 | ];
278 |
--------------------------------------------------------------------------------
/test/format/kdbx-credentials.spec.ts:
--------------------------------------------------------------------------------
1 | import expect from 'expect.js';
2 | import { ByteUtils, Consts, KdbxCredentials, KdbxError, ProtectedValue } from '../../lib';
3 |
4 | describe('KdbxCredentials', () => {
5 | it('calculates hash for null password', async () => {
6 | const cred = new KdbxCredentials(null);
7 | const hash = await cred.getHash();
8 | expect(ByteUtils.bytesToHex(hash)).to.be(
9 | 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
10 | );
11 | });
12 |
13 | it('calculates hash for empty password', async () => {
14 | const cred = new KdbxCredentials(ProtectedValue.fromString(''));
15 | const hash = await cred.getHash();
16 | expect(ByteUtils.bytesToHex(hash)).to.be(
17 | '5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456'
18 | );
19 | });
20 |
21 | it('calculates hash for test password', async () => {
22 | const cred = new KdbxCredentials(ProtectedValue.fromString('test'));
23 | const hash = await cred.getHash();
24 | expect(ByteUtils.bytesToHex(hash)).to.be(
25 | '954d5a49fd70d9b8bcdb35d252267829957f7ef7fa6c74f88419bdc5e82209f4'
26 | );
27 | });
28 |
29 | it('calculates hash for null password and a key file', async () => {
30 | const cred = new KdbxCredentials(null, new Uint8Array(32).fill(1));
31 | const hash = await cred.getHash();
32 | expect(ByteUtils.bytesToHex(hash)).to.be(
33 | '72cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793'
34 | );
35 | });
36 |
37 | it('calculates hash for test password and a key file', async () => {
38 | const cred = new KdbxCredentials(
39 | ProtectedValue.fromString('test'),
40 | new Uint8Array(32).fill(1)
41 | );
42 | const hash = await cred.getHash();
43 | expect(ByteUtils.bytesToHex(hash)).to.be(
44 | 'e37a11dc890fae6114bbc310a22a5b9bef0d253d4843679b4d76501bb849600e'
45 | );
46 | });
47 |
48 | it('calculates hash with challenge-response', async () => {
49 | const cred = new KdbxCredentials(
50 | ProtectedValue.fromString('test'),
51 | new Uint8Array(32).fill(1),
52 | (challenge) => Promise.resolve(challenge)
53 | );
54 | const hash = await cred.getHash(new Uint8Array(32).fill(2).buffer);
55 | expect(ByteUtils.bytesToHex(hash)).to.be(
56 | '8cdc398b5e3906296d8b69f9a88162fa65b46bca0f9ac4024b083411d4a76324'
57 | );
58 | });
59 |
60 | it('accepts an array in challenge-response', async () => {
61 | const cred = new KdbxCredentials(
62 | ProtectedValue.fromString('test'),
63 | new Uint8Array(32).fill(1),
64 | (challenge) => Promise.resolve(new Uint8Array(challenge))
65 | );
66 | const hash = await cred.getHash(new Uint8Array(32).fill(2).buffer);
67 | expect(ByteUtils.bytesToHex(hash)).to.be(
68 | '8cdc398b5e3906296d8b69f9a88162fa65b46bca0f9ac4024b083411d4a76324'
69 | );
70 | });
71 |
72 | it('calculates hash for a bad xml key file', async () => {
73 | const keyFile = new TextEncoder().encode('boo');
74 | const cred = new KdbxCredentials(null, keyFile);
75 | const hash = await cred.getHash();
76 | expect(ByteUtils.bytesToHex(hash)).to.be(
77 | '3ab83b7980ccad2dca61dd5f60d306c71d80f2d9856a72e2743d17cbb1c3cbf6'
78 | );
79 | });
80 |
81 | it('calculates hash for a plaintext key file', async () => {
82 | const keyFile = new Uint8Array(32).fill(1).buffer;
83 | const cred = new KdbxCredentials(null, keyFile);
84 | const hash = await cred.getHash();
85 | expect(ByteUtils.bytesToHex(hash)).to.be(
86 | '72cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793'
87 | );
88 | });
89 |
90 | it('calculates hash for a hex key file', async () => {
91 | const keyFile = new TextEncoder().encode(
92 | 'DEADbeef0a0f0212812374283418418237418734873829748917389472314243'
93 | );
94 | const cred = new KdbxCredentials(null, keyFile);
95 | const hash = await cred.getHash();
96 | expect(ByteUtils.bytesToHex(hash)).to.be(
97 | 'cf18a98ff868a7978dddc09861f792e6fe6d13503f4364ae2e1abeef2ba5bfc9'
98 | );
99 | });
100 |
101 | it('throws an error for a key file without meta', async () => {
102 | const keyFile = new TextEncoder().encode('');
103 | const cred = new KdbxCredentials(null, keyFile);
104 | try {
105 | await cred.getHash();
106 | } catch (e) {
107 | expect(e).to.be.a(KdbxError);
108 | expect((e as KdbxError).code).to.be(Consts.ErrorCodes.InvalidArg);
109 | expect((e as KdbxError).message).to.contain('key file without meta');
110 | return;
111 | }
112 | expect().fail();
113 | });
114 |
115 | it('throws an error for a key file without version', async () => {
116 | const keyFile = new TextEncoder().encode('');
117 | const cred = new KdbxCredentials(null, keyFile);
118 | try {
119 | await cred.getHash();
120 | } catch (e) {
121 | expect(e).to.be.a(KdbxError);
122 | expect((e as KdbxError).code).to.be(Consts.ErrorCodes.InvalidArg);
123 | expect((e as KdbxError).message).to.contain('key file without version');
124 | return;
125 | }
126 | expect().fail();
127 | });
128 |
129 | it('throws an error for a key file with bad version', async () => {
130 | const keyFile = new TextEncoder().encode(
131 | '10.000'
132 | );
133 | const cred = new KdbxCredentials(null, keyFile);
134 | try {
135 | await cred.getHash();
136 | } catch (e) {
137 | expect(e).to.be.a(KdbxError);
138 | expect((e as KdbxError).code).to.be(Consts.ErrorCodes.FileCorrupt);
139 | expect((e as KdbxError).message).to.contain('bad keyfile version');
140 | return;
141 | }
142 | expect().fail();
143 | });
144 |
145 | it('throws an error for a key file without key', async () => {
146 | const keyFile = new TextEncoder().encode(
147 | '1.0'
148 | );
149 | const cred = new KdbxCredentials(null, keyFile);
150 | try {
151 | await cred.getHash();
152 | } catch (e) {
153 | expect(e).to.be.a(KdbxError);
154 | expect((e as KdbxError).code).to.be(Consts.ErrorCodes.InvalidArg);
155 | expect((e as KdbxError).message).to.contain('key file without key');
156 | return;
157 | }
158 | expect().fail();
159 | });
160 |
161 | it('throws an error for a key file without data', async () => {
162 | const keyFile = new TextEncoder().encode(
163 | '1.0'
164 | );
165 | const cred = new KdbxCredentials(null, keyFile);
166 | try {
167 | await cred.getHash();
168 | } catch (e) {
169 | expect(e).to.be.a(KdbxError);
170 | expect((e as KdbxError).code).to.be(Consts.ErrorCodes.InvalidArg);
171 | expect((e as KdbxError).message).to.contain('key file without key data');
172 | return;
173 | }
174 | expect().fail();
175 | });
176 |
177 | it('calculates hash for a v1 key file', async () => {
178 | const keyFile = new TextEncoder().encode(
179 | '1.0AtY2GR2pVt6aWz2ugfxfSQWjRId9l0JWe/LEMJWVJ1k='
180 | );
181 | const cred = new KdbxCredentials(null, keyFile);
182 | const hash = await cred.getHash();
183 | expect(ByteUtils.bytesToHex(hash)).to.be(
184 | '829bd09b8d05fafaa0e80b7307a978c496931815feb0a5cf82ce872ee36fa355'
185 | );
186 | });
187 |
188 | it('calculates hash for a v2 key file', async () => {
189 | const keyFile = new TextEncoder().encode(
190 | '2.0A7007945 D07D54BA 28DF6434 1B4500FC 9750DFB1 D36ADA2D 9C32DC19 4C7AB01B'
191 | );
192 | const cred = new KdbxCredentials(null, keyFile);
193 | const hash = await cred.getHash();
194 | expect(ByteUtils.bytesToHex(hash)).to.be(
195 | 'fe2949b83209abdbd99f049b6a0231282b5854214b0b58f5135148f905ad5a95'
196 | );
197 | });
198 |
199 | it('throws an error for a v2 key file with bad hash', async () => {
200 | const keyFile = new TextEncoder().encode(
201 | '2.0A7007945 D07D54BA 28DF6434 1B4500FC 9750DFB1 D36ADA2D 9C32DC19 4C7AB01B'
202 | );
203 | const cred = new KdbxCredentials(null, keyFile);
204 | try {
205 | await cred.getHash();
206 | } catch (e) {
207 | expect(e).to.be.a(KdbxError);
208 | expect((e as KdbxError).code).to.be(Consts.ErrorCodes.FileCorrupt);
209 | expect((e as KdbxError).message).to.contain('key file data hash mismatch');
210 | return;
211 | }
212 | expect().fail();
213 | });
214 |
215 | it('sets passwordHash and keyFileHash', async () => {
216 | const keyFile = new TextEncoder().encode(
217 | '2.0A7007945 D07D54BA 28DF6434 1B4500FC 9750DFB1 D36ADA2D 9C32DC19 4C7AB01B'
218 | );
219 | const cred = new KdbxCredentials(ProtectedValue.fromString('123'), keyFile);
220 | const hash = await cred.getHash();
221 | expect(ByteUtils.bytesToHex(hash)).to.be(
222 | '4ecd13e7ea764ce2909e460864f4d4a513b07f612a1adb013770a40bb1cf77fc'
223 | );
224 | expect(cred.passwordHash).to.be.ok();
225 | expect(cred.keyFileHash).to.be.ok();
226 | expect(ByteUtils.bytesToHex(cred.passwordHash!.getBinary())).to.be(
227 | 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
228 | );
229 | expect(ByteUtils.bytesToHex(cred.keyFileHash!.getBinary())).to.be(
230 | 'a7007945d07d54ba28df64341b4500fc9750dfb1d36ada2d9c32dc194c7ab01b'
231 | );
232 | });
233 | });
234 |
--------------------------------------------------------------------------------
/lib/format/kdbx-credentials.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import * as XmlUtils from '../utils/xml-utils';
3 | import * as CryptoEngine from '../crypto/crypto-engine';
4 | import { ProtectedValue } from '../crypto/protected-value';
5 | import { KdbxError } from '../errors/kdbx-error';
6 | import { ErrorCodes } from '../defs/consts';
7 | import {
8 | arrayToBuffer,
9 | base64ToBytes,
10 | bytesToBase64,
11 | bytesToHex,
12 | bytesToString,
13 | hexToBytes,
14 | stringToBytes,
15 | zeroBuffer
16 | } from '../utils/byte-utils';
17 |
18 | export type KdbxChallengeResponseFn = (challenge: ArrayBuffer) => Promise;
19 |
20 | export class KdbxCredentials {
21 | readonly ready: Promise;
22 | passwordHash: ProtectedValue | undefined;
23 | keyFileHash: ProtectedValue | undefined;
24 | private _challengeResponse: KdbxChallengeResponseFn | undefined;
25 |
26 | constructor(
27 | password: ProtectedValue | null,
28 | keyFile?: ArrayBuffer | Uint8Array | null,
29 | challengeResponse?: KdbxChallengeResponseFn
30 | ) {
31 | this.ready = Promise.all([
32 | this.setPassword(password),
33 | this.setKeyFile(keyFile),
34 | this.setChallengeResponse(challengeResponse)
35 | ]).then(() => this);
36 | }
37 |
38 | setPassword(password: ProtectedValue | null): Promise {
39 | if (!password) {
40 | this.passwordHash = undefined;
41 | return Promise.resolve();
42 | } else if (password instanceof ProtectedValue) {
43 | return password.getHash().then((hash) => {
44 | this.passwordHash = ProtectedValue.fromBinary(hash);
45 | });
46 | } else {
47 | return Promise.reject(new KdbxError(ErrorCodes.InvalidArg, 'password'));
48 | }
49 | }
50 |
51 | setKeyFile(keyFile: ArrayBuffer | Uint8Array | null | undefined): Promise {
52 | if (keyFile && !(keyFile instanceof ArrayBuffer) && !(keyFile instanceof Uint8Array)) {
53 | return Promise.reject(new KdbxError(ErrorCodes.InvalidArg, 'keyFile'));
54 | }
55 | if (keyFile) {
56 | if (keyFile.byteLength === 32) {
57 | this.keyFileHash = ProtectedValue.fromBinary(arrayToBuffer(keyFile));
58 | return Promise.resolve();
59 | }
60 | let keyFileVersion;
61 | let dataEl;
62 | try {
63 | const keyFileStr = bytesToString(arrayToBuffer(keyFile));
64 | if (/^[a-f\d]{64}$/i.exec(keyFileStr)) {
65 | const bytes = hexToBytes(keyFileStr);
66 | this.keyFileHash = ProtectedValue.fromBinary(bytes);
67 | return Promise.resolve();
68 | }
69 | const xml = XmlUtils.parse(keyFileStr.trim());
70 | const metaEl = XmlUtils.getChildNode(xml.documentElement, 'Meta');
71 | if (!metaEl) {
72 | return Promise.reject(
73 | new KdbxError(ErrorCodes.InvalidArg, 'key file without meta')
74 | );
75 | }
76 |
77 | const versionEl = XmlUtils.getChildNode(metaEl, 'Version');
78 | if (!versionEl?.textContent) {
79 | return Promise.reject(
80 | new KdbxError(ErrorCodes.InvalidArg, 'key file without version')
81 | );
82 | }
83 | keyFileVersion = +versionEl.textContent.split('.')[0];
84 |
85 | const keyEl = XmlUtils.getChildNode(xml.documentElement, 'Key');
86 | if (!keyEl) {
87 | return Promise.reject(
88 | new KdbxError(ErrorCodes.InvalidArg, 'key file without key')
89 | );
90 | }
91 |
92 | dataEl = XmlUtils.getChildNode(keyEl, 'Data');
93 | if (!dataEl?.textContent) {
94 | return Promise.reject(
95 | new KdbxError(ErrorCodes.InvalidArg, 'key file without key data')
96 | );
97 | }
98 | } catch (e) {
99 | return CryptoEngine.sha256(keyFile).then((hash) => {
100 | this.keyFileHash = ProtectedValue.fromBinary(hash);
101 | });
102 | }
103 |
104 | switch (keyFileVersion) {
105 | case 1:
106 | this.keyFileHash = ProtectedValue.fromBinary(base64ToBytes(dataEl.textContent));
107 | break;
108 | case 2: {
109 | const keyFileData = hexToBytes(dataEl.textContent.replace(/\s+/g, ''));
110 | const keyFileDataHash = dataEl.getAttribute('Hash');
111 | return CryptoEngine.sha256(keyFileData).then((computedHash) => {
112 | const computedHashStr = bytesToHex(
113 | new Uint8Array(computedHash).subarray(0, 4)
114 | ).toUpperCase();
115 | if (computedHashStr !== keyFileDataHash) {
116 | throw new KdbxError(
117 | ErrorCodes.FileCorrupt,
118 | 'key file data hash mismatch'
119 | );
120 | }
121 | this.keyFileHash = ProtectedValue.fromBinary(keyFileData);
122 | });
123 | }
124 | default: {
125 | return Promise.reject(
126 | new KdbxError(ErrorCodes.FileCorrupt, 'bad keyfile version')
127 | );
128 | }
129 | }
130 | } else {
131 | this.keyFileHash = undefined;
132 | }
133 | return Promise.resolve();
134 | }
135 |
136 | private setChallengeResponse(
137 | challengeResponse: KdbxChallengeResponseFn | undefined
138 | ): Promise {
139 | this._challengeResponse = challengeResponse;
140 | return Promise.resolve();
141 | }
142 |
143 | getHash(challenge?: ArrayBuffer): Promise {
144 | return this.ready.then(() => {
145 | return this.getChallengeResponse(challenge).then((chalResp) => {
146 | const buffers: Uint8Array[] = [];
147 | if (this.passwordHash) {
148 | buffers.push(this.passwordHash.getBinary());
149 | }
150 | if (this.keyFileHash) {
151 | buffers.push(this.keyFileHash.getBinary());
152 | }
153 | if (chalResp) {
154 | buffers.push(new Uint8Array(chalResp));
155 | }
156 | const totalLength = buffers.reduce((acc, buf) => acc + buf.byteLength, 0);
157 | const allBytes = new Uint8Array(totalLength);
158 | let offset = 0;
159 | for (const buffer of buffers) {
160 | allBytes.set(buffer, offset);
161 | zeroBuffer(buffer);
162 | offset += buffer.length;
163 | }
164 | return CryptoEngine.sha256(arrayToBuffer(allBytes)).then((hash) => {
165 | zeroBuffer(allBytes);
166 | return hash;
167 | });
168 | });
169 | });
170 | }
171 |
172 | getChallengeResponse(challenge?: ArrayBuffer): Promise {
173 | return Promise.resolve().then(() => {
174 | if (!this._challengeResponse || !challenge) {
175 | return null;
176 | }
177 | return this._challengeResponse(challenge).then((response) => {
178 | return CryptoEngine.sha256(arrayToBuffer(response)).then((hash) => {
179 | zeroBuffer(response);
180 | return hash;
181 | });
182 | });
183 | });
184 | }
185 |
186 | static createRandomKeyFile(version = 1): Promise {
187 | const keyLength = 32;
188 | const keyBytes = CryptoEngine.random(keyLength),
189 | salt = CryptoEngine.random(keyLength);
190 | for (let i = 0; i < keyLength; i++) {
191 | keyBytes[i] ^= salt[i];
192 | keyBytes[i] ^= (Math.random() * 1000) % 255;
193 | }
194 | return KdbxCredentials.createKeyFileWithHash(keyBytes, version);
195 | }
196 |
197 | static createKeyFileWithHash(keyBytes: ArrayBuffer, version = 1): Promise {
198 | const xmlVersion = version === 2 ? '2.0' : '1.00';
199 | const dataPadding = ' ';
200 | let makeDataElPromise;
201 | if (version === 2) {
202 | const keyDataPadding = dataPadding + ' ';
203 | makeDataElPromise = CryptoEngine.sha256(keyBytes).then((computedHash) => {
204 | const keyHash = bytesToHex(
205 | new Uint8Array(computedHash).subarray(0, 4)
206 | ).toUpperCase();
207 | const keyStr = bytesToHex(keyBytes).toUpperCase();
208 | let dataElXml = dataPadding + '\n';
209 | for (let num = 0; num < 2; num++) {
210 | const parts = [0, 1, 2, 3].map((ix) => {
211 | return keyStr.substr(num * 32 + ix * 8, 8);
212 | });
213 | dataElXml += keyDataPadding;
214 | dataElXml += parts.join(' ');
215 | dataElXml += '\n';
216 | }
217 | dataElXml += dataPadding + '\n';
218 | return dataElXml;
219 | });
220 | } else {
221 | const dataElXml = dataPadding + '' + bytesToBase64(keyBytes) + '\n';
222 | makeDataElPromise = Promise.resolve(dataElXml);
223 | }
224 | return makeDataElPromise.then((dataElXml) => {
225 | const xml =
226 | '\n' +
227 | '\n' +
228 | ' \n' +
229 | ' ' +
230 | xmlVersion +
231 | '\n' +
232 | ' \n' +
233 | ' \n' +
234 | dataElXml +
235 | ' \n' +
236 | '';
237 | return stringToBytes(xml);
238 | });
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/lib/utils/var-dictionary.ts:
--------------------------------------------------------------------------------
1 | import { KdbxError } from '../errors/kdbx-error';
2 | import { ErrorCodes } from '../defs/consts';
3 | import { arrayToBuffer, bytesToString, stringToBytes } from './byte-utils';
4 | import { Int64 } from './int64';
5 | import { BinaryStream } from './binary-stream';
6 |
7 | const MaxSupportedVersion = 1;
8 | const DefaultVersion = 0x0100;
9 |
10 | export enum ValueType {
11 | UInt32 = 0x04,
12 | UInt64 = 0x05,
13 | Bool = 0x08,
14 | Int32 = 0x0c,
15 | Int64 = 0x0d,
16 | String = 0x18,
17 | Bytes = 0x42
18 | }
19 |
20 | export type VarDictionaryAnyValue = number | Int64 | boolean | string | ArrayBuffer | undefined;
21 |
22 | interface VarDictionaryItemInt {
23 | type: ValueType.UInt32 | ValueType.Int32;
24 | key: string;
25 | value: number;
26 | }
27 |
28 | interface VarDictionaryItemInt64 {
29 | type: ValueType.UInt64 | ValueType.Int64;
30 | key: string;
31 | value: Int64;
32 | }
33 |
34 | interface VarDictionaryItemBool {
35 | type: ValueType.Bool;
36 | key: string;
37 | value: boolean;
38 | }
39 |
40 | interface VarDictionaryItemString {
41 | type: ValueType.String;
42 | key: string;
43 | value: string;
44 | }
45 |
46 | interface VarDictionaryItemBytes {
47 | type: ValueType.Bytes;
48 | key: string;
49 | value: ArrayBuffer;
50 | }
51 |
52 | type VarDictionaryItem =
53 | | VarDictionaryItemInt
54 | | VarDictionaryItemInt64
55 | | VarDictionaryItemBool
56 | | VarDictionaryItemString
57 | | VarDictionaryItemBytes;
58 |
59 | export class VarDictionary {
60 | private _items: VarDictionaryItem[] = [];
61 | private readonly _map = new Map();
62 |
63 | static readonly ValueType = ValueType;
64 |
65 | keys(): string[] {
66 | return this._items.map((item) => item.key);
67 | }
68 |
69 | get length(): number {
70 | return this._items.length;
71 | }
72 |
73 | get(key: string): VarDictionaryAnyValue {
74 | const item = this._map.get(key);
75 | return item ? item.value : undefined;
76 | }
77 |
78 | set(key: string, type: ValueType, value: VarDictionaryAnyValue): void {
79 | let item: VarDictionaryItem;
80 |
81 | switch (type) {
82 | case ValueType.UInt32:
83 | if (typeof value !== 'number' || value < 0) {
84 | throw new KdbxError(ErrorCodes.InvalidArg);
85 | }
86 | item = { key, type, value };
87 | break;
88 | case ValueType.UInt64:
89 | if (!(value instanceof Int64)) {
90 | throw new KdbxError(ErrorCodes.InvalidArg);
91 | }
92 | item = { key, type, value };
93 | break;
94 | case ValueType.Bool:
95 | if (typeof value !== 'boolean') {
96 | throw new KdbxError(ErrorCodes.InvalidArg);
97 | }
98 | item = { key, type, value };
99 | break;
100 | case ValueType.Int32:
101 | if (typeof value !== 'number') {
102 | throw new KdbxError(ErrorCodes.InvalidArg);
103 | }
104 | item = { key, type, value };
105 | break;
106 | case ValueType.Int64:
107 | if (!(value instanceof Int64)) {
108 | throw new KdbxError(ErrorCodes.InvalidArg);
109 | }
110 | item = { key, type, value };
111 | break;
112 | case ValueType.String:
113 | if (typeof value !== 'string') {
114 | throw new KdbxError(ErrorCodes.InvalidArg);
115 | }
116 | item = { key, type, value };
117 | break;
118 | case ValueType.Bytes:
119 | if (value instanceof Uint8Array) {
120 | value = arrayToBuffer(value);
121 | }
122 | if (!(value instanceof ArrayBuffer)) {
123 | throw new KdbxError(ErrorCodes.InvalidArg);
124 | }
125 | item = { key, type, value };
126 | break;
127 | default:
128 | throw new KdbxError(ErrorCodes.InvalidArg);
129 | }
130 |
131 | const existing = this._map.get(key);
132 | if (existing) {
133 | const ix = this._items.indexOf(existing);
134 | this._items.splice(ix, 1, item);
135 | } else {
136 | this._items.push(item);
137 | }
138 |
139 | this._map.set(key, item);
140 | }
141 |
142 | remove(key: string): void {
143 | this._items = this._items.filter((item) => {
144 | return item.key !== key;
145 | });
146 | this._map.delete(key);
147 | }
148 |
149 | static read(stm: BinaryStream): VarDictionary {
150 | const dict = new VarDictionary();
151 | dict.readVersion(stm);
152 | for (let item; (item = dict.readItem(stm)); ) {
153 | dict._items.push(item);
154 | dict._map.set(item.key, item);
155 | }
156 | return dict;
157 | }
158 |
159 | private readVersion(stm: BinaryStream): void {
160 | stm.getUint8();
161 | const versionMajor = stm.getUint8();
162 | if (versionMajor === 0 || versionMajor > MaxSupportedVersion) {
163 | throw new KdbxError(ErrorCodes.InvalidVersion);
164 | }
165 | }
166 |
167 | private readItem(stm: BinaryStream): VarDictionaryItem | undefined {
168 | const type = stm.getUint8();
169 | if (!type) {
170 | return undefined;
171 | }
172 | const keyLength = stm.getInt32(true);
173 | if (keyLength <= 0) {
174 | throw new KdbxError(ErrorCodes.FileCorrupt, 'bad key length');
175 | }
176 | const key = bytesToString(stm.readBytes(keyLength));
177 | const valueLength = stm.getInt32(true);
178 | if (valueLength < 0) {
179 | throw new KdbxError(ErrorCodes.FileCorrupt, 'bad value length');
180 | }
181 | switch (type) {
182 | case ValueType.UInt32: {
183 | if (valueLength !== 4) {
184 | throw new KdbxError(ErrorCodes.FileCorrupt, 'bad uint32');
185 | }
186 | const value = stm.getUint32(true);
187 | return { key, type, value };
188 | }
189 | case ValueType.UInt64: {
190 | if (valueLength !== 8) {
191 | throw new KdbxError(ErrorCodes.FileCorrupt, 'bad uint64');
192 | }
193 | const loInt = stm.getUint32(true);
194 | const hiInt = stm.getUint32(true);
195 | const value = new Int64(loInt, hiInt);
196 | return { key, type, value };
197 | }
198 | case ValueType.Bool: {
199 | if (valueLength !== 1) {
200 | throw new KdbxError(ErrorCodes.FileCorrupt, 'bad bool');
201 | }
202 | const value = stm.getUint8() !== 0;
203 | return { key, type, value };
204 | }
205 | case ValueType.Int32: {
206 | if (valueLength !== 4) {
207 | throw new KdbxError(ErrorCodes.FileCorrupt, 'bad int32');
208 | }
209 | const value = stm.getInt32(true);
210 | return { key, type, value };
211 | }
212 | case ValueType.Int64: {
213 | if (valueLength !== 8) {
214 | throw new KdbxError(ErrorCodes.FileCorrupt, 'bad int64');
215 | }
216 | const loUint = stm.getUint32(true);
217 | const hiUint = stm.getUint32(true);
218 | const value = new Int64(loUint, hiUint);
219 | return { key, type, value };
220 | }
221 | case ValueType.String: {
222 | const value = bytesToString(stm.readBytes(valueLength));
223 | return { key, type, value };
224 | }
225 | case ValueType.Bytes: {
226 | const value = stm.readBytes(valueLength);
227 | return { key, type, value };
228 | }
229 | default:
230 | throw new KdbxError(ErrorCodes.FileCorrupt, `bad value type: ${type}`);
231 | }
232 | }
233 |
234 | write(stm: BinaryStream): void {
235 | this.writeVersion(stm);
236 | for (const item of this._items) {
237 | this.writeItem(stm, item);
238 | }
239 | stm.setUint8(0);
240 | }
241 |
242 | private writeVersion(stm: BinaryStream): void {
243 | stm.setUint16(DefaultVersion, true);
244 | }
245 |
246 | private writeItem(stm: BinaryStream, item: VarDictionaryItem) {
247 | stm.setUint8(item.type);
248 |
249 | const keyBytes = stringToBytes(item.key);
250 | stm.setInt32(keyBytes.length, true);
251 | stm.writeBytes(keyBytes);
252 |
253 | switch (item.type) {
254 | case ValueType.UInt32:
255 | stm.setInt32(4, true);
256 | stm.setUint32(item.value, true);
257 | break;
258 | case ValueType.UInt64:
259 | stm.setInt32(8, true);
260 | stm.setUint32(item.value.lo, true);
261 | stm.setUint32(item.value.hi, true);
262 | break;
263 | case ValueType.Bool:
264 | stm.setInt32(1, true);
265 | stm.setUint8(item.value ? 1 : 0);
266 | break;
267 | case ValueType.Int32:
268 | stm.setInt32(4, true);
269 | stm.setInt32(item.value, true);
270 | break;
271 | case ValueType.Int64:
272 | stm.setInt32(8, true);
273 | stm.setUint32(item.value.lo, true);
274 | stm.setUint32(item.value.hi, true);
275 | break;
276 | case ValueType.String: {
277 | const strBytes = stringToBytes(item.value);
278 | stm.setInt32(strBytes.length, true);
279 | stm.writeBytes(strBytes);
280 | break;
281 | }
282 | case ValueType.Bytes: {
283 | const bytesBuffer = arrayToBuffer(item.value);
284 | stm.setInt32(bytesBuffer.byteLength, true);
285 | stm.writeBytes(bytesBuffer);
286 | break;
287 | }
288 | default:
289 | throw new KdbxError(ErrorCodes.Unsupported);
290 | }
291 | }
292 | }
293 |
--------------------------------------------------------------------------------