├── .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 | ![](KDBX-HexFiend.png) 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 | --------------------------------------------------------------------------------