This commit is contained in:
proteriax
2021-07-18 16:12:04 -04:00
parent 99fa963fc0
commit 98cc916432
24 changed files with 726 additions and 269 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ lib
docs docs
ref ref
*.opvault *.opvault
freddy

12
.vscode/launch.json vendored
View File

@ -5,11 +5,15 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "pwa-node",
"request": "launch",
"name": "REPL", "name": "REPL",
"skipFiles": ["<node_internals>/**"], "type": "node",
"program": "repl.ts" "request": "launch",
"runtimeExecutable": "node",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
"args": ["repl.ts"],
"cwd": "${workspaceRoot}",
"internalConsoleOptions": "openOnSessionStart",
"skipFiles": ["<node_internals>/**"]
}, },
{ {
"type": "node", "type": "node",

View File

@ -5,19 +5,22 @@
"repository": "https://git.aet.ac/aet/opvault.js.git", "repository": "https://git.aet.ac/aet/opvault.js.git",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "rollup -c; prettier --write lib >/dev/null", "build": "rollup -c; cp src/adapters/index.d.ts lib/adapters/; prettier --write lib >/dev/null",
"build:docs": "typedoc --out docs src/index.ts --excludePrivate", "build:docs": "typedoc --out docs src/index.ts --excludePrivate",
"test": "mocha test/**/*.test.ts", "test": "node --expose-gc node_modules/mocha/bin/_mocha test/**/*.test.ts",
"repl": "node -r ts-node/register/transpile-only src/repl.ts" "repl": "node -r ts-node/register/transpile-only src/repl.ts"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-replace": "^3.0.0",
"@types/chai": "^4.2.19", "@types/chai": "^4.2.19",
"@types/chai-as-promised": "^7.1.4", "@types/chai-as-promised": "^7.1.4",
"@types/fs-extra": "^9.0.11", "@types/fs-extra": "^9.0.11",
"@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768", "@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768",
"@types/node": "^16.0.0", "@types/node": "^16.0.0",
"@types/prompts": "^2.0.13", "@types/prompts": "^2.0.13",
"@types/sinon": "^10.0.2",
"@types/sinon-chai": "^3.2.5",
"@typescript-eslint/eslint-plugin": "4.28.2", "@typescript-eslint/eslint-plugin": "4.28.2",
"@typescript-eslint/parser": "4.28.2", "@typescript-eslint/parser": "4.28.2",
"chai": "^4.3.4", "chai": "^4.3.4",
@ -37,6 +40,8 @@
"prompts": "^2.4.1", "prompts": "^2.4.1",
"rollup": "^2.52.7", "rollup": "^2.52.7",
"rollup-plugin-ts": "^1.4.0", "rollup-plugin-ts": "^1.4.0",
"sinon": "^11.1.1",
"sinon-chai": "^3.7.0",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"tsconfig-paths": "^3.9.0", "tsconfig-paths": "^3.9.0",
"typedoc": "^0.21.2", "typedoc": "^0.21.2",

110
pnpm-lock.yaml generated
View File

@ -2,12 +2,15 @@ lockfileVersion: 5.3
specifiers: specifiers:
'@rollup/plugin-json': ^4.1.0 '@rollup/plugin-json': ^4.1.0
'@rollup/plugin-replace': ^3.0.0
'@types/chai': ^4.2.19 '@types/chai': ^4.2.19
'@types/chai-as-promised': ^7.1.4 '@types/chai-as-promised': ^7.1.4
'@types/fs-extra': ^9.0.11 '@types/fs-extra': ^9.0.11
'@types/mocha': github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768 '@types/mocha': github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768
'@types/node': ^16.0.0 '@types/node': ^16.0.0
'@types/prompts': ^2.0.13 '@types/prompts': ^2.0.13
'@types/sinon': ^10.0.2
'@types/sinon-chai': ^3.2.5
'@typescript-eslint/eslint-plugin': 4.28.2 '@typescript-eslint/eslint-plugin': 4.28.2
'@typescript-eslint/parser': 4.28.2 '@typescript-eslint/parser': 4.28.2
chai: ^4.3.4 chai: ^4.3.4
@ -27,6 +30,8 @@ specifiers:
prompts: ^2.4.1 prompts: ^2.4.1
rollup: ^2.52.7 rollup: ^2.52.7
rollup-plugin-ts: ^1.4.0 rollup-plugin-ts: ^1.4.0
sinon: ^11.1.1
sinon-chai: ^3.7.0
tiny-invariant: 1.1.0 tiny-invariant: 1.1.0
ts-node: ^10.0.0 ts-node: ^10.0.0
tsconfig-paths: ^3.9.0 tsconfig-paths: ^3.9.0
@ -40,12 +45,15 @@ dependencies:
devDependencies: devDependencies:
'@rollup/plugin-json': 4.1.0_rollup@2.52.7 '@rollup/plugin-json': 4.1.0_rollup@2.52.7
'@rollup/plugin-replace': 3.0.0_rollup@2.52.7
'@types/chai': 4.2.19 '@types/chai': 4.2.19
'@types/chai-as-promised': 7.1.4 '@types/chai-as-promised': 7.1.4
'@types/fs-extra': 9.0.11 '@types/fs-extra': 9.0.11
'@types/mocha': github.com/whitecolor/mocha-types/da22474cf43f48a56c86f8c23a5a0ea36e295768 '@types/mocha': github.com/whitecolor/mocha-types/da22474cf43f48a56c86f8c23a5a0ea36e295768
'@types/node': 16.0.0 '@types/node': 16.0.0
'@types/prompts': 2.0.13 '@types/prompts': 2.0.13
'@types/sinon': 10.0.2
'@types/sinon-chai': 3.2.5
'@typescript-eslint/eslint-plugin': 4.28.2_5031fffb45dfb7117e61c1d8ea1ef3ff '@typescript-eslint/eslint-plugin': 4.28.2_5031fffb45dfb7117e61c1d8ea1ef3ff
'@typescript-eslint/parser': 4.28.2_eslint@7.30.0+typescript@4.3.5 '@typescript-eslint/parser': 4.28.2_eslint@7.30.0+typescript@4.3.5
chai: 4.3.4 chai: 4.3.4
@ -65,6 +73,8 @@ devDependencies:
prompts: 2.4.1 prompts: 2.4.1
rollup: 2.52.7 rollup: 2.52.7
rollup-plugin-ts: 1.4.0_rollup@2.52.7+typescript@4.3.5 rollup-plugin-ts: 1.4.0_rollup@2.52.7+typescript@4.3.5
sinon: 11.1.1
sinon-chai: 3.7.0_chai@4.3.4+sinon@11.1.1
ts-node: 10.0.0_488376d43314e2606bceb2872a37d0ef ts-node: 10.0.0_488376d43314e2606bceb2872a37d0ef
tsconfig-paths: 3.9.0 tsconfig-paths: 3.9.0
typedoc: 0.21.2_typescript@4.3.5 typedoc: 0.21.2_typescript@4.3.5
@ -1275,6 +1285,16 @@ packages:
rollup: 2.52.7 rollup: 2.52.7
dev: true dev: true
/@rollup/plugin-replace/3.0.0_rollup@2.52.7:
resolution: {integrity: sha512-3c7JCbMuYXM4PbPWT4+m/4Y6U60SgsnDT/cCyAyUKwFHg7pTSfsSQzIpETha3a3ig6OdOKzZz87D9ZXIK3qsDg==}
peerDependencies:
rollup: ^1.20.0 || ^2.0.0
dependencies:
'@rollup/pluginutils': 3.1.0_rollup@2.52.7
magic-string: 0.25.7
rollup: 2.52.7
dev: true
/@rollup/pluginutils/3.1.0_rollup@2.52.7: /@rollup/pluginutils/3.1.0_rollup@2.52.7:
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
@ -1298,6 +1318,30 @@ packages:
rollup: 2.52.7 rollup: 2.52.7
dev: true dev: true
/@sinonjs/commons/1.8.3:
resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==}
dependencies:
type-detect: 4.0.8
dev: true
/@sinonjs/fake-timers/7.1.2:
resolution: {integrity: sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==}
dependencies:
'@sinonjs/commons': 1.8.3
dev: true
/@sinonjs/samsam/6.0.2:
resolution: {integrity: sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==}
dependencies:
'@sinonjs/commons': 1.8.3
lodash.get: 4.4.2
type-detect: 4.0.8
dev: true
/@sinonjs/text-encoding/0.7.1:
resolution: {integrity: sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==}
dev: true
/@tsconfig/node10/1.0.8: /@tsconfig/node10/1.0.8:
resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==} resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==}
dev: true dev: true
@ -1357,6 +1401,10 @@ packages:
resolution: {integrity: sha512-E121rHk/4BlcEwANZOwcHl8L/Sl0zyIFXJoyggXkl7FCT/4MTf5u25f+qiphe0V5ELaFIkCptgvbf4whCJUVMA==} resolution: {integrity: sha512-E121rHk/4BlcEwANZOwcHl8L/Sl0zyIFXJoyggXkl7FCT/4MTf5u25f+qiphe0V5ELaFIkCptgvbf4whCJUVMA==}
dev: true dev: true
/@types/chai/4.2.21:
resolution: {integrity: sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==}
dev: true
/@types/estree/0.0.39: /@types/estree/0.0.39:
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
dev: true dev: true
@ -1397,6 +1445,19 @@ packages:
resolution: {integrity: sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw==} resolution: {integrity: sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw==}
dev: true dev: true
/@types/sinon-chai/3.2.5:
resolution: {integrity: sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ==}
dependencies:
'@types/chai': 4.2.21
'@types/sinon': 10.0.2
dev: true
/@types/sinon/10.0.2:
resolution: {integrity: sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw==}
dependencies:
'@sinonjs/fake-timers': 7.1.2
dev: true
/@types/ua-parser-js/0.7.36: /@types/ua-parser-js/0.7.36:
resolution: {integrity: sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==} resolution: {integrity: sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==}
dev: true dev: true
@ -2820,6 +2881,10 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/isarray/0.0.1:
resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=}
dev: true
/isbot/3.0.26: /isbot/3.0.26:
resolution: {integrity: sha512-y1IwTPP6pRGDmQUTrCz1bZ9ZPSmij3eWruBBIiCOARX5ueyLv58xuFxvUGg6uI0k9u1swnOmJR8DKYZbcDXLqQ==} resolution: {integrity: sha512-y1IwTPP6pRGDmQUTrCz1bZ9ZPSmij3eWruBBIiCOARX5ueyLv58xuFxvUGg6uI0k9u1swnOmJR8DKYZbcDXLqQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2916,6 +2981,10 @@ packages:
object.assign: 4.1.1 object.assign: 4.1.1
dev: true dev: true
/just-extend/4.2.1:
resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==}
dev: true
/kleur/3.0.3: /kleur/3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -2970,6 +3039,10 @@ packages:
resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=} resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=}
dev: true dev: true
/lodash.get/4.4.2:
resolution: {integrity: sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=}
dev: true
/lodash.isempty/4.4.0: /lodash.isempty/4.4.0:
resolution: {integrity: sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=} resolution: {integrity: sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=}
dev: true dev: true
@ -3170,6 +3243,16 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true dev: true
/nise/5.1.0:
resolution: {integrity: sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==}
dependencies:
'@sinonjs/commons': 1.8.3
'@sinonjs/fake-timers': 7.1.2
'@sinonjs/text-encoding': 0.7.1
just-extend: 4.2.1
path-to-regexp: 1.8.0
dev: true
/node-releases/1.1.73: /node-releases/1.1.73:
resolution: {integrity: sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==} resolution: {integrity: sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==}
dev: true dev: true
@ -3383,6 +3466,12 @@ packages:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
dev: true dev: true
/path-to-regexp/1.8.0:
resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==}
dependencies:
isarray: 0.0.1
dev: true
/path-type/3.0.0: /path-type/3.0.0:
resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -3717,6 +3806,27 @@ packages:
object-inspect: 1.10.3 object-inspect: 1.10.3
dev: true dev: true
/sinon-chai/3.7.0_chai@4.3.4+sinon@11.1.1:
resolution: {integrity: sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==}
peerDependencies:
chai: ^4.0.0
sinon: '>=4.0.0'
dependencies:
chai: 4.3.4
sinon: 11.1.1
dev: true
/sinon/11.1.1:
resolution: {integrity: sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg==}
dependencies:
'@sinonjs/commons': 1.8.3
'@sinonjs/fake-timers': 7.1.2
'@sinonjs/samsam': 6.0.2
diff: 5.0.0
nise: 5.1.0
supports-color: 7.2.0
dev: true
/sisteransi/1.0.5: /sisteransi/1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: true dev: true

14
repl.ts
View File

@ -1,11 +1,13 @@
#!/usr/bin/env ts-node-transpile-only #!/usr/bin/env node -r ts-node/register/transpile-only
import fs from "fs" import fs from "fs"
import chalk from "chalk" import chalk from "chalk"
import prompts from "prompts" import prompts from "prompts"
import { OnePassword } from "./src/index" import { OnePassword } from "./src/index"
const path = "./freddy"
async function main(args: string[]) { async function main(args: string[]) {
const instance = new OnePassword({ path: args[0] }) const instance = new OnePassword({ path })
const profiles = await instance.getProfileNames() const profiles = await instance.getProfileNames()
// const { profile } = await prompts({ // const { profile } = await prompts({
@ -28,13 +30,17 @@ async function main(args: string[]) {
vault.unlock(password) vault.unlock(password)
const find = vault.overviews.get("A note with some attachments")!
const item = vault.getItem(find.uuid!)
const d = vault.decryptAttachment( const d = vault.decryptAttachment(
item!.original,
fs.readFileSync( fs.readFileSync(
"./freddy-2013-12-04.opvault/default/1C7D72EFA19A4EE98DB7A9661D2F5732_3B94A1F475014E27BFB00C99A42214DF.attachment" "./freddy/default/F2DB5DA3FCA64372A751E0E85C67A538_AFBDA49A5F684179A78161E40CA2AAD3.attachment"
) )
) )
fs.writeFileSync("./test", d) fs.writeFileSync("./test2.png", d.file)
// console.log(vault.overviews.values()) // console.log(vault.overviews.values())
} }

View File

@ -1,15 +1,28 @@
import { builtinModules } from "module" import { builtinModules } from "module"
import ts from "rollup-plugin-ts" import ts from "rollup-plugin-ts"
import json from "@rollup/plugin-json" import json from "@rollup/plugin-json"
import replace from "@rollup/plugin-replace"
import { dependencies } from "./package.json" import { dependencies } from "./package.json"
/** @returns {import("rollup").RollupOptions} */ /** @returns {import("rollup").RollupOptions} */
export default () => ({ export default () => ({
input: "./src/index.ts", input: {
index: "./src/index.ts",
"adapters/node": "./src/adapters/node.ts",
},
external: builtinModules.concat(Object.keys(dependencies)), external: builtinModules.concat(Object.keys(dependencies)),
output: { output: {
file: "lib/index.js", dir: "lib",
format: "cjs", format: "cjs",
}, },
plugins: [ts(), json()], plugins: [
ts({ transpileOnly: true }),
json(),
replace({
preventAssignment: true,
values: {
"process.env.NODE_ENV": '"production"',
},
}),
],
}) })

0
src/adapters/browser.ts Normal file
View File

43
src/adapters/index.d.ts vendored Normal file
View File

@ -0,0 +1,43 @@
/**
* An object that implements basic file system functionalities.
*/
export interface IFileSystem {
/**
* Synchronously tests whether or not the given path exists by checking with the file system.
* @param path A path to a file or directory.
*/
existsSync(path: string): boolean
/**
* Asynchronously reads the entire contents of a file.
* @param path A path to a file.
*/
readFile(path: string): Promise<string>
/**
* Asynchronously writes data to a file, replacing the file if it already exists.
* @param path A path to a file.
* @param data The data to write.
*/
writeFile(path: string, data: string): Promise<void>
/**
* Asynchronous readdir(3) - read a directory.
* @param path A path to a directory.
*/
readdir(path: string): Promise<string[]>
/**
* Asynchronous stat(2) - Get file status.
* @param path A path to a file.
*/
stat(path: string): Promise<{ isDirectory(): boolean }>
}
export interface IAdapter {
/**
* Underlying `fs` module. You can replace it with a wrapper of
* `memfs` or any object that implements `IFileSystem`.
*/
fs: IFileSystem
}

19
src/adapters/node.ts Normal file
View File

@ -0,0 +1,19 @@
import { promises as fs, existsSync } from "fs"
import type { IAdapter } from "./index"
/**
* Default Node.js adapter. This can be used while using `opvault.js`
* in a Node.js environment.
*/
const nodeAdapter: IAdapter = {
fs: {
readFile: path => fs.readFile(path, "utf-8"),
writeFile: fs.writeFile,
readdir: fs.readdir,
stat: fs.stat,
existsSync,
},
}
export default nodeAdapter

View File

@ -1,9 +1,18 @@
import { webcrypto } from "crypto" import { webcrypto, createHmac, createDecipheriv, createHash } from "crypto"
import { HMACAssertionError } from "./errors"
declare module "crypto" { declare module "crypto" {
export const webcrypto: Crypto export const webcrypto: Crypto
} }
/** Encryption and MAC */
export interface Cipher {
/** Encryption key */
key: Buffer
/** HMAC key */
hmac: Buffer
}
async function pbkdf2(password: string, salt: string, iterations = 1000, length = 256) { async function pbkdf2(password: string, salt: string, iterations = 1000, length = 256) {
const encoder = new TextEncoder() const encoder = new TextEncoder()
const key = await webcrypto.subtle.importKey( const key = await webcrypto.subtle.importKey(
@ -25,3 +34,40 @@ async function pbkdf2(password: string, salt: string, iterations = 1000, length
) )
return bits return bits
} }
export function assertHMac(data: Buffer, key: Buffer, expected: Buffer) {
const actual = createHmac("sha256", key).update(data).digest()
if (!actual.equals(expected)) {
throw new HMACAssertionError()
}
}
export const splitPlainText = (derivedKey: Buffer): Cipher => ({
key: derivedKey.slice(0, 32),
hmac: derivedKey.slice(32, 64),
})
export function decryptKeys(encryptedKey: string, derived: Cipher) {
const buffer = Buffer.from(encryptedKey, "base64")
const base = decryptOPData(buffer, derived)
const digest = createHash("sha512").update(base).digest()
return splitPlainText(digest)
}
export function decryptData(key: Buffer, iv: Buffer, data: Buffer) {
const cipher = createDecipheriv("aes-256-cbc", key, iv).setAutoPadding(false)
return Buffer.concat([cipher.update(data), cipher.final()])
}
export function decryptOPData(cipherText: Buffer, cipher: Cipher) {
const key = cipherText.slice(0, -32)
assertHMac(key, cipher.hmac, cipherText.slice(-32))
const plaintext = decryptData(cipher.key, key.slice(16, 32), key.slice(32))
const size = readUint16(key.slice(8, 16))
return plaintext.slice(-size)
}
function readUint16({ buffer, byteOffset, length }: Buffer) {
return new DataView(buffer, byteOffset, length).getUint16(0, true)
}

49
src/ee.ts Normal file
View File

@ -0,0 +1,49 @@
type EventKeyWithNoArg<T> = {
[K in keyof T]: T[K] extends void ? K : never
}[keyof T]
type CallbackSignature<T, K extends keyof T> = K extends EventKeyWithNoArg<T>
? () => void
: (value: T[K]) => void
export class EventEmitter<T extends Record</* eventName */ string, /* eventArg */ any>> {
#listeners = new Map<keyof T, Set<(...args: any[]) => any>>()
#getList(key: keyof T) {
if (!this.#listeners.has(key)) {
this.#listeners.set(key, new Set())
}
return this.#listeners.get(key)!
}
on<K extends keyof T>(key: K, fn: CallbackSignature<T, K>) {
this.#getList(key).add(fn)
return this
}
off<K extends keyof T>(key: K, fn: CallbackSignature<T, K>) {
this.#getList(key).delete(fn)
return this
}
once<K extends keyof T>(key: K, fn: CallbackSignature<T, K>) {
const wrapped = (...arg: any[]) => {
;(fn as any)(...arg)
this.off(key, wrapped)
}
return this.on(key, wrapped)
}
emit<K extends EventKeyWithNoArg<T>>(key: K): this
emit<K extends keyof T>(key: K, value: T[K]): this
emit<K extends keyof T>(key: K, value?: T[K]) {
const listeners = this.#getList(key)
if (arguments.length === 1) {
listeners.forEach(fn => fn())
} else {
listeners.forEach(fn => fn(value!))
}
return this
}
}

144
src/fs.ts
View File

@ -1,94 +1,64 @@
import { resolve } from "path" import { resolve, extname } from "path"
import invariant from "tiny-invariant" import invariant from "tiny-invariant"
import type { Stats } from "fs" import type { IFileSystem } from "./adapters"
import { once } from "./util"
/** export type OnePasswordFileManager = ReturnType<typeof OnePasswordFileManager>
* An object that implements basic file system functionalities.
*
*/
export interface IFileSystem {
/**
* Synchronously tests whether or not the given path exists by checking with the file system.
* @param path A path to a file or directory.
*/
existsSync(path: string): boolean
/** export function OnePasswordFileManager(
* Asynchronously reads the entire contents of a file. fs: IFileSystem,
* @param path A path to a file. path: string,
*/ profileName: string
readFile(path: string, encoding: BufferEncoding): Promise<string> ) {
const root = resolve(path, profileName)
invariant(fs.existsSync(path), `Path ${path} does not exist.`)
invariant(fs.existsSync(root), `Profile ${profileName} does not exist.`)
/** const abs = (path: string) => resolve(root, path)
* Asynchronously writes data to a file, replacing the file if it already exists.
* @param path A path to a file.
* @param data The data to write.
*/
writeFile(path: string, data: string): Promise<void>
/** const result = {
* Asynchronous readdir(3) - read a directory. getProfile() {
* @param path A path to a directory. return fs.readFile(abs("profile.js"))
*/ },
readdir(path: string): Promise<string[]>
/** getFolders() {
* Asynchronous stat(2) - Get file status. return fs.readFile(abs("folders.js"))
* @param path A path to a file. },
*/
stat(path: string): Promise<Stats> async getAttachments() {
} const files = await fs.readdir(root)
files
export function getDefaultFileSystem(): IFileSystem { .filter(name => extname(name) === ".attachment")
const fs: typeof import("fs") = require("fs") .forEach(name => {
return { ...fs.promises, existsSync: fs.existsSync } const sep = name.indexOf("_")
} const path = resolve(root, name)
const [itemUUID, fileUUID] = [name.slice(0, sep), name.slice(sep + 1)]
export class OnePasswordFileManager { return {
private root: string itemUUID,
fileUUID,
constructor(private fs: IFileSystem, path: string, profileName: string) { getFile: once(() => fs.readFile(path)),
this.root = resolve(path, profileName) }
invariant(fs.existsSync(path), `Path ${path} does not exist.`) })
invariant(fs.existsSync(this.root), `Profile ${profileName} does not exist.`) },
}
async getBand(name: string) {
#hasFile(path: string) { const path = abs(`band_${name}.js`)
return this.fs.existsSync(resolve(this.root, path)) if (fs.existsSync(path)) {
} return await fs.readFile(path)
}
async #readFile(path: string) { },
return await this.fs.readFile(resolve(this.root, path), "utf-8")
} async setProfile(profile: string) {
await fs.writeFile("profile.js", profile)
async #writeFile(path: string, value: string) { },
return await this.fs.writeFile(resolve(this.root, path), value)
} async setFolders(folders: string) {
await fs.writeFile("folders.js", folders)
getProfile() { },
return this.#readFile("profile.js")
} async setBand(name: string, band: string) {
await fs.writeFile(`band_${name}.js`, band)
getFolders() { },
return this.#readFile("folders.js") }
} return result
async getBand(name: string) {
const fileName = `band_${name.toUpperCase()}.js`
if (this.#hasFile(fileName)) {
return await this.#readFile(fileName)
}
}
setProfile(profile: string) {
this.#writeFile("profile.js", profile)
}
setFolders(folders: string) {
this.#writeFile("folders.js", folders)
}
setBand(name: string, band: string) {
this.#writeFile(`band_${name}.js`, band)
}
} }

5
src/global.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
type integer = number
interface Disposable {
dispose(): void
}

View File

@ -1,22 +1,22 @@
import { resolve } from "path" import { resolve } from "path"
import type { IFileSystem } from "./fs" import { Vault } from "./models/vault"
import { getDefaultFileSystem } from "./fs" import { invariant } from "./errors"
import { Vault } from "./vault" import type { IAdapter } from "./adapters"
import { asyncMap } from "./util"
export type { Vault } from "./vault" export type { Vault } from "./models/vault"
export { Category, FieldType } from "./models" export { Category, FieldType } from "./models"
interface Options { interface IOptions {
/** /**
* Path to `.opvault` directory * Path to `.opvault` directory
*/ */
path: string path: string
/** /**
* Underlying `fs` module. You can replace it with a wrapper of * Adapter used to interact with the file system and cryptography modules
* `memfs` or any object that implements `IFileSystem`.
*/ */
fs?: IFileSystem adapter?: IAdapter
} }
/** /**
@ -24,29 +24,28 @@ interface Options {
*/ */
export class OnePassword { export class OnePassword {
readonly #path: string readonly #path: string
readonly #fs: IFileSystem readonly #adapter: IAdapter
constructor({ path, fs = getDefaultFileSystem() }: Options) { constructor({ path, adapter = require("./adapters/node").default }: IOptions) {
this.#fs = fs this.#adapter = adapter
this.#path = path this.#path = path
invariant(path, "Path must not be empty")
} }
/** /**
* @returns A list of names of profiles of the current vault. * @returns A list of names of profiles of the current vault.
*/ */
async getProfileNames() { async getProfileNames() {
const [fs, path] = [this.#fs, this.#path] const [fs, path] = [this.#adapter.fs, this.#path]
const children = await fs.readdir(path) const children = await fs.readdir(path)
const profiles: string[] = [] const profiles: string[] = []
await Promise.all( await asyncMap(children, async child => {
children.map(async child => { const fullPath = resolve(path, child)
const fullPath = resolve(path, child) const stats = await fs.stat(fullPath)
const stats = await fs.stat(fullPath) if (stats.isDirectory() && fs.existsSync(resolve(fullPath, "profile.js"))) {
if (stats.isDirectory() && fs.existsSync(resolve(fullPath, "profile.js"))) { profiles.push(child)
profiles.push(child) }
} })
})
)
return profiles return profiles
} }
@ -54,6 +53,6 @@ export class OnePassword {
* @returns A OnePassword Vault instance. * @returns A OnePassword Vault instance.
*/ */
async getProfile(profileName: string) { async getProfile(profileName: string) {
return await Vault.of(this.#path, profileName, this.#fs) return await Vault.of(this.#path, profileName, this.#adapter)
} }
} }

83
src/models/attachment.ts Normal file
View File

@ -0,0 +1,83 @@
import type { Cipher } from "../crypto"
import { decryptOPData } from "../crypto"
import { invariant } from "../errors"
import { cache } from "../util"
import type { Item } from "./item"
type integer = number
export interface AttachmentMetadata {
itemUUID: string
contentSize: integer
external: boolean
updatedAt: integer
txTimestamp: integer
/** Base64 encoded OPData */
overview: string
createdAt: integer
uuid: string
}
export class Attachment implements Disposable {
#item: WeakRef<Item>
private metadataSize: number
private iconSize: number
constructor(item: Item, private buffer: Buffer) {
this.#validate()
this.#item = new WeakRef(item)
this.metadataSize = buffer.readIntLE(8, 2)
this.iconSize = buffer.readIntLE(12, 3)
}
/**
* Validate attachment file.
*/
#validate() {
const file = this.buffer
invariant(
file.slice(0, 6).toString("utf-8") === "OPCLDA",
"Attachment must start with OPCLDA"
)
invariant(
file.readIntLE(7, 1) === 1,
"The version for this attachment file format is not supported."
)
}
@cache()
get metadata(): AttachmentMetadata {
return JSON.parse(this.buffer.slice(16, 16 + this.metadataSize).toString("utf-8"))
}
@cache()
get icon() {
const { buffer, metadataSize, iconSize } = this
const iconData = buffer.slice(16 + metadataSize, 16 + metadataSize + iconSize)
return decryptOPData(iconData, cipher)
}
/**
* Decrypt the content of this attachment. This function
* shall be called by an `Item` where `cipher` is `deriveConcreteKey(item, master)`.
* @internal
*/
decrypt(cipher: Cipher) {
const { buffer, metadataSize, iconSize } = this
const metadata: AttachmentMetadata = JSON.parse(
buffer.slice(16, 16 + metadataSize).toString("utf-8")
)
const iconData = buffer.slice(16 + metadataSize, 16 + metadataSize + iconSize)
return {
metadata,
icon: decryptOPData(iconData, cipher),
file: decryptOPData(buffer.slice(16 + metadataSize + iconSize), cipher),
}
}
dispose() {
this.buffer = null!
}
}

1
src/models/crypto.ts Normal file
View File

@ -0,0 +1 @@
export class Crypto {}

67
src/models/item.ts Normal file
View File

@ -0,0 +1,67 @@
import { assertHMac, decryptData, decryptOPData, splitPlainText } from "../crypto"
import type { ItemDetails } from "../types"
import type { Cipher } from "../crypto"
import type { Attachment } from "./attachment"
export interface EncryptedItem {
category: string // "001"
/** Unix seconds */
created: integer
d: string // "b3BkYXRhMbt"
folder: string // 32 chars
hmac: string // base64
k: string // base64
o: string // base64
tx: integer // Unix seconds
updated: integer // Unix seconds
uuid: string // 32 chars
}
export class ItemAttachmentBridge {
constructor(public cipher?: Cipher) {}
}
export class Item implements Disposable {
#data: EncryptedItem
itemDetails!: ItemDetails
attachments: Attachment[] = []
get uuid() {
return this.#data.uuid
}
constructor(data: EncryptedItem) {
this.#data = data
}
dispose() {
this.attachments.forEach(a => a.dispose())
this.itemDetails = null!
}
#deriveConcreteKey(master: Cipher) {
const k = Buffer.from(this.#data.k, "base64")
const data = k.slice(0, -32)
assertHMac(data, master.hmac, k.slice(-32))
const derivedKey = decryptData(master.key, data.slice(0, 16), data.slice(16))
return splitPlainText(derivedKey)
}
/** @internal */
decryptItemDetail(master: Cipher): ItemDetails {
const cipher = this.#deriveConcreteKey(master)
const detail = decryptOPData(
/* cipherText */ Buffer.from(this.#data.d, "base64"),
/* cipher */ cipher
)
this.itemDetails = JSON.parse(detail.toString("utf-8"))
return this.itemDetails
}
/** @internal */
decryptAttachments(master: Cipher) {
const cipher = this.#deriveConcreteKey(master)
this.attachments.forEach(a => a.decrypt(cipher))
}
}

View File

@ -1,24 +1,27 @@
import * as crypto from "crypto" import * as crypto from "crypto"
import { HMACAssertionError, invariant } from "./errors" import type { IAdapter } from "../adapters"
import type { IFileSystem } from "./fs" import type { Cipher } from "../crypto"
import { OnePasswordFileManager } from "./fs" import { decryptKeys, splitPlainText, decryptOPData } from "../crypto"
import { i18n } from "./i18n" import { HMACAssertionError, invariant } from "../errors"
import { OnePasswordFileManager } from "../fs"
import type { import { i18n } from "../i18n"
Profile, import type { EncryptedItem } from "./item"
Band, import { Item } from "./item"
Overview, import type { Profile, Overview } from "../types"
EncryptedItem, import { WeakValueMap } from "../weakMap"
Item, import { EventEmitter } from "../ee"
AttachmentMetadata,
} from "./types"
type Band = { [uuid: string]: Item }
type FoldersMap = { [uuid: string]: Band } type FoldersMap = { [uuid: string]: Band }
interface VaultEvents {
lock: void
}
/** /**
* Main OnePassword Vault class * Main OnePassword Vault class
*/ */
export class Vault { export class Vault extends EventEmitter<VaultEvents> {
// Ciphers // Ciphers
#master?: Cipher #master?: Cipher
#overview?: Cipher #overview?: Cipher
@ -26,42 +29,50 @@ export class Vault {
// File system interface // File system interface
#files: OnePasswordFileManager #files: OnePasswordFileManager
// Encrypted contents
#profile: Profile #profile: Profile
#folders: FoldersMap #folders: FoldersMap
#bands = new Map<string, Band>()
// Decrypted contents with plain-texts
#overviews = new Map<string, Overview>() #overviews = new Map<string, Overview>()
#items = new Map<string, EncryptedItem>() #items: Item[] = []
#itemsMap = new WeakValueMap<string, Item>()
private constructor( private constructor(
files: OnePasswordFileManager, files: OnePasswordFileManager,
profile: Profile, profile: Profile,
folders: FoldersMap, folders: FoldersMap,
bands: Map<string, Band> items: Item[]
) { ) {
super()
this.#files = files this.#files = files
this.#profile = profile this.#profile = profile
this.#folders = folders this.#folders = folders
this.#bands = bands this.#items = items
items.forEach(item => {
this.#itemsMap.set(item.uuid, item)
})
} }
/** /**
* Create a new OnePassword Vault instance and read all bands. * Create a new OnePassword Vault instance and read all bands.
* @internal
*/ */
static async of(path: string, profileName = "default", fs: IFileSystem) { static async of(path: string, profileName = "default", adapter: IAdapter) {
const files = new OnePasswordFileManager(fs, path, profileName) const files = OnePasswordFileManager(adapter.fs, path, profileName)
const profile = JSON.parse( const profile = JSON.parse(
stripText(await files.getProfile(), /^var profile\s*=/, ";") stripText(await files.getProfile(), /^var profile\s*=/, ";")
) )
const folders = JSON.parse(stripText(await files.getFolders(), "loadFolders(", ");")) const folders = JSON.parse(stripText(await files.getFolders(), "loadFolders(", ");"))
const bands = new Map<string, Band>() const bands: Item[] = []
for (let i = 0; i < 36; i++) { for (let i = 0; i < 36; i++) {
const letter = i.toString(36) const letter = i.toString(36).toUpperCase()
const source = await files.getBand(letter) const source = await files.getBand(letter)
bands.set(letter, source ? JSON.parse(stripText(source, "ld(", ");")) : {}) if (!source) continue
const object = JSON.parse(stripText(source, "ld(", ");"))
for (const value of Object.values(object)) {
bands.push(new Item(value as EncryptedItem))
}
} }
return new Vault(files, profile, folders, bands) return new Vault(files, profile, folders, bands)
@ -120,10 +131,6 @@ export class Vault {
return this return this
} }
decryptAttachment(buffer: Buffer) {
return decryptAttachment(buffer, this.#master!)
}
/** /**
* Remove derived keys stored within the class instance. * Remove derived keys stored within the class instance.
*/ */
@ -131,6 +138,7 @@ export class Vault {
this.#master = null! this.#master = null!
this.#overview = null! this.#overview = null!
this.#overviews.clear() this.#overviews.clear()
this.emit("lock")
return this return this
} }
@ -146,13 +154,25 @@ export class Vault {
getItem(uuid: string) { getItem(uuid: string) {
this.#assertUnlocked() this.#assertUnlocked()
const encrypted = uuid ? this.#bands.get(uuid[0])![uuid] : undefined const item = this.#itemsMap.get(uuid)
return encrypted && decryptItem(encrypted, this.#master!) if (!item) return
if (!item.itemDetails) {
item.decryptItemDetail(this.#master!)
}
const encrypted = uuid ? this.#encryptedItems.get(uuid[0])![uuid] : undefined
if (!encrypted) return
return {
original: encrypted,
details: decryptItemDetail(encrypted, this.#master!),
}
} }
#readOverviews() { #readOverviews() {
this.#assertUnlocked() this.#assertUnlocked()
this.#bands.forEach(value => { this.#encryptedItems.forEach(value => {
for (const [uuid, item] of Object.entries(value)) { for (const [uuid, item] of Object.entries(value)) {
const overview = decryptOverview(item, this.#overview!) const overview = decryptOverview(item, this.#overview!)
overview.uuid = uuid overview.uuid = uuid
@ -162,7 +182,7 @@ export class Vault {
} }
} }
function decryptOverview(item: EncryptedItem, overviewCipher: Cipher) { function decryptOverview(item: EncryptedItem, overviewCipher: Cipher): Overview {
try { try {
const overview = decryptOPData(toBuffer(item.o), overviewCipher) const overview = decryptOPData(toBuffer(item.o), overviewCipher)
return JSON.parse(overview.toString("utf8")) as Overview return JSON.parse(overview.toString("utf8")) as Overview
@ -172,49 +192,6 @@ function decryptOverview(item: EncryptedItem, overviewCipher: Cipher) {
} }
} }
function decryptItem(item: EncryptedItem, master: Cipher): Item {
const k = toBuffer(item.k)
const data = k.slice(0, -32)
assertHMac(data, master.hmac, k.slice(-32))
const derivedKey = decryptData(master.key, data.slice(0, 16), data.slice(16))
const detail = decryptOPData(
/* cipherText */ toBuffer(item.d),
/* cipher */ splitPlainText(derivedKey)
)
return JSON.parse(detail.toString("utf-8"))
}
function decryptAttachment(item: Buffer, master: Cipher) {
invariant(
item.slice(0, 6).toString("utf-8") === "OPCLDA",
"Attachment must start with OPCLDA"
)
invariant(
item.readIntLE(7, 1) === 1,
"The version for this attachment file format is not supported."
)
const metadataSize = item.readIntLE(8, 2)
const iconSize = item.readIntLE(12, 3)
const metadata: AttachmentMetadata = JSON.parse(
item.slice(16, 16 + metadataSize).toString("utf-8")
)
const icondata = item.slice(16 + metadataSize, 16 + metadataSize + iconSize)
console.log(icondata.slice(0, 8).toString())
const iconData = decryptOPData(icondata, master)
return iconData
}
/** Encryption and MAC */
interface Cipher {
/** Encryption key */
key: Buffer
/** HMAC key */
hmac: Buffer
}
function stripText(text: string, prefix: string | RegExp, suffix: string | RegExp) { function stripText(text: string, prefix: string | RegExp, suffix: string | RegExp) {
if (typeof prefix === "string") { if (typeof prefix === "string") {
if (text.startsWith(prefix)) { if (text.startsWith(prefix)) {
@ -239,43 +216,6 @@ function stripText(text: string, prefix: string | RegExp, suffix: string | RegEx
return text return text
} }
const splitPlainText = (derivedKey: Buffer): Cipher => ({
key: derivedKey.slice(0, 32),
hmac: derivedKey.slice(32, 64),
})
function decryptOPData(cipherText: Buffer, cipher: Cipher) {
const key = cipherText.slice(0, -32)
assertHMac(key, cipher.hmac, cipherText.slice(-32))
const plaintext = decryptData(cipher.key, key.slice(16, 32), key.slice(32))
const size = readUint16(key.slice(8, 16))
return plaintext.slice(-size)
}
function assertHMac(data: Buffer, key: Buffer, expected: Buffer) {
const actual = crypto.createHmac("sha256", key).update(data).digest()
if (!actual.equals(expected)) {
throw new HMACAssertionError()
}
}
function decryptKeys(encryptedKey: string, derived: Cipher) {
const buffer = toBuffer(encryptedKey)
const base = decryptOPData(buffer, derived)
const digest = crypto.createHash("sha512").update(base).digest()
return splitPlainText(digest)
}
function decryptData(key: Buffer, iv: Buffer, data: Buffer) {
const cipher = crypto.createDecipheriv("aes-256-cbc", key, iv).setAutoPadding(false)
return Buffer.concat([cipher.update(data), cipher.final()])
}
function readUint16({ buffer, byteOffset, length }: Buffer) {
return new DataView(buffer, byteOffset, length).getUint16(0, true)
}
function toBuffer(data: string) { function toBuffer(data: string) {
return Buffer.from(data, "base64") return Buffer.from(data, "base64")
} }

View File

@ -15,32 +15,6 @@ export interface Profile {
createdAt: integer // Unix seconds createdAt: integer // Unix seconds
} }
export interface EncryptedItem {
category: string // "001"
/** Unix seconds */
created: integer
d: string // "b3BkYXRhMbt"
folder: string // 32 chars
hmac: string // base64
k: string // base64
o: string // base64
tx: integer // Unix seconds
updated: integer // Unix seconds
uuid: string // 32 chars
}
export interface AttachmentMetadata {
itemUUID: string
contentSize: integer
external: boolean
updatedAt: integer
txTimestamp: integer
/** Base64 encoded OPData */
overview: string
createdAt: integer
uuid: string
}
export type TextField = { export type TextField = {
type: FieldType.Text type: FieldType.Text
value: string value: string
@ -119,7 +93,7 @@ declare namespace ItemSection {
} }
// One of them is empty?, 0C4F27910A64488BB339AED63565D148 // One of them is empty?, 0C4F27910A64488BB339AED63565D148
export interface Item { export interface ItemDetails {
htmlForm?: { htmlForm?: {
htmlAction: string // "/login/" htmlAction: string // "/login/"
htmlMethod: "post" | "get" htmlMethod: "post" | "get"
@ -134,10 +108,6 @@ export interface Item {
fields?: ItemField[] fields?: ItemField[]
} }
export interface Band {
[uuid: string]: EncryptedItem
}
export interface Folder { export interface Folder {
created: number // 1373754128 created: number // 1373754128
overview: string // "b3BkYXRhT/../KBM=" overview: string // "b3BkYXRhT/../KBM="

39
src/util.ts Normal file
View File

@ -0,0 +1,39 @@
import { invariant } from "./errors"
export function asyncMap<T, R>(
list: T[],
fn: (value: T, index: number, list: T[]) => Promise<R>
) {
return Promise.all(list.map(fn))
}
export function once<T extends (...args: any[]) => any>(fn: T): T {
let result: ReturnType<T>
let executed = false
const res = function (this: ThisParameterType<T>) {
if (executed) {
return result
} else {
result = fn.apply(this, arguments as any as Parameters<T>)
executed = true
return result
}
}
return res as any
}
export const cache = (): MethodDecorator => (_, key, descriptor: any) => {
if (process.env.NODE_ENV !== "production") {
invariant(typeof key === "string")
invariant(descriptor.get != null)
}
const cacheMap = new WeakMap()
const fn = descriptor.get
descriptor.get = function () {
if (!cacheMap.has(this)) {
cacheMap.set(this, fn.call(this))
}
return cacheMap.get(this)!
}
}

40
src/weakMap.ts Normal file
View File

@ -0,0 +1,40 @@
export class WeakValueMap<K, V extends object> {
#map = new Map<K, WeakRef<V>>()
#delete = (key: K) => {
const value = this.#map.get(key)!
this.#finalizers.unregister(value)
this.#map.delete(key)
return false
}
#finalizers = new FinalizationRegistry((key: K) => {
this.#map.delete(key)
})
delete(key: K) {
return this.#map.has(key) && !this.#delete(key)
}
has(key: K) {
const has = this.#map.has(key)
const value = has && !this.#map.get(key)!.deref()
return value ? this.#delete(key) : has
}
get(key: K) {
return this.#map.get(key)?.deref()
}
set(key: K, value: V) {
this.delete(key)
const ref = new WeakRef(value)
this.#finalizers.register(value, key, ref)
this.#map.set(key, ref)
return this
}
clear(): void {
this.#map.clear()
}
}

View File

@ -1,5 +1,8 @@
import chai from "chai"; import chai from "chai";
import chaiAsPromised from "chai-as-promised"; import chaiAsPromised from "chai-as-promised";
import sinonChai from "sinon-chai";
process.env.LOCALE = "en"; process.env.LOCALE = "en";
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
chai.use(sinonChai);

View File

@ -5,6 +5,7 @@ import { expect } from "chai";
import type { Vault } from "../src/index"; import type { Vault } from "../src/index";
import { OnePassword } from "../src/index"; import { OnePassword } from "../src/index";
// import adapter from "../src/adapters/node";
describe("OnePassword", () => { describe("OnePassword", () => {
const freddy = resolve(__dirname, "../freddy-2013-12-04.opvault"); const freddy = resolve(__dirname, "../freddy-2013-12-04.opvault");

43
test/weakMap.test.ts Normal file
View File

@ -0,0 +1,43 @@
import { describe, it } from "mocha";
import { expect } from "chai";
import { WeakValueMap } from "../src/weakMap";
declare const gc: () => void;
describe("WeakValueMap", () => {
interface Value {
value: number;
}
it("covers base use cases", () => {
const map = new WeakValueMap<string, Value>();
const object = { value: 1 };
map.set("key", object);
expect(map.get("key")!.value).to.equal(1);
expect(map.delete("key")).to.be.true;
expect(!map.delete("key")).to.be.true;
});
it("overrides previous value", () => {
const map = new WeakValueMap<string, Value>();
map.set("key", { value: 2 });
map.set("key", { value: 3 });
expect(map.get("key")!.value).to.equal(3);
});
it("deletes garbage collected values", (done) => {
const map = new WeakValueMap<string, Value>();
map.set("key", { value: 1 });
setTimeout(() => {
gc();
expect(map.has("key")).to.be.false;
map.set("key", { value: 2 });
setTimeout(() => {
gc();
done();
});
});
});
});