mirror of
https://github.com/standardnotes/app
synced 2026-09-17 12:45:54 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03497cbf5b | ||
|
|
2ec1a4dbb4 | ||
|
|
d026b659fb | ||
|
|
4feaff2621 | ||
|
|
945248d7d3 | ||
|
|
d311ce40b9 | ||
|
|
354fb13256 | ||
|
|
e5771fcbde |
@@ -30,9 +30,9 @@ jobs:
|
||||
run: yarn test
|
||||
working-directory: packages/components
|
||||
|
||||
- name: Lint all
|
||||
run: yarn lint
|
||||
- name: Build all
|
||||
run: yarn build:all
|
||||
- name: Lint all
|
||||
run: yarn lint
|
||||
- name: Test all
|
||||
run: yarn test
|
||||
|
||||
@@ -18,10 +18,10 @@ jobs:
|
||||
uses: ruby/setup-ruby@v1
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
- name: ESLint
|
||||
run: yarn lint
|
||||
- name: Build
|
||||
run: yarn build:all
|
||||
- name: ESLint
|
||||
run: yarn lint
|
||||
- name: Build Android
|
||||
run: yarn android:bundle
|
||||
- name: Test
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Version Bump Main
|
||||
name: Publish Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,19 +7,22 @@ on:
|
||||
jobs:
|
||||
Build:
|
||||
if: "${{ contains(github.event.head_commit.message, 'chore(release): publish') == false }}"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.CI_NPM_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
node-version: 16
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
|
||||
- name: Setup git config
|
||||
run: |
|
||||
@@ -32,13 +35,14 @@ jobs:
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
|
||||
- name: Install Dependencies
|
||||
run: yarn install
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Bump Prod Version
|
||||
run: |
|
||||
yarn release:prod
|
||||
yarn publish:prod
|
||||
- name: Bump version
|
||||
run: yarn release:prod
|
||||
|
||||
- name: Publish
|
||||
run: yarn publish:prod
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.CI_NPM_TOKEN }}
|
||||
|
||||
+3
-1
@@ -19,6 +19,8 @@ codeqldb
|
||||
coverage
|
||||
lerna-debug.log
|
||||
|
||||
packages/encryption/dist
|
||||
|
||||
**/.pnp.*
|
||||
**/.yarn/*
|
||||
!.yarn/patches
|
||||
@@ -26,4 +28,4 @@ lerna-debug.log
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
!.yarn/cache
|
||||
!.yarn/cache
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -33,7 +33,7 @@
|
||||
"prepare": "husky install",
|
||||
"reset": "find . -type dir -name node_modules | xargs rm -rf && rm -rf yarn.lock && yarn install",
|
||||
"release:prod": "lerna version --conventional-commits --yes -m \"chore(release): publish\"",
|
||||
"publish:prod": "lerna publish from-git --yes --no-verify-access",
|
||||
"publish:prod": "lerna publish from-git --yes --no-verify-access --loglevel verbose",
|
||||
"version": "yarn install --no-immutable && git add yarn.lock",
|
||||
"postversion": "./scripts/push-tags-one-by-one.sh",
|
||||
"workspace:list": " yarn lerna list -all",
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.23.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.34](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.33](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.23.32",
|
||||
"version": "3.23.35",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../.eslintrc",
|
||||
"parserOptions": {
|
||||
"project": "./linter.tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": ["warn", { "ignoreRestArgs": true }],
|
||||
"@typescript-eslint/no-non-null-assertion": "warn"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.10.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* extract encryption type split functions ([2ec1a4d](https://github.com/standardnotes/app/commit/2ec1a4dbb40497d893290e9eb66e38842c875b50))
|
||||
|
||||
## [1.10.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* encryption exports ([#1205](https://github.com/standardnotes/app/issues/1205)) ([945248d](https://github.com/standardnotes/app/commit/945248d7d3f0ac0fa153aeedd8ad18d64f30d6be))
|
||||
|
||||
# 1.10.0 (2022-07-05)
|
||||
|
||||
### Features
|
||||
|
||||
* add @standardnotes/encryption package ([#1199](https://github.com/standardnotes/app/issues/1199)) ([e5771fc](https://github.com/standardnotes/app/commit/e5771fcbde827afd178edde3b7bd872734cd3338))
|
||||
|
||||
## [1.8.24](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing reflect-metadata package to all packages ([ce3a5bb](https://github.com/standardnotes/snjs/commit/ce3a5bbf3f1d2276ac4abc3eec3c6a44c8c3ba9b))
|
||||
* unit tests running ([9ddc55c](https://github.com/standardnotes/snjs/commit/9ddc55c59c781e2bcc366304a6d0cc88d0e0865d))
|
||||
|
||||
## [1.8.23](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.22](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.21](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.20](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.19](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.18](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.17](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.16](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.15](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.14](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.13](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.12](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.11](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.10](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.9](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.8](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.7](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.6](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.5](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.4](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.3](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.2](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.1](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.8.0](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-21)
|
||||
|
||||
### Features
|
||||
|
||||
* display controllers ([#743](https://github.com/standardnotes/snjs/issues/743)) ([5fadce3](https://github.com/standardnotes/snjs/commit/5fadce37f1b3f2f51b8a90c257bc666ac7710074))
|
||||
|
||||
## [1.7.12](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.11](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.10](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.9](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.8](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* workspace signout all ([0ac4501](https://github.com/standardnotes/snjs/commit/0ac45019428946016ef02384b07b8190378008fc))
|
||||
|
||||
## [1.7.7](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.6](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.5](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.4](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.3](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.2](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.1](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-12)
|
||||
|
||||
### Features
|
||||
|
||||
* file desktop backups ([#731](https://github.com/standardnotes/snjs/issues/731)) ([0dbce7d](https://github.com/standardnotes/snjs/commit/0dbce7dc9712fde848445b951079c81479c8bc11))
|
||||
|
||||
## [1.6.12](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.11](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.10](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.9](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.8](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.7](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.6](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.5](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.4](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* config package missing dependencies ([3dec12f](https://github.com/standardnotes/snjs/commit/3dec12fa4a83a8aed8419819eafb7c34795cb09f))
|
||||
|
||||
## [1.6.3](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* config package missing dependencies ([3dec12f](https://github.com/standardnotes/snjs/commit/3dec12fa4a83a8aed8419819eafb7c34795cb09f))
|
||||
|
||||
## [1.6.2](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.1](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.6.0](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-29)
|
||||
|
||||
### Features
|
||||
|
||||
* service diagnostics ([#718](https://github.com/standardnotes/snjs/issues/718)) ([17cf40f](https://github.com/standardnotes/snjs/commit/17cf40f4489c8f1915b19c0318d252cf83bc050d))
|
||||
|
||||
## [1.5.2](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.5.1](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-28)
|
||||
|
||||
### Features
|
||||
|
||||
* refactor sncrypto to add unified sha256 and base64 usage ([#715](https://github.com/standardnotes/snjs/issues/715)) ([93aef4d](https://github.com/standardnotes/snjs/commit/93aef4d39228a63f01aa90a88e5d28c3375ed707))
|
||||
|
||||
## [1.4.13](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.12](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.11](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.10](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.9](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.8](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.7](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* abort key recovery after aborted challenge ([#703](https://github.com/standardnotes/snjs/issues/703)) ([a67fb7e](https://github.com/standardnotes/snjs/commit/a67fb7e8cde41a5c9fadf545933e35d525faeaf0))
|
||||
|
||||
## [1.4.6](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.5](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.4](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.3](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.2](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.1](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* make timestamps required in payload construction ([#695](https://github.com/standardnotes/snjs/issues/695)) ([b3326c0](https://github.com/standardnotes/snjs/commit/b3326c0a998cd9d44a76afc377f182885ef48275))
|
||||
|
||||
# [1.4.0](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-15)
|
||||
|
||||
### Features
|
||||
|
||||
* introduce sync resolved payloads to ensure deltas always return up to date dirty state ([#694](https://github.com/standardnotes/snjs/issues/694)) ([e5278ba](https://github.com/standardnotes/snjs/commit/e5278ba0b2afa987c37f009a2101fb91949d44c6))
|
||||
|
||||
# [1.3.0](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-15)
|
||||
|
||||
### Features
|
||||
|
||||
* no merge payloads in payload manager ([#693](https://github.com/standardnotes/snjs/issues/693)) ([68a577c](https://github.com/standardnotes/snjs/commit/68a577cb887fd2d5556dc9ddec461f6ae665fcb6))
|
||||
|
||||
## [1.2.6](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.2.5](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* map ignored item timestamps so application remains in sync ([#692](https://github.com/standardnotes/snjs/issues/692)) ([966cbb0](https://github.com/standardnotes/snjs/commit/966cbb0c254d4d95c802bd8951488a499d1f8bef))
|
||||
|
||||
## [1.2.4](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.2.3](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.2.2](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.2.1](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.2.0](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-01)
|
||||
|
||||
### Features
|
||||
|
||||
* content interfaces and model type strictness ([#685](https://github.com/standardnotes/snjs/issues/685)) ([e2450c5](https://github.com/standardnotes/snjs/commit/e2450c59e8309d7080efaa03905b2abc728d9403))
|
||||
|
||||
## [1.1.4](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.1.3](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-03-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.1.2](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-03-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.1.1](https://github.com/standardnotes/snjs/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-03-31)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* return undefined instead of exception when items key is not yet available ([#682](https://github.com/standardnotes/snjs/issues/682)) ([0dd7dbe](https://github.com/standardnotes/snjs/commit/0dd7dbe64c086bbdd539f24d428ac1d2bb491f16))
|
||||
|
||||
# 1.1.0 (2022-03-31)
|
||||
|
||||
### Features
|
||||
|
||||
* encryption and models packages ([#679](https://github.com/standardnotes/snjs/issues/679)) ([5e03d48](https://github.com/standardnotes/snjs/commit/5e03d48aba7e3dd266117201139ab869b1f70cc9))
|
||||
@@ -0,0 +1,11 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const base = require('../../node_modules/@standardnotes/config/src/jest.json');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
tsconfig: 'tsconfig.json',
|
||||
},
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.10.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
"description": "Payload encryption used in SNJS library",
|
||||
"main": "dist/index.js",
|
||||
"author": "Standard Notes",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"clean": "rm -fr dist",
|
||||
"prestart": "yarn clean",
|
||||
"start": "tsc -p tsconfig.json --watch",
|
||||
"prebuild": "yarn clean",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@standardnotes/config": "2.4.3",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/node": "^18.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jest": "^27.5.1",
|
||||
"ts-jest": "^27.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.23.1",
|
||||
"@standardnotes/models": "^1.11.13",
|
||||
"@standardnotes/responses": "^1.6.39",
|
||||
"@standardnotes/services": "^1.13.23",
|
||||
"@standardnotes/sncrypto-common": "^1.9.0",
|
||||
"@standardnotes/utils": "^1.6.12",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export const V001Algorithm = Object.freeze({
|
||||
SaltSeedLength: 128,
|
||||
/**
|
||||
* V001 supported a variable PBKDF2 cost
|
||||
*/
|
||||
PbkdfMinCost: 3000,
|
||||
PbkdfCostsUsed: [3000, 5000, 10_000, 60_000],
|
||||
PbkdfOutputLength: 512,
|
||||
EncryptionKeyLength: 256,
|
||||
})
|
||||
|
||||
export const V002Algorithm = Object.freeze({
|
||||
SaltSeedLength: 128,
|
||||
/**
|
||||
* V002 supported a variable PBKDF2 cost
|
||||
*/
|
||||
PbkdfMinCost: 3000,
|
||||
/**
|
||||
* While some 002 accounts also used costs in V001.PbkdfCostsUsed,
|
||||
* the vast majority used costs >= 100,000
|
||||
*/
|
||||
PbkdfCostsUsed: V001Algorithm.PbkdfCostsUsed.concat([100_000, 101_000, 102_000, 103_000]),
|
||||
/** Possible costs used, but statistically more likely these were 001 accounts */
|
||||
ImprobablePbkdfCostsUsed: [3000, 5000],
|
||||
PbkdfOutputLength: 768,
|
||||
EncryptionKeyLength: 256,
|
||||
EncryptionIvLength: 128,
|
||||
})
|
||||
|
||||
export enum V003Algorithm {
|
||||
SaltSeedLength = 256,
|
||||
PbkdfCost = 110000,
|
||||
PbkdfOutputLength = 768,
|
||||
EncryptionKeyLength = 256,
|
||||
EncryptionIvLength = 128,
|
||||
}
|
||||
|
||||
export enum V004Algorithm {
|
||||
ArgonSaltSeedLength = 256,
|
||||
ArgonSaltLength = 128,
|
||||
ArgonIterations = 5,
|
||||
ArgonMemLimit = 67108864,
|
||||
ArgonOutputKeyBytes = 64,
|
||||
EncryptionKeyLength = 256,
|
||||
EncryptionNonceLength = 192,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { BackupFileDecryptedContextualPayload, BackupFileEncryptedContextualPayload } from '@standardnotes/models'
|
||||
import { AnyKeyParamsContent, ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export type BackupFile = {
|
||||
version?: ProtocolVersion
|
||||
keyParams?: AnyKeyParamsContent
|
||||
auth_params?: AnyKeyParamsContent
|
||||
items: (BackupFileDecryptedContextualPayload | BackupFileEncryptedContextualPayload)[]
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
import {
|
||||
AnyKeyParamsContent,
|
||||
ContentType,
|
||||
ProtocolVersion,
|
||||
leftVersionGreaterThanOrEqualToRight,
|
||||
compareVersions,
|
||||
} from '@standardnotes/common'
|
||||
import { BackupFile } from './BackupFile'
|
||||
import { BackupFileType } from './BackupFileType'
|
||||
import { extendArray } from '@standardnotes/utils'
|
||||
import { EncryptionService } from '../Service/Encryption/EncryptionService'
|
||||
import {
|
||||
PayloadInterface,
|
||||
DecryptedPayloadInterface,
|
||||
ItemsKeyContent,
|
||||
EncryptedPayloadInterface,
|
||||
isEncryptedPayload,
|
||||
isDecryptedPayload,
|
||||
isEncryptedTransferPayload,
|
||||
EncryptedPayload,
|
||||
DecryptedPayload,
|
||||
isDecryptedTransferPayload,
|
||||
CreateDecryptedItemFromPayload,
|
||||
ItemsKeyInterface,
|
||||
CreatePayloadSplit,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { CreateAnyKeyParams } from '../Keys/RootKey/KeyParamsFunctions'
|
||||
import { SNRootKeyParams } from '../Keys/RootKey/RootKeyParams'
|
||||
import { SNRootKey } from '../Keys/RootKey/RootKey'
|
||||
import { ContentTypeUsesRootKeyEncryption } from '../Keys/RootKey/Functions'
|
||||
import { isItemsKey, SNItemsKey } from '../Keys/ItemsKey/ItemsKey'
|
||||
|
||||
export async function DecryptBackupFile(
|
||||
file: BackupFile,
|
||||
protocolService: EncryptionService,
|
||||
password?: string,
|
||||
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = file.items.map((item) => {
|
||||
if (isEncryptedTransferPayload(item)) {
|
||||
return new EncryptedPayload(item)
|
||||
} else if (isDecryptedTransferPayload(item)) {
|
||||
return new DecryptedPayload(item)
|
||||
} else {
|
||||
throw Error('Unhandled case in decryptBackupFile')
|
||||
}
|
||||
})
|
||||
|
||||
const { encrypted, decrypted } = CreatePayloadSplit(payloads)
|
||||
|
||||
const type = getBackupFileType(file, payloads)
|
||||
|
||||
switch (type) {
|
||||
case BackupFileType.Corrupt:
|
||||
return new ClientDisplayableError('Invalid backup file.')
|
||||
case BackupFileType.Encrypted: {
|
||||
if (!password) {
|
||||
throw Error('Attempting to decrypt encrypted file with no password')
|
||||
}
|
||||
|
||||
const keyParamsData = (file.keyParams || file.auth_params) as AnyKeyParamsContent
|
||||
|
||||
return [
|
||||
...decrypted,
|
||||
...(await decryptEncrypted(password, CreateAnyKeyParams(keyParamsData), encrypted, protocolService)),
|
||||
]
|
||||
}
|
||||
case BackupFileType.EncryptedWithNonEncryptedItemsKey:
|
||||
return [...decrypted, ...(await decryptEncryptedWithNonEncryptedItemsKey(payloads, protocolService))]
|
||||
case BackupFileType.FullyDecrypted:
|
||||
return [...decrypted, ...encrypted]
|
||||
}
|
||||
}
|
||||
|
||||
function getBackupFileType(file: BackupFile, payloads: PayloadInterface[]): BackupFileType {
|
||||
if (file.keyParams || file.auth_params) {
|
||||
return BackupFileType.Encrypted
|
||||
} else {
|
||||
const hasEncryptedItem = payloads.find(isEncryptedPayload)
|
||||
const hasDecryptedItemsKey = payloads.find(
|
||||
(payload) => payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload),
|
||||
)
|
||||
|
||||
if (hasEncryptedItem && hasDecryptedItemsKey) {
|
||||
return BackupFileType.EncryptedWithNonEncryptedItemsKey
|
||||
} else if (!hasEncryptedItem) {
|
||||
return BackupFileType.FullyDecrypted
|
||||
} else {
|
||||
return BackupFileType.Corrupt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function decryptEncryptedWithNonEncryptedItemsKey(
|
||||
allPayloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[],
|
||||
protocolService: EncryptionService,
|
||||
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const decryptedItemsKeys: DecryptedPayloadInterface<ItemsKeyContent>[] = []
|
||||
const encryptedPayloads: EncryptedPayloadInterface[] = []
|
||||
|
||||
allPayloads.forEach((payload) => {
|
||||
if (payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload)) {
|
||||
decryptedItemsKeys.push(payload as DecryptedPayloadInterface<ItemsKeyContent>)
|
||||
} else if (isEncryptedPayload(payload)) {
|
||||
encryptedPayloads.push(payload)
|
||||
}
|
||||
})
|
||||
|
||||
const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload<ItemsKeyContent, SNItemsKey>(p))
|
||||
|
||||
return decryptWithItemsKeys(encryptedPayloads, itemsKeys, protocolService)
|
||||
}
|
||||
|
||||
function findKeyToUseForPayload(
|
||||
payload: EncryptedPayloadInterface,
|
||||
availableKeys: ItemsKeyInterface[],
|
||||
protocolService: EncryptionService,
|
||||
keyParams?: SNRootKeyParams,
|
||||
fallbackRootKey?: SNRootKey,
|
||||
): ItemsKeyInterface | SNRootKey | undefined {
|
||||
let itemsKey: ItemsKeyInterface | SNRootKey | undefined
|
||||
|
||||
if (payload.items_key_id) {
|
||||
itemsKey = protocolService.itemsKeyForPayload(payload)
|
||||
if (itemsKey) {
|
||||
return itemsKey
|
||||
}
|
||||
}
|
||||
|
||||
itemsKey = availableKeys.find((itemsKeyPayload) => {
|
||||
return payload.items_key_id === itemsKeyPayload.uuid
|
||||
})
|
||||
|
||||
if (itemsKey) {
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
if (!keyParams) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const payloadVersion = payload.version as ProtocolVersion
|
||||
|
||||
/**
|
||||
* Payloads with versions <= 003 use root key directly for encryption.
|
||||
* However, if the incoming key params are >= 004, this means we should
|
||||
* have an items key based off the 003 root key. We can't use the 004
|
||||
* root key directly because it's missing dataAuthenticationKey.
|
||||
*/
|
||||
if (leftVersionGreaterThanOrEqualToRight(keyParams.version, ProtocolVersion.V004)) {
|
||||
itemsKey = protocolService.defaultItemsKeyForItemVersion(payloadVersion, availableKeys)
|
||||
} else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) {
|
||||
itemsKey = fallbackRootKey
|
||||
}
|
||||
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
async function decryptWithItemsKeys(
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
itemsKeys: ItemsKeyInterface[],
|
||||
protocolService: EncryptionService,
|
||||
keyParams?: SNRootKeyParams,
|
||||
fallbackRootKey?: SNRootKey,
|
||||
): Promise<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]> {
|
||||
const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = []
|
||||
|
||||
for (const encryptedPayload of payloads) {
|
||||
if (ContentTypeUsesRootKeyEncryption(encryptedPayload.content_type)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const key = findKeyToUseForPayload(encryptedPayload, itemsKeys, protocolService, keyParams, fallbackRootKey)
|
||||
|
||||
if (!key) {
|
||||
results.push(
|
||||
encryptedPayload.copy({
|
||||
errorDecrypting: true,
|
||||
}),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (isItemsKey(key)) {
|
||||
const decryptedPayload = await protocolService.decryptSplitSingle({
|
||||
usesItemsKey: {
|
||||
items: [encryptedPayload],
|
||||
key: key,
|
||||
},
|
||||
})
|
||||
results.push(decryptedPayload)
|
||||
} else {
|
||||
const decryptedPayload = await protocolService.decryptSplitSingle({
|
||||
usesRootKey: {
|
||||
items: [encryptedPayload],
|
||||
key: key,
|
||||
},
|
||||
})
|
||||
results.push(decryptedPayload)
|
||||
}
|
||||
} catch (e) {
|
||||
results.push(
|
||||
encryptedPayload.copy({
|
||||
errorDecrypting: true,
|
||||
}),
|
||||
)
|
||||
console.error('Error decrypting payload', encryptedPayload, e)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
async function decryptEncrypted(
|
||||
password: string,
|
||||
keyParams: SNRootKeyParams,
|
||||
payloads: EncryptedPayloadInterface[],
|
||||
protocolService: EncryptionService,
|
||||
): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> {
|
||||
const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = []
|
||||
const rootKey = await protocolService.computeRootKey(password, keyParams)
|
||||
|
||||
const itemsKeysPayloads = payloads.filter((payload) => {
|
||||
return payload.content_type === ContentType.ItemsKey
|
||||
})
|
||||
|
||||
const itemsKeysDecryptionResults = await protocolService.decryptSplit({
|
||||
usesRootKey: {
|
||||
items: itemsKeysPayloads,
|
||||
key: rootKey,
|
||||
},
|
||||
})
|
||||
|
||||
extendArray(results, itemsKeysDecryptionResults)
|
||||
|
||||
const decryptedPayloads = await decryptWithItemsKeys(
|
||||
payloads,
|
||||
itemsKeysDecryptionResults.filter(isDecryptedPayload).map((p) => CreateDecryptedItemFromPayload(p)),
|
||||
protocolService,
|
||||
keyParams,
|
||||
rootKey,
|
||||
)
|
||||
|
||||
extendArray(results, decryptedPayloads)
|
||||
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum BackupFileType {
|
||||
Encrypted = 'Encrypted',
|
||||
|
||||
/**
|
||||
* Generated when an export is made from an application with no account and no passcode. The
|
||||
* items are encrypted, but the items keys are not.
|
||||
*/
|
||||
EncryptedWithNonEncryptedItemsKey = 'EncryptedWithNonEncryptedItemsKey',
|
||||
|
||||
FullyDecrypted = 'FullyDecrypted',
|
||||
|
||||
Corrupt = 'Corrupt',
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
ConflictStrategy,
|
||||
ItemsKeyContent,
|
||||
DecryptedItem,
|
||||
DecryptedPayloadInterface,
|
||||
DecryptedItemInterface,
|
||||
HistoryEntryInterface,
|
||||
ItemsKeyInterface,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export function isItemsKey(x: ItemsKeyInterface | RootKeyInterface): x is ItemsKeyInterface {
|
||||
return x.content_type === ContentType.ItemsKey
|
||||
}
|
||||
|
||||
/**
|
||||
* A key used to encrypt other items. Items keys are synced and persisted.
|
||||
*/
|
||||
export class SNItemsKey extends DecryptedItem<ItemsKeyContent> implements ItemsKeyInterface {
|
||||
keyVersion: ProtocolVersion
|
||||
isDefault: boolean | undefined
|
||||
itemsKey: string
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<ItemsKeyContent>) {
|
||||
super(payload)
|
||||
this.keyVersion = payload.content.version
|
||||
this.isDefault = payload.content.isDefault
|
||||
this.itemsKey = this.payload.content.itemsKey
|
||||
}
|
||||
|
||||
/** Do not duplicate items keys. Always keep original */
|
||||
override strategyWhenConflictingWithItem(
|
||||
_item: DecryptedItemInterface,
|
||||
_previousRevision?: HistoryEntryInterface,
|
||||
): ConflictStrategy {
|
||||
return ConflictStrategy.KeepBase
|
||||
}
|
||||
|
||||
get dataAuthenticationKey(): string | undefined {
|
||||
if (this.keyVersion === ProtocolVersion.V004) {
|
||||
throw 'Attempting to access legacy data authentication key.'
|
||||
}
|
||||
return this.payload.content.dataAuthenticationKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { DecryptedItemMutator, ItemsKeyMutatorInterface, ItemsKeyContent } from '@standardnotes/models'
|
||||
|
||||
export class ItemsKeyMutator extends DecryptedItemMutator<ItemsKeyContent> implements ItemsKeyMutatorInterface {
|
||||
set isDefault(isDefault: boolean) {
|
||||
this.mutableContent.isDefault = isDefault
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { RegisterItemClass, DecryptedItemMutator, ItemsKeyContent } from '@standardnotes/models'
|
||||
import { SNItemsKey } from './ItemsKey'
|
||||
import { ItemsKeyMutator } from './ItemsKeyMutator'
|
||||
|
||||
RegisterItemClass(ContentType.ItemsKey, SNItemsKey, ItemsKeyMutator as unknown as DecryptedItemMutator<ItemsKeyContent>)
|
||||
@@ -0,0 +1,51 @@
|
||||
import { SNRootKey } from './RootKey'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
FillItemContentSpecialized,
|
||||
PayloadTimestampDefaults,
|
||||
RootKeyContent,
|
||||
RootKeyContentSpecialized,
|
||||
} from '@standardnotes/models'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export function CreateNewRootKey(content: RootKeyContentSpecialized): SNRootKey {
|
||||
const uuid = UuidGenerator.GenerateUuid()
|
||||
|
||||
const payload = new DecryptedPayload<RootKeyContent>({
|
||||
uuid: uuid,
|
||||
content_type: ContentType.RootKey,
|
||||
content: FillRootKeyContent(content),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
|
||||
return new SNRootKey(payload)
|
||||
}
|
||||
|
||||
export function FillRootKeyContent(content: Partial<RootKeyContentSpecialized>): RootKeyContent {
|
||||
if (!content.version) {
|
||||
if (content.dataAuthenticationKey) {
|
||||
/**
|
||||
* If there's no version stored, it must be either 001 or 002.
|
||||
* If there's a dataAuthenticationKey, it has to be 002. Otherwise it's 001.
|
||||
*/
|
||||
content.version = ProtocolVersion.V002
|
||||
} else {
|
||||
content.version = ProtocolVersion.V001
|
||||
}
|
||||
}
|
||||
|
||||
return FillItemContentSpecialized(content)
|
||||
}
|
||||
|
||||
export function ContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean {
|
||||
return (
|
||||
contentType === ContentType.RootKey ||
|
||||
contentType === ContentType.ItemsKey ||
|
||||
contentType === ContentType.EncryptedStorage
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean {
|
||||
return contentType === ContentType.ItemsKey
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { KeyParamsResponse } from '@standardnotes/responses'
|
||||
import {
|
||||
KeyParamsContent001,
|
||||
KeyParamsContent002,
|
||||
KeyParamsContent003,
|
||||
KeyParamsContent004,
|
||||
AnyKeyParamsContent,
|
||||
} from '@standardnotes/common'
|
||||
import { SNRootKeyParams } from './RootKeyParams'
|
||||
import { ProtocolVersionForKeyParams } from './ProtocolVersionForKeyParams'
|
||||
|
||||
/**
|
||||
* 001, 002:
|
||||
* - Nonce is not uploaded to server, instead used to compute salt locally and send to server
|
||||
* - Salt is returned from server
|
||||
* - Cost/iteration count is returned from the server
|
||||
* - Account identifier is returned as 'email'
|
||||
* 003, 004:
|
||||
* - Salt is computed locally via the seed (pw_nonce) returned from the server
|
||||
* - Cost/iteration count is determined locally by the protocol version
|
||||
* - Account identifier is returned as 'identifier'
|
||||
*/
|
||||
|
||||
export type AllKeyParamsContents = KeyParamsContent001 & KeyParamsContent002 & KeyParamsContent003 & KeyParamsContent004
|
||||
|
||||
export function Create001KeyParams(keyParams: KeyParamsContent001) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function Create002KeyParams(keyParams: KeyParamsContent002) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function Create003KeyParams(keyParams: KeyParamsContent003) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function Create004KeyParams(keyParams: KeyParamsContent004) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function CreateAnyKeyParams(keyParams: AnyKeyParamsContent) {
|
||||
if ('content' in keyParams) {
|
||||
throw Error('Raw key params shouldnt have content; perhaps you passed in a SNRootKeyParams object.')
|
||||
}
|
||||
return new SNRootKeyParams(keyParams)
|
||||
}
|
||||
|
||||
export function KeyParamsFromApiResponse(response: KeyParamsResponse, identifier?: string) {
|
||||
const rawKeyParams: AnyKeyParamsContent = {
|
||||
identifier: identifier || response.data.identifier!,
|
||||
pw_cost: response.data.pw_cost!,
|
||||
pw_nonce: response.data.pw_nonce!,
|
||||
pw_salt: response.data.pw_salt!,
|
||||
version: ProtocolVersionForKeyParams(response.data),
|
||||
origination: response.data.origination,
|
||||
created: response.data.created,
|
||||
}
|
||||
return CreateAnyKeyParams(rawKeyParams)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { V001Algorithm, V002Algorithm } from '../../Algorithm'
|
||||
import { KeyParamsData } from '@standardnotes/responses'
|
||||
import { AnyKeyParamsContent, ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export function ProtocolVersionForKeyParams(response: KeyParamsData | AnyKeyParamsContent): ProtocolVersion {
|
||||
if (response.version) {
|
||||
return response.version
|
||||
}
|
||||
/**
|
||||
* 001 and 002 key params (as stored locally) may not report a version number.
|
||||
* In some cases it may be impossible to differentiate between 001 and 002 params,
|
||||
* but there are a few rules we can use to find a best fit.
|
||||
*/
|
||||
/**
|
||||
* First try to determine by cost. If the cost appears in V002 costs but not V001 costs,
|
||||
* we know it's 002.
|
||||
*/
|
||||
const cost = response.pw_cost!
|
||||
const appearsInV001 = V001Algorithm.PbkdfCostsUsed.includes(cost)
|
||||
const appearsInV002 = V002Algorithm.PbkdfCostsUsed.includes(cost)
|
||||
|
||||
if (appearsInV001 && !appearsInV002) {
|
||||
return ProtocolVersion.V001
|
||||
} else if (appearsInV002 && !appearsInV001) {
|
||||
return ProtocolVersion.V002
|
||||
} else if (appearsInV002 && appearsInV001) {
|
||||
/**
|
||||
* If the cost appears in both versions, we can be certain it's 002 if it's missing
|
||||
* the pw_nonce property. (However late versions of 002 also used a pw_nonce, so its
|
||||
* presence doesn't automatically indicate 001.)
|
||||
*/
|
||||
if (!response.pw_nonce) {
|
||||
return ProtocolVersion.V002
|
||||
} else {
|
||||
/**
|
||||
* We're now at the point that the cost has appeared in both versions, and a pw_nonce
|
||||
* is present. We'll have to go with what is more statistically likely.
|
||||
*/
|
||||
if (V002Algorithm.ImprobablePbkdfCostsUsed.includes(cost)) {
|
||||
return ProtocolVersion.V001
|
||||
} else {
|
||||
return ProtocolVersion.V002
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/** Doesn't appear in either V001 or V002; unlikely possibility. */
|
||||
return ProtocolVersion.V002
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { SNRootKeyParams } from './RootKeyParams'
|
||||
import {
|
||||
RootKeyInterface,
|
||||
RootKeyContent,
|
||||
DecryptedItem,
|
||||
DecryptedPayloadInterface,
|
||||
RootKeyContentInStorage,
|
||||
NamespacedRootKeyInKeychain,
|
||||
} from '@standardnotes/models'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { timingSafeEqual } from '@standardnotes/sncrypto-common'
|
||||
|
||||
/**
|
||||
* A root key is a local only construct that houses the key used for the encryption
|
||||
* and decryption of items keys. A root key extends SNItem for local convenience, but is
|
||||
* not part of the syncing or storage ecosystem—root keys are managed independently.
|
||||
*/
|
||||
export class SNRootKey extends DecryptedItem<RootKeyContent> implements RootKeyInterface {
|
||||
public readonly keyParams: SNRootKeyParams
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<RootKeyContent>) {
|
||||
super(payload)
|
||||
|
||||
this.keyParams = new SNRootKeyParams(payload.content.keyParams)
|
||||
}
|
||||
|
||||
public get keyVersion(): ProtocolVersion {
|
||||
return this.content.version
|
||||
}
|
||||
|
||||
/**
|
||||
* When the root key is used to encrypt items, we use the masterKey directly.
|
||||
*/
|
||||
public get itemsKey(): string {
|
||||
return this.masterKey
|
||||
}
|
||||
|
||||
public get masterKey(): string {
|
||||
return this.content.masterKey
|
||||
}
|
||||
|
||||
/**
|
||||
* serverPassword is not persisted as part of keychainValue, so if loaded from disk,
|
||||
* this value may be undefined.
|
||||
*/
|
||||
public get serverPassword(): string | undefined {
|
||||
return this.content.serverPassword
|
||||
}
|
||||
|
||||
/** 003 and below only. */
|
||||
public get dataAuthenticationKey(): string | undefined {
|
||||
return this.content.dataAuthenticationKey
|
||||
}
|
||||
|
||||
public compare(otherKey: SNRootKey): boolean {
|
||||
if (this.keyVersion !== otherKey.keyVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.serverPassword && otherKey.serverPassword) {
|
||||
return (
|
||||
timingSafeEqual(this.masterKey, otherKey.masterKey) &&
|
||||
timingSafeEqual(this.serverPassword, otherKey.serverPassword)
|
||||
)
|
||||
} else {
|
||||
return timingSafeEqual(this.masterKey, otherKey.masterKey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Object suitable for persist in storage when wrapped
|
||||
*/
|
||||
public persistableValueWhenWrapping(): RootKeyContentInStorage {
|
||||
return {
|
||||
...this.getKeychainValue(),
|
||||
keyParams: this.keyParams.getPortableValue(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Object that is suitable for persisting in a keychain
|
||||
*/
|
||||
public getKeychainValue(): NamespacedRootKeyInKeychain {
|
||||
const values: NamespacedRootKeyInKeychain = {
|
||||
version: this.keyVersion,
|
||||
masterKey: this.masterKey,
|
||||
}
|
||||
|
||||
if (this.dataAuthenticationKey) {
|
||||
values.dataAuthenticationKey = this.dataAuthenticationKey
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
KeyParamsContent001,
|
||||
KeyParamsContent002,
|
||||
KeyParamsContent003,
|
||||
KeyParamsContent004,
|
||||
AnyKeyParamsContent,
|
||||
ProtocolVersion,
|
||||
KeyParamsOrigination,
|
||||
} from '@standardnotes/common'
|
||||
import { RootKeyParamsInterface } from '@standardnotes/models'
|
||||
import { pickByCopy } from '@standardnotes/utils'
|
||||
import { ProtocolVersionForKeyParams } from './ProtocolVersionForKeyParams'
|
||||
import { ValidKeyParamsKeys } from './ValidKeyParamsKeys'
|
||||
|
||||
export class SNRootKeyParams implements RootKeyParamsInterface {
|
||||
public readonly content: AnyKeyParamsContent
|
||||
|
||||
constructor(content: AnyKeyParamsContent) {
|
||||
this.content = {
|
||||
...content,
|
||||
origination: content.origination || KeyParamsOrigination.Registration,
|
||||
version: content.version || ProtocolVersionForKeyParams(content),
|
||||
}
|
||||
}
|
||||
|
||||
get isKeyParamsObject(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
get identifier(): string {
|
||||
return this.content004.identifier || this.content002.email
|
||||
}
|
||||
|
||||
get version(): ProtocolVersion {
|
||||
return this.content.version
|
||||
}
|
||||
|
||||
get origination(): KeyParamsOrigination | undefined {
|
||||
return this.content.origination
|
||||
}
|
||||
|
||||
get content001(): KeyParamsContent001 {
|
||||
return this.content as KeyParamsContent001
|
||||
}
|
||||
|
||||
get content002(): KeyParamsContent002 {
|
||||
return this.content as KeyParamsContent002
|
||||
}
|
||||
|
||||
get content003(): KeyParamsContent003 {
|
||||
return this.content as KeyParamsContent003
|
||||
}
|
||||
|
||||
get content004(): KeyParamsContent004 {
|
||||
return this.content as KeyParamsContent004
|
||||
}
|
||||
|
||||
get createdDate(): Date | undefined {
|
||||
if (!this.content004.created) {
|
||||
return undefined
|
||||
}
|
||||
return new Date(Number(this.content004.created))
|
||||
}
|
||||
|
||||
compare(other: SNRootKeyParams): boolean {
|
||||
if (this.version !== other.version) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ([ProtocolVersion.V004, ProtocolVersion.V003].includes(this.version)) {
|
||||
return this.identifier === other.identifier && this.content004.pw_nonce === other.content003.pw_nonce
|
||||
} else if ([ProtocolVersion.V002, ProtocolVersion.V001].includes(this.version)) {
|
||||
return this.identifier === other.identifier && this.content002.pw_salt === other.content001.pw_salt
|
||||
} else {
|
||||
throw Error('Unhandled version in KeyParams.compare')
|
||||
}
|
||||
}
|
||||
|
||||
getPortableValue(): AnyKeyParamsContent {
|
||||
return pickByCopy(this.content, ValidKeyParamsKeys as (keyof AnyKeyParamsContent)[])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AllKeyParamsContents } from './KeyParamsFunctions'
|
||||
|
||||
export const ValidKeyParamsKeys: (keyof AllKeyParamsContents)[] = [
|
||||
'identifier',
|
||||
'pw_cost',
|
||||
'pw_nonce',
|
||||
'pw_salt',
|
||||
'version',
|
||||
'origination',
|
||||
'created',
|
||||
]
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayloadInterface,
|
||||
isDecryptedPayload,
|
||||
ItemsKeyContent,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import {
|
||||
ChallengePrompt,
|
||||
ChallengeReason,
|
||||
ChallengeServiceInterface,
|
||||
ChallengeValidation,
|
||||
} from '@standardnotes/services'
|
||||
import { EncryptionProvider } from '../../Service/Encryption/EncryptionProvider'
|
||||
import { SNRootKeyParams } from '../RootKey/RootKeyParams'
|
||||
import { KeyRecoveryStrings } from './KeyRecoveryStrings'
|
||||
|
||||
export async function DecryptItemsKeyWithUserFallback(
|
||||
itemsKey: EncryptedPayloadInterface,
|
||||
encryptor: EncryptionProvider,
|
||||
challengor: ChallengeServiceInterface,
|
||||
): Promise<DecryptedPayloadInterface<ItemsKeyContent> | 'failed' | 'aborted'> {
|
||||
const decryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
|
||||
usesRootKeyWithKeyLookup: {
|
||||
items: [itemsKey],
|
||||
},
|
||||
})
|
||||
|
||||
if (isDecryptedPayload(decryptionResult)) {
|
||||
return decryptionResult
|
||||
}
|
||||
|
||||
const secondDecryptionResult = await DecryptItemsKeyByPromptingUser(itemsKey, encryptor, challengor)
|
||||
|
||||
if (secondDecryptionResult === 'aborted' || secondDecryptionResult === 'failed') {
|
||||
return secondDecryptionResult
|
||||
}
|
||||
|
||||
return secondDecryptionResult.decryptedKey
|
||||
}
|
||||
|
||||
export async function DecryptItemsKeyByPromptingUser(
|
||||
itemsKey: EncryptedPayloadInterface,
|
||||
encryptor: EncryptionProvider,
|
||||
challengor: ChallengeServiceInterface,
|
||||
keyParams?: SNRootKeyParams,
|
||||
): Promise<
|
||||
| {
|
||||
decryptedKey: DecryptedPayloadInterface<ItemsKeyContent>
|
||||
rootKey: RootKeyInterface
|
||||
}
|
||||
| 'failed'
|
||||
| 'aborted'
|
||||
> {
|
||||
if (!keyParams) {
|
||||
keyParams = encryptor.getKeyEmbeddedKeyParams(itemsKey)
|
||||
}
|
||||
|
||||
if (!keyParams) {
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
const challenge = challengor.createChallenge(
|
||||
[new ChallengePrompt(ChallengeValidation.None, undefined, undefined, true)],
|
||||
ChallengeReason.Custom,
|
||||
true,
|
||||
KeyRecoveryStrings.KeyRecoveryLoginFlowPrompt(keyParams),
|
||||
KeyRecoveryStrings.KeyRecoveryPasswordRequired,
|
||||
)
|
||||
|
||||
const response = await challengor.promptForChallengeResponse(challenge)
|
||||
|
||||
if (!response) {
|
||||
return 'aborted'
|
||||
}
|
||||
|
||||
const password = response.values[0].value as string
|
||||
|
||||
const rootKey = await encryptor.computeRootKey(password, keyParams)
|
||||
|
||||
const secondDecryptionResult = await encryptor.decryptSplitSingle<ItemsKeyContent>({
|
||||
usesRootKey: {
|
||||
items: [itemsKey],
|
||||
key: rootKey,
|
||||
},
|
||||
})
|
||||
|
||||
challengor.completeChallenge(challenge)
|
||||
|
||||
if (isDecryptedPayload(secondDecryptionResult)) {
|
||||
return { decryptedKey: secondDecryptionResult, rootKey }
|
||||
}
|
||||
|
||||
return 'failed'
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { KeyParamsOrigination } from '@standardnotes/common'
|
||||
import { SNRootKeyParams } from '../RootKey/RootKeyParams'
|
||||
|
||||
export const KeyRecoveryStrings = {
|
||||
KeyRecoveryLoginFlowPrompt: (keyParams: SNRootKeyParams) => {
|
||||
const dateString = keyParams.createdDate?.toLocaleString()
|
||||
switch (keyParams.origination) {
|
||||
case KeyParamsOrigination.EmailChange:
|
||||
return `Enter your account password as it was when you changed your email on ${dateString}.`
|
||||
case KeyParamsOrigination.PasswordChange:
|
||||
return `Enter your account password after it was changed on ${dateString}.`
|
||||
case KeyParamsOrigination.Registration:
|
||||
return `Enter your account password as it was when you registered ${dateString}.`
|
||||
case KeyParamsOrigination.ProtocolUpgrade:
|
||||
return `Enter your account password as it was when you upgraded your encryption version on ${dateString}.`
|
||||
case KeyParamsOrigination.PasscodeChange:
|
||||
return `Enter your application passcode after it was changed on ${dateString}.`
|
||||
case KeyParamsOrigination.PasscodeCreate:
|
||||
return `Enter your application passcode as it was when you created it on ${dateString}.`
|
||||
default:
|
||||
throw Error('Unhandled KeyParamsOrigination case for KeyRecoveryLoginFlowPrompt')
|
||||
}
|
||||
},
|
||||
KeyRecoveryLoginFlowReason: 'Your account password is required to revalidate your session.',
|
||||
KeyRecoveryLoginFlowInvalidPassword: 'Incorrect credentials entered. Please try again.',
|
||||
KeyRecoveryRootKeyReplaced: 'Your credentials have successfully been updated.',
|
||||
KeyRecoveryPasscodeRequiredTitle: 'Passcode Required',
|
||||
KeyRecoveryPasscodeRequiredText: 'You must enter your passcode in order to save your new credentials.',
|
||||
KeyRecoveryPasswordRequired: 'Your account password is required to recover an encryption key.',
|
||||
KeyRecoveryKeyRecovered: 'Your key has successfully been recovered.',
|
||||
KeyRecoveryUnableToRecover: 'Unable to recover your key with the attempted password. Please try again.',
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion, ProtocolVersionLength } from '@standardnotes/common'
|
||||
import { Create001KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { firstHalfOfString, secondHalfOfString, splitString, UuidGenerator } from '@standardnotes/utils'
|
||||
import { AsynchronousOperator } from '../Operator'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyInterface,
|
||||
FillItemContent,
|
||||
ItemContent,
|
||||
DecryptedPayload,
|
||||
DecryptedPayloadInterface,
|
||||
PayloadTimestampDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { V001Algorithm } from '../../Algorithm'
|
||||
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
|
||||
import { isItemsKey } from '../../Keys/ItemsKey/ItemsKey'
|
||||
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
|
||||
const NO_IV = '00000000000000000000000000000000'
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* A legacy operator no longer used to generate new accounts
|
||||
*/
|
||||
export class SNProtocolOperator001 implements AsynchronousOperator {
|
||||
protected readonly crypto: PureCryptoInterface
|
||||
|
||||
constructor(crypto: PureCryptoInterface) {
|
||||
this.crypto = crypto
|
||||
}
|
||||
|
||||
public getEncryptionDisplayName(): string {
|
||||
return 'AES-256'
|
||||
}
|
||||
|
||||
get version(): ProtocolVersion {
|
||||
return ProtocolVersion.V001
|
||||
}
|
||||
|
||||
protected generateNewItemsKeyContent(): ItemsKeyContent {
|
||||
const keyLength = V001Algorithm.EncryptionKeyLength
|
||||
const itemsKey = this.crypto.generateRandomKey(keyLength)
|
||||
const response = FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: itemsKey,
|
||||
version: ProtocolVersion.V001,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption.
|
||||
* The consumer must save/sync this item.
|
||||
*/
|
||||
public createItemsKey(): ItemsKeyInterface {
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
|
||||
public async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
): Promise<SNRootKey> {
|
||||
const pwCost = V001Algorithm.PbkdfMinCost as number
|
||||
const pwNonce = this.crypto.generateRandomKey(V001Algorithm.SaltSeedLength)
|
||||
const pwSalt = await this.crypto.unsafeSha1(identifier + 'SN' + pwNonce)
|
||||
|
||||
const keyParams = Create001KeyParams({
|
||||
email: identifier,
|
||||
pw_cost: pwCost,
|
||||
pw_nonce: pwNonce,
|
||||
pw_salt: pwSalt,
|
||||
version: ProtocolVersion.V001,
|
||||
origination,
|
||||
created: `${Date.now()}`,
|
||||
})
|
||||
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
public getPayloadAuthenticatedData(
|
||||
_encrypted: EncryptedParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
private async decryptString(ciphertext: string, key: string) {
|
||||
return this.crypto.aes256CbcDecrypt(ciphertext, NO_IV, key)
|
||||
}
|
||||
|
||||
private async encryptString(text: string, key: string) {
|
||||
return this.crypto.aes256CbcEncrypt(text, NO_IV, key)
|
||||
}
|
||||
|
||||
public async generateEncryptedParametersAsync(
|
||||
payload: DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): Promise<EncryptedParameters> {
|
||||
/**
|
||||
* Generate new item key that is double the key size.
|
||||
* Will be split to create encryption key and authentication key.
|
||||
*/
|
||||
const itemKey = this.crypto.generateRandomKey(V001Algorithm.EncryptionKeyLength * 2)
|
||||
const encItemKey = await this.encryptString(itemKey, key.itemsKey)
|
||||
|
||||
/** Encrypt content */
|
||||
const ek = firstHalfOfString(itemKey)
|
||||
const ak = secondHalfOfString(itemKey)
|
||||
const contentCiphertext = await this.encryptString(JSON.stringify(payload.content), ek)
|
||||
const ciphertext = key.keyVersion + contentCiphertext
|
||||
const authHash = await this.crypto.hmac256(ciphertext, ak)
|
||||
|
||||
if (!authHash) {
|
||||
throw Error('Error generating hmac256 authHash')
|
||||
}
|
||||
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
items_key_id: isItemsKey(key) ? key.uuid : undefined,
|
||||
content: ciphertext,
|
||||
enc_item_key: encItemKey,
|
||||
auth_hash: authHash,
|
||||
version: this.version,
|
||||
}
|
||||
}
|
||||
|
||||
public async generateDecryptedParametersAsync<C extends ItemContent = ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
if (!encrypted.enc_item_key) {
|
||||
console.error(Error('Missing item encryption key, skipping decryption.'))
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
let encryptedItemKey = encrypted.enc_item_key
|
||||
encryptedItemKey = this.version + encryptedItemKey
|
||||
const itemKeyComponents = this.encryptionComponentsFromString(encryptedItemKey, key.itemsKey)
|
||||
|
||||
const itemKey = await this.decryptString(itemKeyComponents.ciphertext, itemKeyComponents.key)
|
||||
if (!itemKey) {
|
||||
console.error('Error decrypting parameters', encrypted)
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
const ek = firstHalfOfString(itemKey)
|
||||
const itemParams = this.encryptionComponentsFromString(encrypted.content, ek)
|
||||
const content = await this.decryptString(itemParams.ciphertext, itemParams.key)
|
||||
|
||||
if (!content) {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
content: JSON.parse(content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private encryptionComponentsFromString(string: string, encryptionKey: string) {
|
||||
const encryptionVersion = string.substring(0, ProtocolVersionLength)
|
||||
return {
|
||||
ciphertext: string.substring(ProtocolVersionLength, string.length),
|
||||
version: encryptionVersion,
|
||||
key: encryptionKey,
|
||||
}
|
||||
}
|
||||
|
||||
protected async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
const derivedKey = await this.crypto.pbkdf2(
|
||||
password,
|
||||
keyParams.content001.pw_salt,
|
||||
keyParams.content001.pw_cost,
|
||||
V001Algorithm.PbkdfOutputLength,
|
||||
)
|
||||
|
||||
if (!derivedKey) {
|
||||
throw Error('Error deriving PBKDF2 key')
|
||||
}
|
||||
|
||||
const partitions = splitString(derivedKey, 2)
|
||||
|
||||
return CreateNewRootKey({
|
||||
serverPassword: partitions[0],
|
||||
masterKey: partitions[1],
|
||||
version: ProtocolVersion.V001,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import { Create002KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { SNProtocolOperator001 } from '../001/Operator001'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import { V002Algorithm } from '../../Algorithm'
|
||||
import * as Common from '@standardnotes/common'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as Utils from '@standardnotes/utils'
|
||||
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
|
||||
import { isItemsKey } from '../../Keys/ItemsKey/ItemsKey'
|
||||
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
import { ItemContent, PayloadTimestampDefaults } from '@standardnotes/models'
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* A legacy operator no longer used to generate new accounts.
|
||||
*/
|
||||
export class SNProtocolOperator002 extends SNProtocolOperator001 {
|
||||
override get version(): Common.ProtocolVersion {
|
||||
return Common.ProtocolVersion.V002
|
||||
}
|
||||
|
||||
protected override generateNewItemsKeyContent(): Models.ItemsKeyContent {
|
||||
const keyLength = V002Algorithm.EncryptionKeyLength
|
||||
const itemsKey = this.crypto.generateRandomKey(keyLength)
|
||||
const authKey = this.crypto.generateRandomKey(keyLength)
|
||||
const response = Models.FillItemContent<Models.ItemsKeyContent>({
|
||||
itemsKey: itemsKey,
|
||||
dataAuthenticationKey: authKey,
|
||||
version: Common.ProtocolVersion.V002,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption.
|
||||
* The consumer must save/sync this item.
|
||||
*/
|
||||
public override createItemsKey(): Models.ItemsKeyInterface {
|
||||
const payload = new Models.DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: Common.ContentType.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return Models.CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
|
||||
public override async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: Common.KeyParamsOrigination,
|
||||
): Promise<SNRootKey> {
|
||||
const pwCost = Utils.lastElement(V002Algorithm.PbkdfCostsUsed) as number
|
||||
const pwNonce = this.crypto.generateRandomKey(V002Algorithm.SaltSeedLength)
|
||||
const pwSalt = await this.crypto.unsafeSha1(identifier + ':' + pwNonce)
|
||||
|
||||
const keyParams = Create002KeyParams({
|
||||
email: identifier,
|
||||
pw_nonce: pwNonce,
|
||||
pw_cost: pwCost,
|
||||
pw_salt: pwSalt,
|
||||
version: Common.ProtocolVersion.V002,
|
||||
origination,
|
||||
created: `${Date.now()}`,
|
||||
})
|
||||
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that version 002 supported "dynamic" iteration counts. Some accounts
|
||||
* may have had costs of 5000, and others of 101000. Therefore, when computing
|
||||
* the root key, we must use the value returned by the server.
|
||||
*/
|
||||
public override async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
private async decryptString002(text: string, key: string, iv: string) {
|
||||
return this.crypto.aes256CbcDecrypt(text, iv, key)
|
||||
}
|
||||
|
||||
private async encryptString002(text: string, key: string, iv: string) {
|
||||
return this.crypto.aes256CbcEncrypt(text, iv, key)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keyParams Supplied only when encrypting an items key
|
||||
*/
|
||||
private async encryptTextParams(
|
||||
string: string,
|
||||
encryptionKey: string,
|
||||
authKey: string,
|
||||
uuid: string,
|
||||
version: Common.ProtocolVersion,
|
||||
keyParams?: SNRootKeyParams,
|
||||
) {
|
||||
const iv = this.crypto.generateRandomKey(V002Algorithm.EncryptionIvLength)
|
||||
const contentCiphertext = await this.encryptString002(string, encryptionKey, iv)
|
||||
const ciphertextToAuth = [version, uuid, iv, contentCiphertext].join(':')
|
||||
const authHash = await this.crypto.hmac256(ciphertextToAuth, authKey)
|
||||
|
||||
if (!authHash) {
|
||||
throw Error('Error generating hmac256 authHash')
|
||||
}
|
||||
|
||||
const components: string[] = [version as string, authHash, uuid, iv, contentCiphertext]
|
||||
if (keyParams) {
|
||||
const keyParamsString = this.crypto.base64Encode(JSON.stringify(keyParams.content))
|
||||
components.push(keyParamsString)
|
||||
}
|
||||
const fullCiphertext = components.join(':')
|
||||
return fullCiphertext
|
||||
}
|
||||
|
||||
private async decryptTextParams(
|
||||
ciphertextToAuth: string,
|
||||
contentCiphertext: string,
|
||||
encryptionKey: string,
|
||||
iv: string,
|
||||
authHash: string,
|
||||
authKey: string,
|
||||
) {
|
||||
if (!encryptionKey) {
|
||||
throw 'Attempting to decryptTextParams with null encryptionKey'
|
||||
}
|
||||
const localAuthHash = await this.crypto.hmac256(ciphertextToAuth, authKey)
|
||||
if (!localAuthHash) {
|
||||
throw Error('Error generating hmac256 localAuthHash')
|
||||
}
|
||||
|
||||
if (this.crypto.timingSafeEqual(authHash, localAuthHash) === false) {
|
||||
console.error(Error('Auth hash does not match.'))
|
||||
return null
|
||||
}
|
||||
return this.decryptString002(contentCiphertext, encryptionKey, iv)
|
||||
}
|
||||
|
||||
public override getPayloadAuthenticatedData(
|
||||
encrypted: EncryptedParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
const itemKeyComponents = this.encryptionComponentsFromString002(encrypted.enc_item_key)
|
||||
const authenticatedData = itemKeyComponents.keyParams
|
||||
|
||||
if (!authenticatedData) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const decoded = JSON.parse(this.crypto.base64Decode(authenticatedData))
|
||||
const data: LegacyAttachedData = {
|
||||
...(decoded as Common.AnyKeyParamsContent),
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
public override async generateEncryptedParametersAsync(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: Models.ItemsKeyInterface | SNRootKey,
|
||||
): Promise<EncryptedParameters> {
|
||||
/**
|
||||
* Generate new item key that is double the key size.
|
||||
* Will be split to create encryption key and authentication key.
|
||||
*/
|
||||
const itemKey = this.crypto.generateRandomKey(V002Algorithm.EncryptionKeyLength * 2)
|
||||
const encItemKey = await this.encryptTextParams(
|
||||
itemKey,
|
||||
key.itemsKey,
|
||||
key.dataAuthenticationKey as string,
|
||||
payload.uuid,
|
||||
key.keyVersion,
|
||||
key instanceof SNRootKey ? (key as SNRootKey).keyParams : undefined,
|
||||
)
|
||||
|
||||
const ek = Utils.firstHalfOfString(itemKey)
|
||||
const ak = Utils.secondHalfOfString(itemKey)
|
||||
const ciphertext = await this.encryptTextParams(
|
||||
JSON.stringify(payload.content),
|
||||
ek,
|
||||
ak,
|
||||
payload.uuid,
|
||||
key.keyVersion,
|
||||
key instanceof SNRootKey ? (key as SNRootKey).keyParams : undefined,
|
||||
)
|
||||
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
items_key_id: isItemsKey(key) ? key.uuid : undefined,
|
||||
content: ciphertext,
|
||||
enc_item_key: encItemKey,
|
||||
version: this.version,
|
||||
}
|
||||
}
|
||||
|
||||
public override async generateDecryptedParametersAsync<C extends ItemContent = ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: Models.ItemsKeyInterface | SNRootKey,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
if (!encrypted.enc_item_key) {
|
||||
console.error(Error('Missing item encryption key, skipping decryption.'))
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
const encryptedItemKey = encrypted.enc_item_key
|
||||
const itemKeyComponents = this.encryptionComponentsFromString002(
|
||||
encryptedItemKey,
|
||||
key.itemsKey,
|
||||
key.dataAuthenticationKey,
|
||||
)
|
||||
|
||||
const itemKey = await this.decryptTextParams(
|
||||
itemKeyComponents.ciphertextToAuth,
|
||||
itemKeyComponents.contentCiphertext,
|
||||
itemKeyComponents.encryptionKey as string,
|
||||
itemKeyComponents.iv,
|
||||
itemKeyComponents.authHash,
|
||||
itemKeyComponents.authKey as string,
|
||||
)
|
||||
if (!itemKey) {
|
||||
console.error('Error decrypting item_key parameters', encrypted)
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
const ek = Utils.firstHalfOfString(itemKey)
|
||||
const ak = Utils.secondHalfOfString(itemKey)
|
||||
const itemParams = this.encryptionComponentsFromString002(encrypted.content, ek, ak)
|
||||
const content = await this.decryptTextParams(
|
||||
itemParams.ciphertextToAuth,
|
||||
itemParams.contentCiphertext,
|
||||
itemParams.encryptionKey as string,
|
||||
itemParams.iv,
|
||||
itemParams.authHash,
|
||||
itemParams.authKey as string,
|
||||
)
|
||||
|
||||
if (!content) {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
content: JSON.parse(content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
const derivedKey = await this.crypto.pbkdf2(
|
||||
password,
|
||||
keyParams.content002.pw_salt,
|
||||
keyParams.content002.pw_cost,
|
||||
V002Algorithm.PbkdfOutputLength,
|
||||
)
|
||||
|
||||
if (!derivedKey) {
|
||||
throw Error('Error deriving PBKDF2 key')
|
||||
}
|
||||
|
||||
const partitions = Utils.splitString(derivedKey, 3)
|
||||
|
||||
return CreateNewRootKey({
|
||||
serverPassword: partitions[0],
|
||||
masterKey: partitions[1],
|
||||
dataAuthenticationKey: partitions[2],
|
||||
version: Common.ProtocolVersion.V002,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
|
||||
private encryptionComponentsFromString002(string: string, encryptionKey?: string, authKey?: string) {
|
||||
const components = string.split(':')
|
||||
return {
|
||||
encryptionVersion: components[0],
|
||||
authHash: components[1],
|
||||
uuid: components[2],
|
||||
iv: components[3],
|
||||
contentCiphertext: components[4],
|
||||
keyParams: components[5],
|
||||
ciphertextToAuth: [components[0], components[2], components[3], components[4]].join(':'),
|
||||
encryptionKey: encryptionKey,
|
||||
authKey: authKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { splitString, UuidGenerator } from '@standardnotes/utils'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
DecryptedPayload,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyInterface,
|
||||
FillItemContent,
|
||||
PayloadTimestampDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { V003Algorithm } from '../../Algorithm'
|
||||
import { Create003KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { SNProtocolOperator002 } from '../002/Operator002'
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
|
||||
/**
|
||||
* @legacy
|
||||
* Non-expired operator but no longer used for generating new accounts.
|
||||
* This operator subclasses the 002 operator to share functionality that has not
|
||||
* changed, and overrides functions where behavior may differ.
|
||||
*/
|
||||
export class SNProtocolOperator003 extends SNProtocolOperator002 {
|
||||
override get version(): ProtocolVersion {
|
||||
return ProtocolVersion.V003
|
||||
}
|
||||
|
||||
protected override generateNewItemsKeyContent(): ItemsKeyContent {
|
||||
const keyLength = V003Algorithm.EncryptionKeyLength
|
||||
const itemsKey = this.crypto.generateRandomKey(keyLength)
|
||||
const authKey = this.crypto.generateRandomKey(keyLength)
|
||||
const response = FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: itemsKey,
|
||||
dataAuthenticationKey: authKey,
|
||||
version: ProtocolVersion.V003,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption.
|
||||
* The consumer must save/sync this item.
|
||||
*/
|
||||
public override createItemsKey(): ItemsKeyInterface {
|
||||
const content = this.generateNewItemsKeyContent()
|
||||
const payload = new DecryptedPayload({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: FillItemContent(content),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
|
||||
public override async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
protected override async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
const salt = await this.generateSalt(
|
||||
keyParams.content003.identifier,
|
||||
ProtocolVersion.V003,
|
||||
V003Algorithm.PbkdfCost,
|
||||
keyParams.content003.pw_nonce,
|
||||
)
|
||||
|
||||
const derivedKey = await this.crypto.pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
V003Algorithm.PbkdfCost,
|
||||
V003Algorithm.PbkdfOutputLength,
|
||||
)
|
||||
|
||||
if (!derivedKey) {
|
||||
throw Error('Error deriving PBKDF2 key')
|
||||
}
|
||||
|
||||
const partitions = splitString(derivedKey, 3)
|
||||
|
||||
return CreateNewRootKey({
|
||||
serverPassword: partitions[0],
|
||||
masterKey: partitions[1],
|
||||
dataAuthenticationKey: partitions[2],
|
||||
version: ProtocolVersion.V003,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
|
||||
public override async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
): Promise<SNRootKey> {
|
||||
const version = ProtocolVersion.V003
|
||||
const pwNonce = this.crypto.generateRandomKey(V003Algorithm.SaltSeedLength)
|
||||
const keyParams = Create003KeyParams({
|
||||
identifier: identifier,
|
||||
pw_nonce: pwNonce,
|
||||
version: version,
|
||||
origination: origination,
|
||||
created: `${Date.now()}`,
|
||||
})
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
private async generateSalt(identifier: string, version: ProtocolVersion, cost: number, nonce: string) {
|
||||
const result = await this.crypto.sha256([identifier, 'SF', version, cost, nonce].join(':'))
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { DecryptedPayload, ItemContent, ItemsKeyContent, PayloadTimestampDefaults } from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNItemsKey } from '../../Keys/ItemsKey/ItemsKey'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { SNProtocolOperator004 } from './Operator004'
|
||||
|
||||
const b64 = (text: string): string => {
|
||||
return Buffer.from(text).toString('base64')
|
||||
}
|
||||
|
||||
describe('operator 004', () => {
|
||||
let crypto: PureCryptoInterface
|
||||
let operator: SNProtocolOperator004
|
||||
|
||||
beforeEach(() => {
|
||||
crypto = {} as jest.Mocked<PureCryptoInterface>
|
||||
crypto.base64Encode = jest.fn().mockImplementation((text: string) => {
|
||||
return b64(text)
|
||||
})
|
||||
crypto.base64Decode = jest.fn().mockImplementation((text: string) => {
|
||||
return Buffer.from(text, 'base64').toString('ascii')
|
||||
})
|
||||
crypto.xchacha20Encrypt = jest.fn().mockImplementation((text: string) => {
|
||||
return `<e>${text}<e>`
|
||||
})
|
||||
crypto.xchacha20Decrypt = jest.fn().mockImplementation((text: string) => {
|
||||
return text.split('<e>')[1]
|
||||
})
|
||||
crypto.generateRandomKey = jest.fn().mockImplementation(() => {
|
||||
return 'random-string'
|
||||
})
|
||||
|
||||
operator = new SNProtocolOperator004(crypto)
|
||||
})
|
||||
|
||||
it('should generateEncryptedProtocolString', () => {
|
||||
const aad: ItemAuthenticatedData = {
|
||||
u: '123',
|
||||
v: ProtocolVersion.V004,
|
||||
}
|
||||
|
||||
const nonce = 'noncy'
|
||||
const plaintext = 'foo'
|
||||
|
||||
operator.generateEncryptionNonce = jest.fn().mockReturnValue(nonce)
|
||||
|
||||
const result = operator.generateEncryptedProtocolString(plaintext, 'secret', aad)
|
||||
|
||||
expect(result).toEqual(`004:${nonce}:<e>${plaintext}<e>:${b64(JSON.stringify(aad))}`)
|
||||
})
|
||||
|
||||
it('should deconstructEncryptedPayloadString', () => {
|
||||
const string = '004:noncy:<e>foo<e>:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9'
|
||||
|
||||
const result = operator.deconstructEncryptedPayloadString(string)
|
||||
|
||||
expect(result).toEqual({
|
||||
version: '004',
|
||||
nonce: 'noncy',
|
||||
ciphertext: '<e>foo<e>',
|
||||
authenticatedData: 'eyJ1IjoiMTIzIiwidiI6IjAwNCJ9',
|
||||
})
|
||||
})
|
||||
|
||||
it('should generateEncryptedParametersSync', () => {
|
||||
const payload = {
|
||||
uuid: '123',
|
||||
content_type: ContentType.Note,
|
||||
content: { foo: 'bar' } as unknown as jest.Mocked<ItemContent>,
|
||||
...PayloadTimestampDefaults(),
|
||||
} as jest.Mocked<DecryptedPayload>
|
||||
|
||||
const key = new SNItemsKey(
|
||||
new DecryptedPayload<ItemsKeyContent>({
|
||||
uuid: 'key-456',
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: {
|
||||
itemsKey: 'secret',
|
||||
version: ProtocolVersion.V004,
|
||||
} as jest.Mocked<ItemsKeyContent>,
|
||||
...PayloadTimestampDefaults(),
|
||||
}),
|
||||
)
|
||||
|
||||
const result = operator.generateEncryptedParametersSync(payload, key)
|
||||
|
||||
expect(result).toEqual({
|
||||
uuid: '123',
|
||||
items_key_id: 'key-456',
|
||||
content: '004:random-string:<e>{"foo":"bar"}<e>:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9',
|
||||
enc_item_key: '004:random-string:<e>random-string<e>:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9',
|
||||
version: '004',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,319 @@
|
||||
import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common'
|
||||
import { Create004KeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { SynchronousOperator } from '../Operator'
|
||||
import {
|
||||
CreateDecryptedItemFromPayload,
|
||||
FillItemContent,
|
||||
ItemContent,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyInterface,
|
||||
PayloadTimestampDefaults,
|
||||
} from '@standardnotes/models'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { V004Algorithm } from '../../Algorithm'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as Utils from '@standardnotes/utils'
|
||||
import { ContentTypeUsesRootKeyEncryption, CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
|
||||
import { isItemsKey } from '../../Keys/ItemsKey/ItemsKey'
|
||||
|
||||
type V004StringComponents = [version: string, nonce: string, ciphertext: string, authenticatedData: string]
|
||||
|
||||
type V004Components = {
|
||||
version: V004StringComponents[0]
|
||||
nonce: V004StringComponents[1]
|
||||
ciphertext: V004StringComponents[2]
|
||||
authenticatedData: V004StringComponents[3]
|
||||
}
|
||||
|
||||
const PARTITION_CHARACTER = ':'
|
||||
|
||||
export class SNProtocolOperator004 implements SynchronousOperator {
|
||||
protected readonly crypto: PureCryptoInterface
|
||||
|
||||
constructor(crypto: PureCryptoInterface) {
|
||||
this.crypto = crypto
|
||||
}
|
||||
|
||||
public getEncryptionDisplayName(): string {
|
||||
return 'XChaCha20-Poly1305'
|
||||
}
|
||||
|
||||
get version(): ProtocolVersion {
|
||||
return ProtocolVersion.V004
|
||||
}
|
||||
|
||||
private generateNewItemsKeyContent() {
|
||||
const itemsKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength)
|
||||
const response = FillItemContent<ItemsKeyContent>({
|
||||
itemsKey: itemsKey,
|
||||
version: ProtocolVersion.V004,
|
||||
})
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption.
|
||||
* The consumer must save/sync this item.
|
||||
*/
|
||||
public createItemsKey(): ItemsKeyInterface {
|
||||
const payload = new Models.DecryptedPayload({
|
||||
uuid: Utils.UuidGenerator.GenerateUuid(),
|
||||
content_type: ContentType.ItemsKey,
|
||||
content: this.generateNewItemsKeyContent(),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
return CreateDecryptedItemFromPayload(payload)
|
||||
}
|
||||
|
||||
/**
|
||||
* We require both a client-side component and a server-side component in generating a
|
||||
* salt. This way, a comprimised server cannot benefit from sending the same seed value
|
||||
* for every user. We mix a client-controlled value that is globally unique
|
||||
* (their identifier), with a server controlled value to produce a salt for our KDF.
|
||||
* @param identifier
|
||||
* @param seed
|
||||
*/
|
||||
private async generateSalt004(identifier: string, seed: string) {
|
||||
const hash = await this.crypto.sha256([identifier, seed].join(PARTITION_CHARACTER))
|
||||
return Utils.truncateHexString(hash, V004Algorithm.ArgonSaltLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a root key given a passworf
|
||||
* qwd and previous keyParams
|
||||
* @param password - Plain string representing raw user password
|
||||
* @param keyParams - KeyParams object
|
||||
*/
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new root key given an identifier and a user password
|
||||
* @param identifier - Plain string representing a unique identifier
|
||||
* @param password - Plain string representing raw user password
|
||||
*/
|
||||
public async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: KeyParamsOrigination,
|
||||
): Promise<SNRootKey> {
|
||||
const version = ProtocolVersion.V004
|
||||
const seed = this.crypto.generateRandomKey(V004Algorithm.ArgonSaltSeedLength)
|
||||
const keyParams = Create004KeyParams({
|
||||
identifier: identifier,
|
||||
pw_nonce: seed,
|
||||
version: version,
|
||||
origination: origination,
|
||||
created: `${Date.now()}`,
|
||||
})
|
||||
return this.deriveKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param plaintext - The plaintext to encrypt.
|
||||
* @param rawKey - The key to use to encrypt the plaintext.
|
||||
* @param nonce - The nonce for encryption.
|
||||
* @param authenticatedData - JavaScript object (will be stringified) representing
|
||||
'Additional authenticated data': data you want to be included in authentication.
|
||||
*/
|
||||
encryptString004(plaintext: string, rawKey: string, nonce: string, authenticatedData: ItemAuthenticatedData) {
|
||||
if (!nonce) {
|
||||
throw 'encryptString null nonce'
|
||||
}
|
||||
if (!rawKey) {
|
||||
throw 'encryptString null rawKey'
|
||||
}
|
||||
return this.crypto.xchacha20Encrypt(plaintext, nonce, rawKey, this.authenticatedDataToString(authenticatedData))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ciphertext The encrypted text to decrypt.
|
||||
* @param rawKey The key to use to decrypt the ciphertext.
|
||||
* @param nonce The nonce for decryption.
|
||||
* @param rawAuthenticatedData String representing
|
||||
'Additional authenticated data' - data you want to be included in authentication.
|
||||
*/
|
||||
private decryptString004(ciphertext: string, rawKey: string, nonce: string, rawAuthenticatedData: string) {
|
||||
return this.crypto.xchacha20Decrypt(ciphertext, nonce, rawKey, rawAuthenticatedData)
|
||||
}
|
||||
|
||||
generateEncryptionNonce(): string {
|
||||
return this.crypto.generateRandomKey(V004Algorithm.EncryptionNonceLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param plaintext The plaintext text to decrypt.
|
||||
* @param rawKey The key to use to encrypt the plaintext.
|
||||
*/
|
||||
generateEncryptedProtocolString(plaintext: string, rawKey: string, authenticatedData: ItemAuthenticatedData) {
|
||||
const nonce = this.generateEncryptionNonce()
|
||||
|
||||
const ciphertext = this.encryptString004(plaintext, rawKey, nonce, authenticatedData)
|
||||
|
||||
const components: V004StringComponents = [
|
||||
ProtocolVersion.V004 as string,
|
||||
nonce,
|
||||
ciphertext,
|
||||
this.authenticatedDataToString(authenticatedData),
|
||||
]
|
||||
|
||||
return components.join(PARTITION_CHARACTER)
|
||||
}
|
||||
|
||||
deconstructEncryptedPayloadString(payloadString: string): V004Components {
|
||||
const components = payloadString.split(PARTITION_CHARACTER) as V004StringComponents
|
||||
|
||||
return {
|
||||
version: components[0],
|
||||
nonce: components[1],
|
||||
ciphertext: components[2],
|
||||
authenticatedData: components[3],
|
||||
}
|
||||
}
|
||||
|
||||
public getPayloadAuthenticatedData(
|
||||
encrypted: EncryptedParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
const itemKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
||||
const authenticatedDataString = itemKeyComponents.authenticatedData
|
||||
const result = this.stringToAuthenticatedData(authenticatedDataString)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* For items that are encrypted with a root key, we append the root key's key params, so
|
||||
* that in the event the client/user loses a reference to their root key, they may still
|
||||
* decrypt data by regenerating the key based on the attached key params.
|
||||
*/
|
||||
private generateAuthenticatedDataForPayload(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): ItemAuthenticatedData | RootKeyEncryptedAuthenticatedData {
|
||||
const baseData: ItemAuthenticatedData = {
|
||||
u: payload.uuid,
|
||||
v: ProtocolVersion.V004,
|
||||
}
|
||||
if (ContentTypeUsesRootKeyEncryption(payload.content_type)) {
|
||||
return {
|
||||
...baseData,
|
||||
kp: (key as SNRootKey).keyParams.content,
|
||||
}
|
||||
} else {
|
||||
if (!isItemsKey(key)) {
|
||||
throw Error('Attempting to use non-items key for regular item.')
|
||||
}
|
||||
return baseData
|
||||
}
|
||||
}
|
||||
|
||||
private authenticatedDataToString(attachedData: ItemAuthenticatedData) {
|
||||
return this.crypto.base64Encode(JSON.stringify(Utils.sortedCopy(Utils.omitUndefinedCopy(attachedData))))
|
||||
}
|
||||
|
||||
private stringToAuthenticatedData(
|
||||
rawAuthenticatedData: string,
|
||||
override?: Partial<ItemAuthenticatedData>,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData {
|
||||
const base = JSON.parse(this.crypto.base64Decode(rawAuthenticatedData))
|
||||
return Utils.sortedCopy({
|
||||
...base,
|
||||
...override,
|
||||
})
|
||||
}
|
||||
|
||||
public generateEncryptedParametersSync(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): EncryptedParameters {
|
||||
const itemKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength)
|
||||
|
||||
const contentPlaintext = JSON.stringify(payload.content)
|
||||
const authenticatedData = this.generateAuthenticatedDataForPayload(payload, key)
|
||||
const encryptedContentString = this.generateEncryptedProtocolString(contentPlaintext, itemKey, authenticatedData)
|
||||
|
||||
const encryptedItemKey = this.generateEncryptedProtocolString(itemKey, key.itemsKey, authenticatedData)
|
||||
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
items_key_id: isItemsKey(key) ? key.uuid : undefined,
|
||||
content: encryptedContentString,
|
||||
enc_item_key: encryptedItemKey,
|
||||
version: this.version,
|
||||
}
|
||||
}
|
||||
|
||||
public generateDecryptedParametersSync<C extends ItemContent = ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: ItemsKeyInterface | SNRootKey,
|
||||
): DecryptedParameters<C> | ErrorDecryptingParameters {
|
||||
const itemKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key)
|
||||
const authenticatedData = this.stringToAuthenticatedData(itemKeyComponents.authenticatedData, {
|
||||
u: encrypted.uuid,
|
||||
v: encrypted.version,
|
||||
})
|
||||
|
||||
const useAuthenticatedString = this.authenticatedDataToString(authenticatedData)
|
||||
const itemKey = this.decryptString004(
|
||||
itemKeyComponents.ciphertext,
|
||||
key.itemsKey,
|
||||
itemKeyComponents.nonce,
|
||||
useAuthenticatedString,
|
||||
)
|
||||
|
||||
if (!itemKey) {
|
||||
console.error('Error decrypting itemKey parameters', encrypted)
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
const contentComponents = this.deconstructEncryptedPayloadString(encrypted.content)
|
||||
const content = this.decryptString004(
|
||||
contentComponents.ciphertext,
|
||||
itemKey,
|
||||
contentComponents.nonce,
|
||||
useAuthenticatedString,
|
||||
)
|
||||
if (!content) {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
uuid: encrypted.uuid,
|
||||
content: JSON.parse(content),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async deriveKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey> {
|
||||
const salt = await this.generateSalt004(keyParams.content004.identifier, keyParams.content004.pw_nonce)
|
||||
const derivedKey = this.crypto.argon2(
|
||||
password,
|
||||
salt,
|
||||
V004Algorithm.ArgonIterations,
|
||||
V004Algorithm.ArgonMemLimit,
|
||||
V004Algorithm.ArgonOutputKeyBytes,
|
||||
)
|
||||
const partitions = Utils.splitString(derivedKey, 2)
|
||||
const masterKey = partitions[0]
|
||||
const serverPassword = partitions[1]
|
||||
|
||||
return CreateNewRootKey({
|
||||
masterKey,
|
||||
serverPassword,
|
||||
version: ProtocolVersion.V004,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNProtocolOperator001 } from '../Operator/001/Operator001'
|
||||
import { SNProtocolOperator002 } from '../Operator/002/Operator002'
|
||||
import { SNProtocolOperator003 } from '../Operator/003/Operator003'
|
||||
import { SNProtocolOperator004 } from '../Operator/004/Operator004'
|
||||
import { AsynchronousOperator, SynchronousOperator } from '../Operator/Operator'
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export function createOperatorForVersion(
|
||||
version: ProtocolVersion,
|
||||
crypto: PureCryptoInterface,
|
||||
): AsynchronousOperator | SynchronousOperator {
|
||||
if (version === ProtocolVersion.V001) {
|
||||
return new SNProtocolOperator001(crypto)
|
||||
} else if (version === ProtocolVersion.V002) {
|
||||
return new SNProtocolOperator002(crypto)
|
||||
} else if (version === ProtocolVersion.V003) {
|
||||
return new SNProtocolOperator003(crypto)
|
||||
} else if (version === ProtocolVersion.V004) {
|
||||
return new SNProtocolOperator004(crypto)
|
||||
} else {
|
||||
throw Error(`Unable to find operator for version ${version}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function isAsyncOperator(
|
||||
operator: AsynchronousOperator | SynchronousOperator,
|
||||
): operator is AsynchronousOperator {
|
||||
return (operator as AsynchronousOperator).generateDecryptedParametersAsync !== undefined
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ItemsKeyInterface, RootKeyInterface } from '@standardnotes/models'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import { SNRootKey } from '../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../Keys/RootKey/RootKeyParams'
|
||||
import { KeyParamsOrigination } from '@standardnotes/common'
|
||||
import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../Types/LegacyAttachedData'
|
||||
|
||||
/**w
|
||||
* An operator is responsible for performing crypto operations, such as generating keys
|
||||
* and encrypting/decrypting payloads. Operators interact directly with
|
||||
* platform dependent SNPureCrypto implementation to directly access cryptographic primitives.
|
||||
* Each operator is versioned according to the protocol version. Functions that are common
|
||||
* across all versions appear in this generic parent class.
|
||||
*/
|
||||
export interface OperatorCommon {
|
||||
createItemsKey(): ItemsKeyInterface
|
||||
/**
|
||||
* Returns encryption protocol display name
|
||||
*/
|
||||
getEncryptionDisplayName(): string
|
||||
|
||||
readonly version: string
|
||||
|
||||
/**
|
||||
* Returns the payload's authenticated data. The passed payload must be in a
|
||||
* non-decrypted, ciphertext state.
|
||||
*/
|
||||
getPayloadAuthenticatedData(
|
||||
encrypted: EncryptedParameters,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined
|
||||
|
||||
/**
|
||||
* Computes a root key given a password and previous keyParams
|
||||
* @param password - Plain string representing raw user password
|
||||
*/
|
||||
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<SNRootKey>
|
||||
|
||||
/**
|
||||
* Creates a new root key given an identifier and a user password
|
||||
* @param identifier - Plain string representing a unique identifier
|
||||
* for the user
|
||||
* @param password - Plain string representing raw user password
|
||||
*/
|
||||
createRootKey(identifier: string, password: string, origination: KeyParamsOrigination): Promise<SNRootKey>
|
||||
}
|
||||
|
||||
export interface SynchronousOperator extends OperatorCommon {
|
||||
/**
|
||||
* Converts a bare payload into an encrypted one in the desired format.
|
||||
* @param payload - The non-encrypted payload object to encrypt
|
||||
* @param key - The key to use to encrypt the payload. Can be either
|
||||
* a RootKey (when encrypting payloads that require root key encryption, such as encrypting
|
||||
* items keys), or an ItemsKey (if encrypted regular items)
|
||||
*/
|
||||
generateEncryptedParametersSync(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | RootKeyInterface,
|
||||
): EncryptedParameters
|
||||
|
||||
generateDecryptedParametersSync<C extends Models.ItemContent = Models.ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: ItemsKeyInterface | RootKeyInterface,
|
||||
): DecryptedParameters<C> | ErrorDecryptingParameters
|
||||
}
|
||||
|
||||
export interface AsynchronousOperator extends OperatorCommon {
|
||||
/**
|
||||
* Converts a bare payload into an encrypted one in the desired format.
|
||||
* @param payload - The non-encrypted payload object to encrypt
|
||||
* @param key - The key to use to encrypt the payload. Can be either
|
||||
* a RootKey (when encrypting payloads that require root key encryption, such as encrypting
|
||||
* items keys), or an ItemsKey (if encrypted regular items)
|
||||
*/
|
||||
generateEncryptedParametersAsync(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: ItemsKeyInterface | RootKeyInterface,
|
||||
): Promise<EncryptedParameters>
|
||||
|
||||
generateDecryptedParametersAsync<C extends Models.ItemContent = Models.ItemContent>(
|
||||
encrypted: EncryptedParameters,
|
||||
key: ItemsKeyInterface | RootKeyInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters>
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { ProtocolVersion, ProtocolVersionLatest } from '@standardnotes/common'
|
||||
import { createOperatorForVersion } from './Functions'
|
||||
import { AsynchronousOperator, SynchronousOperator } from './Operator'
|
||||
|
||||
export class OperatorManager {
|
||||
private operators: Record<string, AsynchronousOperator | SynchronousOperator> = {}
|
||||
|
||||
constructor(private crypto: PureCryptoInterface) {
|
||||
this.crypto = crypto
|
||||
}
|
||||
|
||||
public deinit(): void {
|
||||
;(this.crypto as unknown) = undefined
|
||||
this.operators = {}
|
||||
}
|
||||
|
||||
public operatorForVersion(version: ProtocolVersion): SynchronousOperator | AsynchronousOperator {
|
||||
const operatorKey = version
|
||||
let operator = this.operators[operatorKey]
|
||||
if (!operator) {
|
||||
operator = createOperatorForVersion(version, this.crypto)
|
||||
this.operators[operatorKey] = operator
|
||||
}
|
||||
return operator
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operator corresponding to the latest protocol version
|
||||
*/
|
||||
public defaultOperator(): SynchronousOperator | AsynchronousOperator {
|
||||
return this.operatorForVersion(ProtocolVersionLatest)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { isAsyncOperator } from './Functions'
|
||||
import { OperatorManager } from './OperatorManager'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import {
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
encryptedParametersFromPayload,
|
||||
ErrorDecryptingParameters,
|
||||
} from '../Types/EncryptedParameters'
|
||||
|
||||
export async function encryptPayload(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: Models.ItemsKeyInterface | Models.RootKeyInterface,
|
||||
operatorManager: OperatorManager,
|
||||
): Promise<EncryptedParameters> {
|
||||
const operator = operatorManager.operatorForVersion(key.keyVersion)
|
||||
let encryptionParameters
|
||||
|
||||
if (isAsyncOperator(operator)) {
|
||||
encryptionParameters = await operator.generateEncryptedParametersAsync(payload, key)
|
||||
} else {
|
||||
encryptionParameters = operator.generateEncryptedParametersSync(payload, key)
|
||||
}
|
||||
|
||||
if (!encryptionParameters) {
|
||||
throw 'Unable to generate encryption parameters'
|
||||
}
|
||||
|
||||
return encryptionParameters
|
||||
}
|
||||
|
||||
export async function decryptPayload<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payload: Models.EncryptedPayloadInterface,
|
||||
key: Models.ItemsKeyInterface | Models.RootKeyInterface,
|
||||
operatorManager: OperatorManager,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
const operator = operatorManager.operatorForVersion(payload.version)
|
||||
|
||||
try {
|
||||
if (isAsyncOperator(operator)) {
|
||||
return await operator.generateDecryptedParametersAsync(encryptedParametersFromPayload(payload), key)
|
||||
} else {
|
||||
return operator.generateDecryptedParametersSync(encryptedParametersFromPayload(payload), key)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error decrypting payload', payload, e)
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import {
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayloadInterface,
|
||||
ItemContent,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { BackupFile } from '../../Backups/BackupFile'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { KeyedDecryptionSplit, KeyedEncryptionSplit } from '../../Split/EncryptionSplit'
|
||||
|
||||
export interface EncryptionProvider {
|
||||
encryptSplitSingle(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface>
|
||||
|
||||
encryptSplit(split: KeyedEncryptionSplit): Promise<EncryptedPayloadInterface[]>
|
||||
|
||||
decryptSplitSingle<
|
||||
C extends ItemContent = ItemContent,
|
||||
P extends DecryptedPayloadInterface<C> = DecryptedPayloadInterface<C>,
|
||||
>(
|
||||
split: KeyedDecryptionSplit,
|
||||
): Promise<P | EncryptedPayloadInterface>
|
||||
|
||||
decryptSplit<
|
||||
C extends ItemContent = ItemContent,
|
||||
P extends DecryptedPayloadInterface<C> = DecryptedPayloadInterface<C>,
|
||||
>(
|
||||
split: KeyedDecryptionSplit,
|
||||
): Promise<(P | EncryptedPayloadInterface)[]>
|
||||
|
||||
hasRootKeyEncryptionSource(): boolean
|
||||
|
||||
getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined
|
||||
|
||||
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface>
|
||||
|
||||
/**
|
||||
* @returns The versions that this library supports.
|
||||
*/
|
||||
supportedVersions(): ProtocolVersion[]
|
||||
|
||||
getUserVersion(): ProtocolVersion | undefined
|
||||
|
||||
/**
|
||||
* Decrypts a backup file using user-inputted password
|
||||
* @param password - The raw user password associated with this backup file
|
||||
*/
|
||||
decryptBackupFile(
|
||||
file: BackupFile,
|
||||
password?: string,
|
||||
): Promise<ClientDisplayableError | (EncryptedPayloadInterface | DecryptedPayloadInterface)[]>
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
import { BackupFile } from '../../Backups/BackupFile'
|
||||
import { CreateAnyKeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { DecryptBackupFile } from '../../Backups/BackupFileDecryptor'
|
||||
import { EncryptionProvider } from './EncryptionProvider'
|
||||
import { findDefaultItemsKey } from '../Functions'
|
||||
import { ItemsEncryptionService } from '../Items/ItemsEncryption'
|
||||
import { KeyMode } from '../RootKey/KeyMode'
|
||||
import { OperatorManager } from '../../Operator/OperatorManager'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { V001Algorithm, V002Algorithm } from '../../Algorithm'
|
||||
import * as Common from '@standardnotes/common'
|
||||
import {
|
||||
CreateEncryptionSplitWithKeyLookup,
|
||||
FindPayloadInDecryptionSplit,
|
||||
FindPayloadInEncryptionSplit,
|
||||
KeyedDecryptionSplit,
|
||||
KeyedEncryptionSplit,
|
||||
} from '../../Split/EncryptionSplit'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as RootKeyEncryption from '../RootKey/RootKeyEncryption'
|
||||
import * as Services from '@standardnotes/services'
|
||||
import * as Utils from '@standardnotes/utils'
|
||||
import {
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
isErrorDecryptingParameters,
|
||||
encryptedParametersFromPayload,
|
||||
} from '../../Types/EncryptedParameters'
|
||||
import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData'
|
||||
import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData'
|
||||
import { LegacyAttachedData } from '../../Types/LegacyAttachedData'
|
||||
import {
|
||||
CreateDecryptedBackupFileContextPayload,
|
||||
CreateEncryptedBackupFileContextPayload,
|
||||
EncryptedPayload,
|
||||
isDecryptedPayload,
|
||||
isEncryptedPayload,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { SplitPayloadsByEncryptionType } from '../../Split/Functions'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { isNotUndefined } from '@standardnotes/utils'
|
||||
import { DiagnosticInfo } from '@standardnotes/services'
|
||||
|
||||
export enum EncryptionServiceEvent {
|
||||
RootKeyStatusChanged = 'RootKeyStatusChanged',
|
||||
}
|
||||
|
||||
/**
|
||||
* The encryption service is responsible for the encryption and decryption of payloads, and
|
||||
* handles delegation of a task to the respective protocol operator. Each version of the protocol
|
||||
* (001, 002, 003, 004, etc) uses a respective operator version to perform encryption operations.
|
||||
* Operators are located in /protocol/operator.
|
||||
* The protocol service depends on the keyManager for determining which key to use for the
|
||||
* encryption and decryption of a particular payload.
|
||||
* The protocol service is also responsible for dictating which protocol versions are valid,
|
||||
* and which are no longer valid or not supported.
|
||||
|
||||
* The key manager is responsible for managing root key and root key wrapper states.
|
||||
* When the key manager is initialized, it initiates itself with a keyMode, which
|
||||
* dictates the entire flow of key management. The key manager's responsibilities include:
|
||||
* - interacting with the device keychain to save or clear the root key
|
||||
* - interacting with storage to save root key params or wrapper params, or the wrapped root key.
|
||||
* - exposing methods that allow the application to unwrap the root key (unlock the application)
|
||||
*
|
||||
* It also exposes two primary methods for determining what key should be used to encrypt
|
||||
* or decrypt a particular payload. Some payloads are encrypted directly with the rootKey
|
||||
* (such as itemsKeys and encryptedStorage). Others are encrypted with itemsKeys (notes, tags, etc).
|
||||
|
||||
* The items key manager manages the lifecycle of items keys.
|
||||
* It is responsible for creating the default items key when conditions call for it
|
||||
* (such as after the first sync completes and no key exists).
|
||||
* It also exposes public methods that allows consumers to retrieve an items key
|
||||
* for a particular payload, and also retrieve all available items keys.
|
||||
*/
|
||||
export class EncryptionService extends Services.AbstractService<EncryptionServiceEvent> implements EncryptionProvider {
|
||||
private operatorManager: OperatorManager
|
||||
private readonly itemsEncryption: ItemsEncryptionService
|
||||
private readonly rootKeyEncryption: RootKeyEncryption.RootKeyEncryptionService
|
||||
private rootKeyObserverDisposer: () => void
|
||||
|
||||
constructor(
|
||||
private itemManager: Services.ItemManagerInterface,
|
||||
private payloadManager: Services.PayloadManagerInterface,
|
||||
public deviceInterface: Services.DeviceInterface,
|
||||
private storageService: Services.StorageServiceInterface,
|
||||
private identifier: Common.ApplicationIdentifier,
|
||||
public crypto: PureCryptoInterface,
|
||||
protected override internalEventBus: Services.InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
this.crypto = crypto
|
||||
|
||||
this.operatorManager = new OperatorManager(crypto)
|
||||
|
||||
this.itemsEncryption = new ItemsEncryptionService(
|
||||
itemManager,
|
||||
payloadManager,
|
||||
storageService,
|
||||
this.operatorManager,
|
||||
internalEventBus,
|
||||
)
|
||||
|
||||
this.rootKeyEncryption = new RootKeyEncryption.RootKeyEncryptionService(
|
||||
this.itemManager,
|
||||
this.operatorManager,
|
||||
this.deviceInterface,
|
||||
this.storageService,
|
||||
this.identifier,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.rootKeyObserverDisposer = this.rootKeyEncryption.addEventObserver((event) => {
|
||||
this.itemsEncryption.userVersion = this.getUserVersion()
|
||||
if (event === RootKeyEncryption.RootKeyServiceEvent.RootKeyStatusChanged) {
|
||||
void this.notifyEvent(EncryptionServiceEvent.RootKeyStatusChanged)
|
||||
}
|
||||
})
|
||||
|
||||
Utils.UuidGenerator.SetGenerator(this.crypto.generateUUID)
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.deviceInterface as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.crypto as unknown) = undefined
|
||||
;(this.operatorManager as unknown) = undefined
|
||||
|
||||
this.rootKeyObserverDisposer()
|
||||
;(this.rootKeyObserverDisposer as unknown) = undefined
|
||||
|
||||
this.itemsEncryption.deinit()
|
||||
;(this.itemsEncryption as unknown) = undefined
|
||||
|
||||
this.rootKeyEncryption.deinit()
|
||||
;(this.rootKeyEncryption as unknown) = undefined
|
||||
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
await this.rootKeyEncryption.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns encryption protocol display name for active account/wrapper
|
||||
*/
|
||||
public async getEncryptionDisplayName(): Promise<string> {
|
||||
const version = await this.rootKeyEncryption.getEncryptionSourceVersion()
|
||||
|
||||
if (version) {
|
||||
return this.operatorManager.operatorForVersion(version).getEncryptionDisplayName()
|
||||
}
|
||||
|
||||
throw Error('Attempting to access encryption display name wtihout source')
|
||||
}
|
||||
|
||||
public getLatestVersion() {
|
||||
return Common.ProtocolVersionLatest
|
||||
}
|
||||
|
||||
public hasAccount() {
|
||||
return this.rootKeyEncryption.hasAccount()
|
||||
}
|
||||
|
||||
public hasRootKeyEncryptionSource(): boolean {
|
||||
return this.rootKeyEncryption.hasRootKeyEncryptionSource()
|
||||
}
|
||||
|
||||
public getUserVersion(): Common.ProtocolVersion | undefined {
|
||||
return this.rootKeyEncryption.getUserVersion()
|
||||
}
|
||||
|
||||
public async upgradeAvailable() {
|
||||
const accountUpgradeAvailable = this.accountUpgradeAvailable()
|
||||
const passcodeUpgradeAvailable = await this.passcodeUpgradeAvailable()
|
||||
return accountUpgradeAvailable || passcodeUpgradeAvailable
|
||||
}
|
||||
|
||||
public getSureDefaultItemsKey(): Models.ItemsKeyInterface {
|
||||
return this.itemsEncryption.getDefaultItemsKey() as Models.ItemsKeyInterface
|
||||
}
|
||||
|
||||
async repersistAllItems(): Promise<void> {
|
||||
return this.itemsEncryption.repersistAllItems()
|
||||
}
|
||||
|
||||
public async reencryptItemsKeys(): Promise<void> {
|
||||
await this.rootKeyEncryption.reencryptItemsKeys()
|
||||
}
|
||||
|
||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||
return this.rootKeyEncryption.createNewItemsKeyWithRollback()
|
||||
}
|
||||
|
||||
public async decryptErroredPayloads(): Promise<void> {
|
||||
await this.itemsEncryption.decryptErroredPayloads()
|
||||
}
|
||||
|
||||
public itemsKeyForPayload(payload: Models.EncryptedPayloadInterface): Models.ItemsKeyInterface | undefined {
|
||||
return this.itemsEncryption.itemsKeyForPayload(payload)
|
||||
}
|
||||
|
||||
public defaultItemsKeyForItemVersion(
|
||||
version: Common.ProtocolVersion,
|
||||
fromKeys?: Models.ItemsKeyInterface[],
|
||||
): Models.ItemsKeyInterface | undefined {
|
||||
return this.itemsEncryption.defaultItemsKeyForItemVersion(version, fromKeys)
|
||||
}
|
||||
|
||||
public async encryptSplitSingle(split: KeyedEncryptionSplit): Promise<Models.EncryptedPayloadInterface> {
|
||||
return (await this.encryptSplit(split))[0]
|
||||
}
|
||||
|
||||
public async encryptSplit(split: KeyedEncryptionSplit): Promise<Models.EncryptedPayloadInterface[]> {
|
||||
const allEncryptedParams: EncryptedParameters[] = []
|
||||
|
||||
if (split.usesRootKey) {
|
||||
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads(
|
||||
split.usesRootKey.items,
|
||||
split.usesRootKey.key,
|
||||
)
|
||||
Utils.extendArray(allEncryptedParams, rootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKey) {
|
||||
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloads(
|
||||
split.usesItemsKey.items,
|
||||
split.usesItemsKey.key,
|
||||
)
|
||||
Utils.extendArray(allEncryptedParams, itemsKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesRootKeyWithKeyLookup) {
|
||||
const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup(
|
||||
split.usesRootKeyWithKeyLookup.items,
|
||||
)
|
||||
Utils.extendArray(allEncryptedParams, rootKeyEncrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKeyWithKeyLookup) {
|
||||
const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloadsWithKeyLookup(
|
||||
split.usesItemsKeyWithKeyLookup.items,
|
||||
)
|
||||
Utils.extendArray(allEncryptedParams, itemsKeyEncrypted)
|
||||
}
|
||||
|
||||
const packagedEncrypted = allEncryptedParams.map((encryptedParams) => {
|
||||
const original = FindPayloadInEncryptionSplit(encryptedParams.uuid, split)
|
||||
return new EncryptedPayload({
|
||||
...original,
|
||||
...encryptedParams,
|
||||
waitingForKey: false,
|
||||
errorDecrypting: false,
|
||||
})
|
||||
})
|
||||
|
||||
return packagedEncrypted
|
||||
}
|
||||
|
||||
public async decryptSplitSingle<
|
||||
C extends Models.ItemContent = Models.ItemContent,
|
||||
P extends Models.DecryptedPayloadInterface<C> = Models.DecryptedPayloadInterface<C>,
|
||||
>(split: KeyedDecryptionSplit): Promise<P | Models.EncryptedPayloadInterface> {
|
||||
const results = await this.decryptSplit<C, P>(split)
|
||||
return results[0]
|
||||
}
|
||||
|
||||
public async decryptSplit<
|
||||
C extends Models.ItemContent = Models.ItemContent,
|
||||
P extends Models.DecryptedPayloadInterface<C> = Models.DecryptedPayloadInterface<C>,
|
||||
>(split: KeyedDecryptionSplit): Promise<(P | Models.EncryptedPayloadInterface)[]> {
|
||||
const resultParams: (DecryptedParameters<C> | ErrorDecryptingParameters)[] = []
|
||||
|
||||
if (split.usesRootKey) {
|
||||
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads<C>(
|
||||
split.usesRootKey.items,
|
||||
split.usesRootKey.key,
|
||||
)
|
||||
Utils.extendArray(resultParams, rootKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesRootKeyWithKeyLookup) {
|
||||
const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup<C>(
|
||||
split.usesRootKeyWithKeyLookup.items,
|
||||
)
|
||||
Utils.extendArray(resultParams, rootKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKey) {
|
||||
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads<C>(
|
||||
split.usesItemsKey.items,
|
||||
split.usesItemsKey.key,
|
||||
)
|
||||
Utils.extendArray(resultParams, itemsKeyDecrypted)
|
||||
}
|
||||
|
||||
if (split.usesItemsKeyWithKeyLookup) {
|
||||
const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloadsWithKeyLookup<C>(
|
||||
split.usesItemsKeyWithKeyLookup.items,
|
||||
)
|
||||
Utils.extendArray(resultParams, itemsKeyDecrypted)
|
||||
}
|
||||
|
||||
const packagedResults = resultParams.map((params) => {
|
||||
const original = FindPayloadInDecryptionSplit(params.uuid, split)
|
||||
|
||||
if (isErrorDecryptingParameters(params)) {
|
||||
return new Models.EncryptedPayload({
|
||||
...original.ejected(),
|
||||
...params,
|
||||
})
|
||||
} else {
|
||||
return new Models.DecryptedPayload<C>({
|
||||
...original.ejected(),
|
||||
...params,
|
||||
}) as P
|
||||
}
|
||||
})
|
||||
|
||||
return packagedResults
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user's account protocol version is not equal to the latest version.
|
||||
*/
|
||||
public accountUpgradeAvailable(): boolean {
|
||||
const userVersion = this.getUserVersion()
|
||||
if (!userVersion) {
|
||||
return false
|
||||
}
|
||||
return userVersion !== Common.ProtocolVersionLatest
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user's account protocol version is not equal to the latest version.
|
||||
*/
|
||||
public async passcodeUpgradeAvailable(): Promise<boolean> {
|
||||
return this.rootKeyEncryption.passcodeUpgradeAvailable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current environment is capable of supporting
|
||||
* key derivation.
|
||||
*/
|
||||
public platformSupportsKeyDerivation(keyParams: SNRootKeyParams) {
|
||||
/**
|
||||
* If the version is 003 or lower, key derivation is supported unless the browser is
|
||||
* IE or Edge (or generally, where WebCrypto is not available) or React Native environment is detected.
|
||||
*
|
||||
* Versions 004 and above are always supported.
|
||||
*/
|
||||
if (Common.compareVersions(keyParams.version, Common.ProtocolVersion.V004) >= 0) {
|
||||
/* keyParams.version >= 004 */
|
||||
return true
|
||||
} else {
|
||||
return !!Utils.isWebCryptoAvailable() || Utils.isReactNativeEnvironment()
|
||||
}
|
||||
}
|
||||
|
||||
public supportedVersions(): Common.ProtocolVersion[] {
|
||||
return [
|
||||
Common.ProtocolVersion.V001,
|
||||
Common.ProtocolVersion.V002,
|
||||
Common.ProtocolVersion.V003,
|
||||
Common.ProtocolVersion.V004,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the input version is greater than the latest supported library version.
|
||||
*/
|
||||
public isVersionNewerThanLibraryVersion(version: Common.ProtocolVersion) {
|
||||
const libraryVersion = Common.ProtocolVersionLatest
|
||||
return Common.compareVersions(version, libraryVersion) === 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Versions 001 and 002 of the protocol supported dynamic costs, as reported by the server.
|
||||
* This function returns the client-enforced minimum cost, to prevent the server from
|
||||
* overwhelmingly under-reporting the cost.
|
||||
*/
|
||||
public costMinimumForVersion(version: Common.ProtocolVersion) {
|
||||
if (Common.compareVersions(version, Common.ProtocolVersion.V003) >= 0) {
|
||||
throw 'Cost minimums only apply to versions <= 002'
|
||||
}
|
||||
if (version === Common.ProtocolVersion.V001) {
|
||||
return V001Algorithm.PbkdfMinCost
|
||||
} else if (version === Common.ProtocolVersion.V002) {
|
||||
return V002Algorithm.PbkdfMinCost
|
||||
} else {
|
||||
throw `Invalid version for cost minimum: ${version}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a root key given a password and key params.
|
||||
* Delegates computation to respective protocol operator.
|
||||
*/
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface> {
|
||||
return this.rootKeyEncryption.computeRootKey(password, keyParams)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a root key using the latest protocol version
|
||||
*/
|
||||
public async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: Common.KeyParamsOrigination,
|
||||
version?: Common.ProtocolVersion,
|
||||
) {
|
||||
return this.rootKeyEncryption.createRootKey(identifier, password, origination, version)
|
||||
}
|
||||
|
||||
public async decryptBackupFile(
|
||||
file: BackupFile,
|
||||
password?: string,
|
||||
): Promise<
|
||||
ClientDisplayableError | (Models.EncryptedPayloadInterface | Models.DecryptedPayloadInterface<Models.ItemContent>)[]
|
||||
> {
|
||||
const result = await DecryptBackupFile(file, this, password)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key params object from a raw object
|
||||
* @param keyParams - The raw key params object to create a KeyParams object from
|
||||
*/
|
||||
public createKeyParams(keyParams: Common.AnyKeyParamsContent) {
|
||||
return CreateAnyKeyParams(keyParams)
|
||||
}
|
||||
|
||||
public async createEncryptedBackupFile(): Promise<BackupFile> {
|
||||
const payloads = this.itemManager.items.map((item) => item.payload)
|
||||
|
||||
const split = SplitPayloadsByEncryptionType(payloads)
|
||||
|
||||
const keyLookupSplit = CreateEncryptionSplitWithKeyLookup(split)
|
||||
|
||||
const result = await this.encryptSplit(keyLookupSplit)
|
||||
|
||||
const ejected = result.map((payload) => CreateEncryptedBackupFileContextPayload(payload))
|
||||
|
||||
const data: BackupFile = {
|
||||
version: Common.ProtocolVersionLatest,
|
||||
items: ejected,
|
||||
}
|
||||
|
||||
const keyParams = await this.getRootKeyParams()
|
||||
data.keyParams = keyParams?.getPortableValue()
|
||||
return data
|
||||
}
|
||||
|
||||
public createDecryptedBackupFile(): BackupFile {
|
||||
const payloads = this.payloadManager.nonDeletedItems.filter(
|
||||
(item) => item.content_type !== Common.ContentType.ItemsKey,
|
||||
)
|
||||
|
||||
const data: BackupFile = {
|
||||
version: Common.ProtocolVersionLatest,
|
||||
items: payloads
|
||||
.map((payload) => {
|
||||
if (isDecryptedPayload(payload)) {
|
||||
return CreateDecryptedBackupFileContextPayload(payload)
|
||||
} else if (isEncryptedPayload(payload)) {
|
||||
return CreateEncryptedBackupFileContextPayload(payload)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
.filter(isNotUndefined),
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
public hasPasscode(): boolean {
|
||||
return this.rootKeyEncryption.hasPasscode()
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns True if the root key has not yet been unwrapped (passcode locked).
|
||||
*/
|
||||
public async isPasscodeLocked() {
|
||||
return (await this.rootKeyEncryption.hasRootKeyWrapper()) && this.rootKeyEncryption.getRootKey() == undefined
|
||||
}
|
||||
|
||||
public async getRootKeyParams() {
|
||||
return this.rootKeyEncryption.getRootKeyParams()
|
||||
}
|
||||
|
||||
public getAccountKeyParams() {
|
||||
return this.rootKeyEncryption.memoizedRootKeyParams
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the root key wrapping key given a passcode.
|
||||
* Wrapping key params are read from disk.
|
||||
*/
|
||||
public async computeWrappingKey(passcode: string) {
|
||||
const keyParams = await this.rootKeyEncryption.getSureRootKeyWrapperKeyParams()
|
||||
const key = await this.computeRootKey(passcode, keyParams)
|
||||
return key
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps the persisted root key value using the supplied wrappingKey.
|
||||
* Application interfaces must check to see if the root key requires unwrapping on load.
|
||||
* If so, they must generate the unwrapping key by getting our saved wrapping key keyParams.
|
||||
* After unwrapping, the root key is automatically loaded.
|
||||
*/
|
||||
public async unwrapRootKey(wrappingKey: RootKeyInterface) {
|
||||
return this.rootKeyEncryption.unwrapRootKey(wrappingKey)
|
||||
}
|
||||
/**
|
||||
* Encrypts rootKey and saves it in storage instead of keychain, and then
|
||||
* clears keychain. This is because we don't want to store large encrypted
|
||||
* payloads in the keychain. If the root key is not wrapped, it is stored
|
||||
* in plain form in the user's secure keychain.
|
||||
*/
|
||||
public async setNewRootKeyWrapper(wrappingKey: SNRootKey) {
|
||||
return this.rootKeyEncryption.setNewRootKeyWrapper(wrappingKey)
|
||||
}
|
||||
|
||||
public async removePasscode(): Promise<void> {
|
||||
await this.rootKeyEncryption.removeRootKeyWrapper()
|
||||
}
|
||||
|
||||
public async setRootKey(key: SNRootKey, wrappingKey?: SNRootKey) {
|
||||
await this.rootKeyEncryption.setRootKey(key, wrappingKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-memory root key value.
|
||||
*/
|
||||
public getRootKey() {
|
||||
return this.rootKeyEncryption.getRootKey()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes root key and wrapper from keychain. Used when signing out of application.
|
||||
*/
|
||||
public async deleteWorkspaceSpecificKeyStateFromDevice() {
|
||||
await this.rootKeyEncryption.deleteWorkspaceSpecificKeyStateFromDevice()
|
||||
}
|
||||
|
||||
public async validateAccountPassword(password: string) {
|
||||
return this.rootKeyEncryption.validateAccountPassword(password)
|
||||
}
|
||||
|
||||
public async validatePasscode(passcode: string) {
|
||||
return this.rootKeyEncryption.validatePasscode(passcode)
|
||||
}
|
||||
|
||||
public getEmbeddedPayloadAuthenticatedData(
|
||||
payload: Models.EncryptedPayloadInterface,
|
||||
): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined {
|
||||
const version = payload.version
|
||||
if (!version) {
|
||||
return undefined
|
||||
}
|
||||
const operator = this.operatorManager.operatorForVersion(version)
|
||||
const authenticatedData = operator.getPayloadAuthenticatedData(encryptedParametersFromPayload(payload))
|
||||
return authenticatedData
|
||||
}
|
||||
|
||||
/** Returns the key params attached to this key's encrypted payload */
|
||||
public getKeyEmbeddedKeyParams(key: Models.EncryptedPayloadInterface): SNRootKeyParams | undefined {
|
||||
const authenticatedData = this.getEmbeddedPayloadAuthenticatedData(key)
|
||||
if (!authenticatedData) {
|
||||
return undefined
|
||||
}
|
||||
if (Common.isVersionLessThanOrEqualTo(key.version, Common.ProtocolVersion.V003)) {
|
||||
const rawKeyParams = authenticatedData as LegacyAttachedData
|
||||
return this.createKeyParams(rawKeyParams)
|
||||
} else {
|
||||
const rawKeyParams = (authenticatedData as RootKeyEncryptedAuthenticatedData).kp
|
||||
return this.createKeyParams(rawKeyParams)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A new rootkey-based items key is needed if a user changes their account password
|
||||
* on an 003 client and syncs on a signed in 004 client.
|
||||
*/
|
||||
public needsNewRootKeyBasedItemsKey(): boolean {
|
||||
if (!this.hasAccount()) {
|
||||
return false
|
||||
}
|
||||
|
||||
const rootKey = this.rootKeyEncryption.getRootKey()
|
||||
if (!rootKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (Common.compareVersions(rootKey.keyVersion, Common.ProtocolVersionLastNonrootItemsKey) > 0) {
|
||||
/** Is >= 004, not needed */
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* A new root key based items key is needed if our default items key content
|
||||
* isnt equal to our current root key
|
||||
*/
|
||||
const defaultItemsKey = findDefaultItemsKey(this.itemsEncryption.getItemsKeys())
|
||||
|
||||
/** Shouldn't be undefined, but if it is, we'll take the corrective action */
|
||||
if (!defaultItemsKey) {
|
||||
return true
|
||||
}
|
||||
|
||||
return defaultItemsKey.itemsKey !== rootKey.itemsKey
|
||||
}
|
||||
|
||||
public async createNewDefaultItemsKey(): Promise<Models.ItemsKeyInterface> {
|
||||
return this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||
}
|
||||
|
||||
public getPasswordCreatedDate(): Date | undefined {
|
||||
const rootKey = this.getRootKey()
|
||||
return rootKey ? rootKey.keyParams.createdDate : undefined
|
||||
}
|
||||
|
||||
public async onSyncEvent(eventName: Services.SyncEvent) {
|
||||
if (eventName === Services.SyncEvent.SyncCompletedWithAllItemsUploaded) {
|
||||
await this.handleFullSyncCompletion()
|
||||
}
|
||||
if (eventName === Services.SyncEvent.DownloadFirstSyncCompleted) {
|
||||
await this.handleDownloadFirstSyncCompletion()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a download-first sync completes, it means we've completed a (potentially multipage)
|
||||
* sync where we only downloaded what the server had before uploading anything. We will be
|
||||
* allowed to make local accomadations here before the server begins with the upload
|
||||
* part of the sync (automatically runs after download-first sync completes).
|
||||
* We use this to see if the server has any default itemsKeys, and if so, allows us to
|
||||
* delete any never-synced items keys we have here locally.
|
||||
*/
|
||||
private async handleDownloadFirstSyncCompletion() {
|
||||
if (!this.hasAccount()) {
|
||||
return
|
||||
}
|
||||
|
||||
const itemsKeys = this.itemsEncryption.getItemsKeys()
|
||||
|
||||
const neverSyncedKeys = itemsKeys.filter((key) => {
|
||||
return key.neverSynced
|
||||
})
|
||||
|
||||
const syncedKeys = itemsKeys.filter((key) => {
|
||||
return !key.neverSynced
|
||||
})
|
||||
|
||||
/**
|
||||
* Find isDefault items key that have been previously synced.
|
||||
* If we find one, this means we can delete any non-synced keys.
|
||||
*/
|
||||
const defaultSyncedKey = syncedKeys.find((key) => {
|
||||
return key.isDefault
|
||||
})
|
||||
|
||||
const hasSyncedItemsKey = !Utils.isNullOrUndefined(defaultSyncedKey)
|
||||
if (hasSyncedItemsKey) {
|
||||
/** Delete all never synced keys */
|
||||
await this.itemManager.setItemsToBeDeleted(neverSyncedKeys)
|
||||
} else {
|
||||
/**
|
||||
* No previous synced items key.
|
||||
* We can keep the one(s) we have, only if their version is equal to our root key
|
||||
* version. If their version is not equal to our root key version, delete them. If
|
||||
* we end up with 0 items keys, create a new one. This covers the case when you open
|
||||
* the app offline and it creates an 004 key, and then you sign into an 003 account.
|
||||
*/
|
||||
const rootKeyParams = await this.getRootKeyParams()
|
||||
if (rootKeyParams) {
|
||||
/** If neverSynced.version != rootKey.version, delete. */
|
||||
const toDelete = neverSyncedKeys.filter((itemsKey) => {
|
||||
return itemsKey.keyVersion !== rootKeyParams.version
|
||||
})
|
||||
if (toDelete.length > 0) {
|
||||
await this.itemManager.setItemsToBeDeleted(toDelete)
|
||||
}
|
||||
|
||||
if (this.itemsEncryption.getItemsKeys().length === 0) {
|
||||
await this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
/** If we do not have an items key for our current account version, create one */
|
||||
const userVersion = this.getUserVersion()
|
||||
const accountVersionedKey = this.itemsEncryption.getItemsKeys().find((key) => key.keyVersion === userVersion)
|
||||
if (Utils.isNullOrUndefined(accountVersionedKey)) {
|
||||
await this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||
}
|
||||
|
||||
this.syncUnsycnedItemsKeys()
|
||||
}
|
||||
|
||||
private async handleFullSyncCompletion() {
|
||||
/** Always create a new items key after full sync, if no items key is found */
|
||||
const currentItemsKey = findDefaultItemsKey(this.itemsEncryption.getItemsKeys())
|
||||
if (!currentItemsKey) {
|
||||
await this.rootKeyEncryption.createNewDefaultItemsKey()
|
||||
if (this.rootKeyEncryption.keyMode === KeyMode.WrapperOnly) {
|
||||
return this.itemsEncryption.repersistAllItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* There is presently an issue where an items key created while signed out of account (
|
||||
* or possibly signed in but with invalid session), then signing into account, results in that
|
||||
* items key never syncing to the account even though it is being used to encrypt synced items.
|
||||
* Until we can determine its cause, this corrective function will find any such keys and sync them.
|
||||
*/
|
||||
private syncUnsycnedItemsKeys(): void {
|
||||
if (!this.hasAccount()) {
|
||||
return
|
||||
}
|
||||
|
||||
const unsyncedKeys = this.itemsEncryption.getItemsKeys().filter((key) => key.neverSynced && !key.dirty)
|
||||
if (unsyncedKeys.length > 0) {
|
||||
void this.itemManager.setItemsDirty(unsyncedKeys)
|
||||
}
|
||||
}
|
||||
|
||||
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return {
|
||||
encryption: {
|
||||
getLatestVersion: this.getLatestVersion(),
|
||||
hasAccount: this.hasAccount(),
|
||||
hasRootKeyEncryptionSource: this.hasRootKeyEncryptionSource(),
|
||||
getUserVersion: this.getUserVersion(),
|
||||
upgradeAvailable: await this.upgradeAvailable(),
|
||||
accountUpgradeAvailable: this.accountUpgradeAvailable(),
|
||||
passcodeUpgradeAvailable: await this.passcodeUpgradeAvailable(),
|
||||
hasPasscode: this.hasPasscode(),
|
||||
isPasscodeLocked: await this.isPasscodeLocked(),
|
||||
needsNewRootKeyBasedItemsKey: this.needsNewRootKeyBasedItemsKey(),
|
||||
...(await this.itemsEncryption.getDiagnostics()),
|
||||
...(await this.rootKeyEncryption.getDiagnostics()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { ItemsKeyInterface } from '@standardnotes/models'
|
||||
|
||||
export function findDefaultItemsKey(itemsKeys: ItemsKeyInterface[]): ItemsKeyInterface | undefined {
|
||||
if (itemsKeys.length === 1) {
|
||||
return itemsKeys[0]
|
||||
}
|
||||
|
||||
const defaultKeys = itemsKeys.filter((key) => {
|
||||
return key.isDefault
|
||||
})
|
||||
|
||||
if (defaultKeys.length > 1) {
|
||||
/**
|
||||
* Prioritize one that is synced, as neverSynced keys will likely be deleted after
|
||||
* DownloadFirst sync.
|
||||
*/
|
||||
const syncedKeys = defaultKeys.filter((key) => !key.neverSynced)
|
||||
if (syncedKeys.length > 0) {
|
||||
return syncedKeys[0]
|
||||
}
|
||||
}
|
||||
|
||||
return defaultKeys[0]
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import { ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||
import { findDefaultItemsKey } from '../Functions'
|
||||
import { OperatorManager } from '../../Operator/OperatorManager'
|
||||
import { StandardException } from '../../StandardException'
|
||||
import * as OperatorWrapper from '../../Operator/OperatorWrapper'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as Services from '@standardnotes/services'
|
||||
import {
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
isErrorDecryptingParameters,
|
||||
} from '../../Types/EncryptedParameters'
|
||||
import { isEncryptedPayload } from '@standardnotes/models'
|
||||
import { DiagnosticInfo } from '@standardnotes/services'
|
||||
import { Uuids } from '@standardnotes/utils'
|
||||
|
||||
export class ItemsEncryptionService extends Services.AbstractService {
|
||||
private removeItemsObserver!: () => void
|
||||
public userVersion?: ProtocolVersion
|
||||
|
||||
constructor(
|
||||
private itemManager: Services.ItemManagerInterface,
|
||||
private payloadManager: Services.PayloadManagerInterface,
|
||||
private storageService: Services.StorageServiceInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
protected override internalEventBus: Services.InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
|
||||
this.removeItemsObserver = this.itemManager.addObserver([ContentType.ItemsKey], ({ changed, inserted }) => {
|
||||
if (changed.concat(inserted).length > 0) {
|
||||
void this.decryptErroredPayloads()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
;(this.payloadManager as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
this.removeItemsObserver()
|
||||
;(this.removeItemsObserver as unknown) = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
/**
|
||||
* If encryption status changes (esp. on mobile, where local storage encryption
|
||||
* can be disabled), consumers may call this function to repersist all items to
|
||||
* disk using latest encryption status.
|
||||
*/
|
||||
async repersistAllItems(): Promise<void> {
|
||||
const items = this.itemManager.items
|
||||
const payloads = items.map((item) => item.payload)
|
||||
return this.storageService.savePayloads(payloads)
|
||||
}
|
||||
|
||||
public getItemsKeys() {
|
||||
return this.itemManager.getDisplayableItemsKeys()
|
||||
}
|
||||
|
||||
public itemsKeyForPayload(payload: Models.EncryptedPayloadInterface): Models.ItemsKeyInterface | undefined {
|
||||
return this.getItemsKeys().find(
|
||||
(key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id,
|
||||
)
|
||||
}
|
||||
|
||||
public getDefaultItemsKey(): Models.ItemsKeyInterface | undefined {
|
||||
return findDefaultItemsKey(this.getItemsKeys())
|
||||
}
|
||||
|
||||
private keyToUseForItemEncryption(): Models.ItemsKeyInterface | StandardException {
|
||||
const defaultKey = this.getDefaultItemsKey()
|
||||
let result: Models.ItemsKeyInterface | undefined = undefined
|
||||
|
||||
if (this.userVersion && this.userVersion !== defaultKey?.keyVersion) {
|
||||
/**
|
||||
* The default key appears to be either newer or older than the user's account version
|
||||
* We could throw an exception here, but will instead fall back to a corrective action:
|
||||
* return any items key that corresponds to the user's version
|
||||
*/
|
||||
const itemsKeys = this.getItemsKeys()
|
||||
result = itemsKeys.find((key) => key.keyVersion === this.userVersion)
|
||||
} else {
|
||||
result = defaultKey
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return new StandardException('Cannot find items key to use for encryption')
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private keyToUseForDecryptionOfPayload(
|
||||
payload: Models.EncryptedPayloadInterface,
|
||||
): Models.ItemsKeyInterface | undefined {
|
||||
if (payload.items_key_id) {
|
||||
const itemsKey = this.itemsKeyForPayload(payload)
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
const defaultKey = this.defaultItemsKeyForItemVersion(payload.version)
|
||||
return defaultKey
|
||||
}
|
||||
|
||||
public async encryptPayloadWithKeyLookup(payload: Models.DecryptedPayloadInterface): Promise<EncryptedParameters> {
|
||||
const key = this.keyToUseForItemEncryption()
|
||||
|
||||
if (key instanceof StandardException) {
|
||||
throw Error(key.message)
|
||||
}
|
||||
|
||||
return this.encryptPayload(payload, key)
|
||||
}
|
||||
|
||||
public async encryptPayload(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: Models.ItemsKeyInterface,
|
||||
): Promise<EncryptedParameters> {
|
||||
if (isEncryptedPayload(payload)) {
|
||||
throw Error('Attempting to encrypt already encrypted payload.')
|
||||
}
|
||||
if (!payload.content) {
|
||||
throw Error('Attempting to encrypt payload with no content.')
|
||||
}
|
||||
if (!payload.uuid) {
|
||||
throw Error('Attempting to encrypt payload with no UuidGenerator.')
|
||||
}
|
||||
|
||||
return OperatorWrapper.encryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
|
||||
public async encryptPayloads(
|
||||
payloads: Models.DecryptedPayloadInterface[],
|
||||
key: Models.ItemsKeyInterface,
|
||||
): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
|
||||
}
|
||||
|
||||
public async encryptPayloadsWithKeyLookup(
|
||||
payloads: Models.DecryptedPayloadInterface[],
|
||||
): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload)))
|
||||
}
|
||||
|
||||
public async decryptPayloadWithKeyLookup<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payload: Models.EncryptedPayloadInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
const key = this.keyToUseForDecryptionOfPayload(payload)
|
||||
|
||||
if (key == undefined) {
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
errorDecrypting: true,
|
||||
waitingForKey: true,
|
||||
}
|
||||
}
|
||||
|
||||
return this.decryptPayload(payload, key)
|
||||
}
|
||||
|
||||
public async decryptPayload<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payload: Models.EncryptedPayloadInterface,
|
||||
key: Models.ItemsKeyInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
if (!payload.content) {
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
errorDecrypting: true,
|
||||
}
|
||||
}
|
||||
|
||||
return OperatorWrapper.decryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
|
||||
public async decryptPayloadsWithKeyLookup<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payloads: Models.EncryptedPayloadInterface[],
|
||||
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
|
||||
return Promise.all(payloads.map((payload) => this.decryptPayloadWithKeyLookup<C>(payload)))
|
||||
}
|
||||
|
||||
public async decryptPayloads<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payloads: Models.EncryptedPayloadInterface[],
|
||||
key: Models.ItemsKeyInterface,
|
||||
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
|
||||
return Promise.all(payloads.map((payload) => this.decryptPayload<C>(payload, key)))
|
||||
}
|
||||
|
||||
public async decryptErroredPayloads(): Promise<void> {
|
||||
const payloads = this.payloadManager.invalidPayloads.filter((i) => i.content_type !== ContentType.ItemsKey)
|
||||
if (payloads.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const resultParams = await this.decryptPayloadsWithKeyLookup(payloads)
|
||||
|
||||
const decryptedPayloads = resultParams.map((params) => {
|
||||
const original = Models.SureFindPayload(payloads, params.uuid)
|
||||
if (isErrorDecryptingParameters(params)) {
|
||||
return new Models.EncryptedPayload({
|
||||
...original.ejected(),
|
||||
...params,
|
||||
})
|
||||
} else {
|
||||
return new Models.DecryptedPayload({
|
||||
...original.ejected(),
|
||||
...params,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
await this.payloadManager.emitPayloads(decryptedPayloads, Models.PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
|
||||
/**
|
||||
* When migrating from non-items key architecture, many items will not have a
|
||||
* relationship with any key object. For those items, we can be sure that only 1 key
|
||||
* object will correspond to that protocol version.
|
||||
* @returns The items key object to decrypt items encrypted
|
||||
* with previous protocol version.
|
||||
*/
|
||||
public defaultItemsKeyForItemVersion(
|
||||
version: ProtocolVersion,
|
||||
fromKeys?: Models.ItemsKeyInterface[],
|
||||
): Models.ItemsKeyInterface | undefined {
|
||||
/** Try to find one marked default first */
|
||||
const searchKeys = fromKeys || this.getItemsKeys()
|
||||
const priorityKey = searchKeys.find((key) => {
|
||||
return key.isDefault && key.keyVersion === version
|
||||
})
|
||||
if (priorityKey) {
|
||||
return priorityKey
|
||||
}
|
||||
return searchKeys.find((key) => {
|
||||
return key.keyVersion === version
|
||||
})
|
||||
}
|
||||
|
||||
override async getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
const keyForItems = this.keyToUseForItemEncryption()
|
||||
return {
|
||||
itemsEncryption: {
|
||||
itemsKeysIds: Uuids(this.getItemsKeys()),
|
||||
defaultItemsKeyId: this.getDefaultItemsKey()?.uuid,
|
||||
keyToUseForItemEncryptionId: keyForItems instanceof StandardException ? undefined : keyForItems.uuid,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export enum KeyMode {
|
||||
/** i.e No account and no passcode */
|
||||
RootKeyNone = 0,
|
||||
/** i.e Account but no passcode */
|
||||
RootKeyOnly = 1,
|
||||
/** i.e Account plus passcode */
|
||||
RootKeyPlusWrapper = 2,
|
||||
/** i.e No account, but passcode */
|
||||
WrapperOnly = 3,
|
||||
}
|
||||
@@ -0,0 +1,643 @@
|
||||
import { CreateAnyKeyParams } from '../../Keys/RootKey/KeyParamsFunctions'
|
||||
import { findDefaultItemsKey } from '../Functions'
|
||||
import { KeyMode } from './KeyMode'
|
||||
import { OperatorManager } from '../../Operator/OperatorManager'
|
||||
import { SNRootKey } from '../../Keys/RootKey/RootKey'
|
||||
import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
import * as Common from '@standardnotes/common'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as OperatorWrapper from '../../Operator/OperatorWrapper'
|
||||
import * as Services from '@standardnotes/services'
|
||||
import {
|
||||
DecryptedParameters,
|
||||
EncryptedParameters,
|
||||
ErrorDecryptingParameters,
|
||||
isErrorDecryptingParameters,
|
||||
} from '../../Types/EncryptedParameters'
|
||||
import { ItemsKeyMutator } from '../../Keys/ItemsKey/ItemsKeyMutator'
|
||||
import { CreateNewRootKey } from '../../Keys/RootKey/Functions'
|
||||
import {
|
||||
DecryptedPayload,
|
||||
FillItemContentSpecialized,
|
||||
ItemsKeyContent,
|
||||
ItemsKeyContentSpecialized,
|
||||
PayloadTimestampDefaults,
|
||||
RootKeyContent,
|
||||
RootKeyInterface,
|
||||
NamespacedRootKeyInKeychain,
|
||||
} from '@standardnotes/models'
|
||||
|
||||
export enum RootKeyServiceEvent {
|
||||
RootKeyStatusChanged = 'RootKeyStatusChanged',
|
||||
}
|
||||
|
||||
export class RootKeyEncryptionService extends Services.AbstractService<RootKeyServiceEvent> {
|
||||
private rootKey?: RootKeyInterface
|
||||
public keyMode = KeyMode.RootKeyNone
|
||||
public memoizedRootKeyParams?: SNRootKeyParams
|
||||
|
||||
constructor(
|
||||
private itemManager: Services.ItemManagerInterface,
|
||||
private operatorManager: OperatorManager,
|
||||
public deviceInterface: Services.DeviceInterface,
|
||||
private storageService: Services.StorageServiceInterface,
|
||||
private identifier: Common.ApplicationIdentifier,
|
||||
protected override internalEventBus: Services.InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
}
|
||||
|
||||
public override deinit(): void {
|
||||
;(this.itemManager as unknown) = undefined
|
||||
this.rootKey = undefined
|
||||
this.memoizedRootKeyParams = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
public async initialize() {
|
||||
const wrappedRootKey = this.getWrappedRootKey()
|
||||
const accountKeyParams = await this.recomputeAccountKeyParams()
|
||||
const hasWrapper = await this.hasRootKeyWrapper()
|
||||
const hasRootKey = wrappedRootKey != undefined || accountKeyParams != undefined
|
||||
|
||||
if (hasWrapper && hasRootKey) {
|
||||
this.keyMode = KeyMode.RootKeyPlusWrapper
|
||||
} else if (hasWrapper && !hasRootKey) {
|
||||
this.keyMode = KeyMode.WrapperOnly
|
||||
} else if (!hasWrapper && hasRootKey) {
|
||||
this.keyMode = KeyMode.RootKeyOnly
|
||||
} else if (!hasWrapper && !hasRootKey) {
|
||||
this.keyMode = KeyMode.RootKeyNone
|
||||
} else {
|
||||
throw 'Invalid key mode condition'
|
||||
}
|
||||
|
||||
if (this.keyMode === KeyMode.RootKeyOnly) {
|
||||
this.setRootKeyInstance(await this.getRootKeyFromKeychain())
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
}
|
||||
|
||||
private async handleKeyStatusChange() {
|
||||
await this.recomputeAccountKeyParams()
|
||||
void this.notifyEvent(RootKeyServiceEvent.RootKeyStatusChanged)
|
||||
}
|
||||
|
||||
public async passcodeUpgradeAvailable() {
|
||||
const passcodeParams = await this.getRootKeyWrapperKeyParams()
|
||||
if (!passcodeParams) {
|
||||
return false
|
||||
}
|
||||
return passcodeParams.version !== Common.ProtocolVersionLatest
|
||||
}
|
||||
|
||||
public async hasRootKeyWrapper() {
|
||||
const wrapper = await this.getRootKeyWrapperKeyParams()
|
||||
return wrapper != undefined
|
||||
}
|
||||
|
||||
public hasAccount() {
|
||||
switch (this.keyMode) {
|
||||
case KeyMode.RootKeyNone:
|
||||
case KeyMode.WrapperOnly:
|
||||
return false
|
||||
case KeyMode.RootKeyOnly:
|
||||
case KeyMode.RootKeyPlusWrapper:
|
||||
return true
|
||||
default:
|
||||
throw Error(`Unhandled keyMode value '${this.keyMode}'.`)
|
||||
}
|
||||
}
|
||||
|
||||
public hasRootKeyEncryptionSource(): boolean {
|
||||
return this.hasAccount() || this.hasPasscode()
|
||||
}
|
||||
|
||||
public hasPasscode() {
|
||||
return this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper
|
||||
}
|
||||
|
||||
public async getEncryptionSourceVersion(): Promise<Common.ProtocolVersion> {
|
||||
if (this.hasAccount()) {
|
||||
return this.getSureUserVersion()
|
||||
} else if (this.hasPasscode()) {
|
||||
const passcodeParams = await this.getSureRootKeyWrapperKeyParams()
|
||||
return passcodeParams.version
|
||||
}
|
||||
|
||||
throw Error('Attempting to access encryption source version without source')
|
||||
}
|
||||
|
||||
public getUserVersion(): Common.ProtocolVersion | undefined {
|
||||
const keyParams = this.memoizedRootKeyParams
|
||||
return keyParams?.version
|
||||
}
|
||||
|
||||
private getSureUserVersion(): Common.ProtocolVersion {
|
||||
const keyParams = this.memoizedRootKeyParams as SNRootKeyParams
|
||||
return keyParams.version
|
||||
}
|
||||
|
||||
private async getRootKeyFromKeychain() {
|
||||
const rawKey = (await this.deviceInterface.getNamespacedKeychainValue(this.identifier)) as
|
||||
| NamespacedRootKeyInKeychain
|
||||
| undefined
|
||||
|
||||
if (rawKey == undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const keyParams = await this.getSureRootKeyParams()
|
||||
|
||||
return CreateNewRootKey({
|
||||
...rawKey,
|
||||
keyParams: keyParams.getPortableValue(),
|
||||
})
|
||||
}
|
||||
|
||||
private async saveRootKeyToKeychain() {
|
||||
if (this.getRootKey() == undefined) {
|
||||
throw 'Attempting to non-existent root key to the keychain.'
|
||||
}
|
||||
if (this.keyMode !== KeyMode.RootKeyOnly) {
|
||||
throw 'Should not be persisting wrapped key to keychain.'
|
||||
}
|
||||
|
||||
const rawKey = this.getSureRootKey().getKeychainValue()
|
||||
|
||||
return this.executeCriticalFunction(() => {
|
||||
return this.deviceInterface.setNamespacedKeychainValue(rawKey, this.identifier)
|
||||
})
|
||||
}
|
||||
|
||||
public async getRootKeyWrapperKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
const rawKeyParams = await this.storageService.getValue(
|
||||
Services.StorageKey.RootKeyWrapperKeyParams,
|
||||
Services.StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
if (!rawKeyParams) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return CreateAnyKeyParams(rawKeyParams as Common.AnyKeyParamsContent)
|
||||
}
|
||||
|
||||
public async getSureRootKeyWrapperKeyParams() {
|
||||
return this.getRootKeyWrapperKeyParams() as Promise<SNRootKeyParams>
|
||||
}
|
||||
|
||||
public async getRootKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
return this.getRootKeyWrapperKeyParams()
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
return this.recomputeAccountKeyParams()
|
||||
} else if (this.keyMode === KeyMode.RootKeyNone) {
|
||||
return undefined
|
||||
} else {
|
||||
throw `Unhandled key mode for getRootKeyParams ${this.keyMode}`
|
||||
}
|
||||
}
|
||||
|
||||
public async getSureRootKeyParams(): Promise<SNRootKeyParams> {
|
||||
return this.getRootKeyParams() as Promise<SNRootKeyParams>
|
||||
}
|
||||
|
||||
public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface> {
|
||||
const version = keyParams.version
|
||||
const operator = this.operatorManager.operatorForVersion(version)
|
||||
return operator.computeRootKey(password, keyParams)
|
||||
}
|
||||
|
||||
public async createRootKey(
|
||||
identifier: string,
|
||||
password: string,
|
||||
origination: Common.KeyParamsOrigination,
|
||||
version?: Common.ProtocolVersion,
|
||||
) {
|
||||
const operator = version ? this.operatorManager.operatorForVersion(version) : this.operatorManager.defaultOperator()
|
||||
return operator.createRootKey(identifier, password, origination)
|
||||
}
|
||||
|
||||
private getSureMemoizedRootKeyParams(): SNRootKeyParams {
|
||||
return this.memoizedRootKeyParams as SNRootKeyParams
|
||||
}
|
||||
|
||||
public async validateAccountPassword(password: string) {
|
||||
const key = await this.computeRootKey(password, this.getSureMemoizedRootKeyParams())
|
||||
const valid = this.getSureRootKey().compare(key)
|
||||
if (valid) {
|
||||
return { valid, artifacts: { rootKey: key } }
|
||||
} else {
|
||||
return { valid: false }
|
||||
}
|
||||
}
|
||||
|
||||
public async validatePasscode(passcode: string) {
|
||||
const keyParams = await this.getSureRootKeyWrapperKeyParams()
|
||||
const key = await this.computeRootKey(passcode, keyParams)
|
||||
const valid = await this.validateWrappingKey(key)
|
||||
if (valid) {
|
||||
return { valid, artifacts: { wrappingKey: key } }
|
||||
} else {
|
||||
return { valid: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We know a wrappingKey is correct if it correctly decrypts
|
||||
* wrapped root key.
|
||||
*/
|
||||
public async validateWrappingKey(wrappingKey: SNRootKey) {
|
||||
const wrappedRootKey = this.getWrappedRootKey()
|
||||
|
||||
/** If wrapper only, storage is encrypted directly with wrappingKey */
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
return this.storageService.canDecryptWithKey(wrappingKey)
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
/**
|
||||
* In these modes, storage is encrypted with account keys, and
|
||||
* account keys are encrypted with wrappingKey. Here we validate
|
||||
* by attempting to decrypt account keys.
|
||||
*/
|
||||
const wrappedKeyPayload = new Models.EncryptedPayload(wrappedRootKey)
|
||||
const decrypted = await this.decryptPayload(wrappedKeyPayload, wrappingKey)
|
||||
return !isErrorDecryptingParameters(decrypted)
|
||||
} else {
|
||||
throw 'Unhandled case in validateWrappingKey'
|
||||
}
|
||||
}
|
||||
|
||||
private async recomputeAccountKeyParams(): Promise<SNRootKeyParams | undefined> {
|
||||
const rawKeyParams = await this.storageService.getValue(
|
||||
Services.StorageKey.RootKeyParams,
|
||||
Services.StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
if (!rawKeyParams) {
|
||||
return
|
||||
}
|
||||
|
||||
this.memoizedRootKeyParams = CreateAnyKeyParams(rawKeyParams as Common.AnyKeyParamsContent)
|
||||
return this.memoizedRootKeyParams
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the current in-memory root key value using the wrappingKey,
|
||||
* then persists the wrapped value to disk.
|
||||
*/
|
||||
private async wrapAndPersistRootKey(wrappingKey: SNRootKey) {
|
||||
const rootKey = this.getSureRootKey()
|
||||
const value: Models.DecryptedTransferPayload = {
|
||||
...rootKey.payload.ejected(),
|
||||
content: FillItemContentSpecialized(rootKey.persistableValueWhenWrapping()),
|
||||
}
|
||||
const payload = new Models.DecryptedPayload(value)
|
||||
|
||||
const wrappedKey = await this.encryptPayload(payload, wrappingKey)
|
||||
const wrappedKeyPayload = new Models.EncryptedPayload({
|
||||
...payload.ejected(),
|
||||
...wrappedKey,
|
||||
errorDecrypting: false,
|
||||
waitingForKey: false,
|
||||
})
|
||||
|
||||
this.storageService.setValue(
|
||||
Services.StorageKey.WrappedRootKey,
|
||||
wrappedKeyPayload.ejected(),
|
||||
Services.StorageValueModes.Nonwrapped,
|
||||
)
|
||||
}
|
||||
|
||||
public async unwrapRootKey(wrappingKey: RootKeyInterface) {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.setRootKeyInstance(wrappingKey)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.keyMode !== KeyMode.RootKeyPlusWrapper) {
|
||||
throw 'Invalid key mode condition for unwrapping.'
|
||||
}
|
||||
|
||||
const wrappedKey = this.getWrappedRootKey()
|
||||
const payload = new Models.EncryptedPayload(wrappedKey)
|
||||
const decrypted = await this.decryptPayload<Models.RootKeyContent>(payload, wrappingKey)
|
||||
|
||||
if (isErrorDecryptingParameters(decrypted)) {
|
||||
throw Error('Unable to decrypt root key with provided wrapping key.')
|
||||
} else {
|
||||
const decryptedPayload = new DecryptedPayload<RootKeyContent>({
|
||||
...payload.ejected(),
|
||||
...decrypted,
|
||||
})
|
||||
this.setRootKeyInstance(new SNRootKey(decryptedPayload))
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts rootKey and saves it in storage instead of keychain, and then
|
||||
* clears keychain. This is because we don't want to store large encrypted
|
||||
* payloads in the keychain. If the root key is not wrapped, it is stored
|
||||
* in plain form in the user's secure keychain.
|
||||
*/
|
||||
public async setNewRootKeyWrapper(wrappingKey: SNRootKey) {
|
||||
if (this.keyMode === KeyMode.RootKeyNone) {
|
||||
this.keyMode = KeyMode.WrapperOnly
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly) {
|
||||
this.keyMode = KeyMode.RootKeyPlusWrapper
|
||||
} else {
|
||||
throw Error('Attempting to set wrapper on already wrapped key.')
|
||||
}
|
||||
|
||||
await this.deviceInterface.clearNamespacedKeychainValue(this.identifier)
|
||||
|
||||
if (this.keyMode === KeyMode.WrapperOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.setRootKeyInstance(wrappingKey)
|
||||
await this.reencryptItemsKeys()
|
||||
} else {
|
||||
await this.wrapAndPersistRootKey(wrappingKey)
|
||||
}
|
||||
|
||||
this.storageService.setValue(
|
||||
Services.StorageKey.RootKeyWrapperKeyParams,
|
||||
wrappingKey.keyParams.getPortableValue(),
|
||||
Services.StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
await this.handleKeyStatusChange()
|
||||
} else {
|
||||
throw Error('Invalid keyMode on setNewRootKeyWrapper')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes root key wrapper from local storage and stores root key bare in secure keychain.
|
||||
*/
|
||||
public async removeRootKeyWrapper(): Promise<void> {
|
||||
if (this.keyMode !== KeyMode.WrapperOnly && this.keyMode !== KeyMode.RootKeyPlusWrapper) {
|
||||
throw Error('Attempting to remove root key wrapper on unwrapped key.')
|
||||
}
|
||||
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.keyMode = KeyMode.RootKeyNone
|
||||
this.setRootKeyInstance(undefined)
|
||||
} else if (this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
this.keyMode = KeyMode.RootKeyOnly
|
||||
}
|
||||
|
||||
await this.storageService.removeValue(Services.StorageKey.WrappedRootKey, Services.StorageValueModes.Nonwrapped)
|
||||
await this.storageService.removeValue(
|
||||
Services.StorageKey.RootKeyWrapperKeyParams,
|
||||
Services.StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
if (this.keyMode === KeyMode.RootKeyOnly) {
|
||||
await this.saveRootKeyToKeychain()
|
||||
}
|
||||
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
|
||||
public async setRootKey(key: SNRootKey, wrappingKey?: SNRootKey) {
|
||||
if (!key.keyParams) {
|
||||
throw Error('keyParams must be supplied if setting root key.')
|
||||
}
|
||||
|
||||
if (this.getRootKey() === key) {
|
||||
throw Error('Attempting to set root key as same current value.')
|
||||
}
|
||||
|
||||
if (this.keyMode === KeyMode.WrapperOnly) {
|
||||
this.keyMode = KeyMode.RootKeyPlusWrapper
|
||||
} else if (this.keyMode === KeyMode.RootKeyNone) {
|
||||
this.keyMode = KeyMode.RootKeyOnly
|
||||
} else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
/** Root key is simply changing, mode stays the same */
|
||||
/** this.keyMode = this.keyMode; */
|
||||
} else {
|
||||
throw Error(`Unhandled key mode for setNewRootKey ${this.keyMode}`)
|
||||
}
|
||||
|
||||
this.setRootKeyInstance(key)
|
||||
|
||||
this.storageService.setValue(
|
||||
Services.StorageKey.RootKeyParams,
|
||||
key.keyParams.getPortableValue(),
|
||||
Services.StorageValueModes.Nonwrapped,
|
||||
)
|
||||
|
||||
if (this.keyMode === KeyMode.RootKeyOnly) {
|
||||
await this.saveRootKeyToKeychain()
|
||||
} else if (this.keyMode === KeyMode.RootKeyPlusWrapper) {
|
||||
if (!wrappingKey) {
|
||||
throw Error('wrappingKey must be supplied')
|
||||
}
|
||||
await this.wrapAndPersistRootKey(wrappingKey)
|
||||
}
|
||||
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes root key and wrapper from keychain. Used when signing out of application.
|
||||
*/
|
||||
public async deleteWorkspaceSpecificKeyStateFromDevice() {
|
||||
await this.deviceInterface.clearNamespacedKeychainValue(this.identifier)
|
||||
await this.storageService.removeValue(Services.StorageKey.WrappedRootKey, Services.StorageValueModes.Nonwrapped)
|
||||
await this.storageService.removeValue(
|
||||
Services.StorageKey.RootKeyWrapperKeyParams,
|
||||
Services.StorageValueModes.Nonwrapped,
|
||||
)
|
||||
await this.storageService.removeValue(Services.StorageKey.RootKeyParams, Services.StorageValueModes.Nonwrapped)
|
||||
this.keyMode = KeyMode.RootKeyNone
|
||||
this.setRootKeyInstance(undefined)
|
||||
|
||||
await this.handleKeyStatusChange()
|
||||
}
|
||||
|
||||
private getWrappedRootKey() {
|
||||
return this.storageService.getValue<Models.EncryptedTransferPayload>(
|
||||
Services.StorageKey.WrappedRootKey,
|
||||
Services.StorageValueModes.Nonwrapped,
|
||||
)
|
||||
}
|
||||
|
||||
public setRootKeyInstance(rootKey: RootKeyInterface | undefined): void {
|
||||
this.rootKey = rootKey
|
||||
}
|
||||
|
||||
public getRootKey(): RootKeyInterface | undefined {
|
||||
return this.rootKey
|
||||
}
|
||||
|
||||
private getSureRootKey(): RootKeyInterface {
|
||||
return this.rootKey as RootKeyInterface
|
||||
}
|
||||
|
||||
private getItemsKeys() {
|
||||
return this.itemManager.getDisplayableItemsKeys()
|
||||
}
|
||||
|
||||
public async encrypPayloadWithKeyLookup(payload: Models.DecryptedPayloadInterface): Promise<EncryptedParameters> {
|
||||
const key = this.getRootKey()
|
||||
|
||||
if (key == undefined) {
|
||||
throw Error('Attempting root key encryption with no root key')
|
||||
}
|
||||
|
||||
return this.encryptPayload(payload, key)
|
||||
}
|
||||
|
||||
public async encryptPayloadsWithKeyLookup(
|
||||
payloads: Models.DecryptedPayloadInterface[],
|
||||
): Promise<EncryptedParameters[]> {
|
||||
return Promise.all(payloads.map((payload) => this.encrypPayloadWithKeyLookup(payload)))
|
||||
}
|
||||
|
||||
public async encryptPayload(
|
||||
payload: Models.DecryptedPayloadInterface,
|
||||
key: RootKeyInterface,
|
||||
): Promise<EncryptedParameters> {
|
||||
return OperatorWrapper.encryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
|
||||
public async encryptPayloads(payloads: Models.DecryptedPayloadInterface[], key: RootKeyInterface) {
|
||||
return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key)))
|
||||
}
|
||||
|
||||
public async decryptPayloadWithKeyLookup<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payload: Models.EncryptedPayloadInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
const key = this.getRootKey()
|
||||
|
||||
if (key == undefined) {
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
errorDecrypting: true,
|
||||
waitingForKey: true,
|
||||
}
|
||||
}
|
||||
|
||||
return this.decryptPayload(payload, key)
|
||||
}
|
||||
|
||||
public async decryptPayload<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payload: Models.EncryptedPayloadInterface,
|
||||
key: RootKeyInterface,
|
||||
): Promise<DecryptedParameters<C> | ErrorDecryptingParameters> {
|
||||
return OperatorWrapper.decryptPayload(payload, key, this.operatorManager)
|
||||
}
|
||||
|
||||
public async decryptPayloadsWithKeyLookup<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payloads: Models.EncryptedPayloadInterface[],
|
||||
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
|
||||
return Promise.all(payloads.map((payload) => this.decryptPayloadWithKeyLookup<C>(payload)))
|
||||
}
|
||||
|
||||
public async decryptPayloads<C extends Models.ItemContent = Models.ItemContent>(
|
||||
payloads: Models.EncryptedPayloadInterface[],
|
||||
key: RootKeyInterface,
|
||||
): Promise<(DecryptedParameters<C> | ErrorDecryptingParameters)[]> {
|
||||
return Promise.all(payloads.map((payload) => this.decryptPayload<C>(payload, key)))
|
||||
}
|
||||
|
||||
/**
|
||||
* When the root key changes (non-null only), we must re-encrypt all items
|
||||
* keys with this new root key (by simply re-syncing).
|
||||
*/
|
||||
public async reencryptItemsKeys(): Promise<void> {
|
||||
const itemsKeys = this.getItemsKeys()
|
||||
|
||||
if (itemsKeys.length > 0) {
|
||||
/**
|
||||
* Do not call sync after marking dirty.
|
||||
* Re-encrypting items keys is called by consumers who have specific flows who
|
||||
* will sync on their own timing
|
||||
*/
|
||||
await this.itemManager.setItemsDirty(itemsKeys)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new random items key to use for item encryption, and adds it to model management.
|
||||
* Consumer must call sync. If the protocol version <= 003, only one items key should be created,
|
||||
* and its .itemsKey value should be equal to the root key masterKey value.
|
||||
*/
|
||||
public async createNewDefaultItemsKey(): Promise<Models.ItemsKeyInterface> {
|
||||
const rootKey = this.getSureRootKey()
|
||||
const operatorVersion = rootKey ? rootKey.keyVersion : Common.ProtocolVersionLatest
|
||||
let itemTemplate: Models.ItemsKeyInterface
|
||||
|
||||
if (Common.compareVersions(operatorVersion, Common.ProtocolVersionLastNonrootItemsKey) <= 0) {
|
||||
/** Create root key based items key */
|
||||
const payload = new DecryptedPayload<ItemsKeyContent>({
|
||||
uuid: UuidGenerator.GenerateUuid(),
|
||||
content_type: Common.ContentType.ItemsKey,
|
||||
content: Models.FillItemContentSpecialized<ItemsKeyContentSpecialized, ItemsKeyContent>({
|
||||
itemsKey: rootKey.masterKey,
|
||||
dataAuthenticationKey: rootKey.dataAuthenticationKey,
|
||||
version: operatorVersion,
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
})
|
||||
itemTemplate = Models.CreateDecryptedItemFromPayload(payload)
|
||||
} else {
|
||||
/** Create independent items key */
|
||||
itemTemplate = this.operatorManager.operatorForVersion(operatorVersion).createItemsKey()
|
||||
}
|
||||
|
||||
const itemsKeys = this.getItemsKeys()
|
||||
const defaultKeys = itemsKeys.filter((key) => {
|
||||
return key.isDefault
|
||||
})
|
||||
|
||||
for (const key of defaultKeys) {
|
||||
await this.itemManager.changeItemsKey(key, (mutator) => {
|
||||
mutator.isDefault = false
|
||||
})
|
||||
}
|
||||
|
||||
const itemsKey = (await this.itemManager.insertItem(itemTemplate)) as Models.ItemsKeyInterface
|
||||
|
||||
await this.itemManager.changeItemsKey(itemsKey, (mutator) => {
|
||||
mutator.isDefault = true
|
||||
})
|
||||
|
||||
return itemsKey
|
||||
}
|
||||
|
||||
public async createNewItemsKeyWithRollback(): Promise<() => Promise<void>> {
|
||||
const currentDefaultItemsKey = findDefaultItemsKey(this.getItemsKeys())
|
||||
const newDefaultItemsKey = await this.createNewDefaultItemsKey()
|
||||
|
||||
const rollback = async () => {
|
||||
await this.itemManager.setItemToBeDeleted(newDefaultItemsKey)
|
||||
|
||||
if (currentDefaultItemsKey) {
|
||||
await this.itemManager.changeItem<ItemsKeyMutator>(currentDefaultItemsKey, (mutator) => {
|
||||
mutator.isDefault = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return rollback
|
||||
}
|
||||
|
||||
override async getDiagnostics(): Promise<Services.DiagnosticInfo | undefined> {
|
||||
return {
|
||||
rootKeyEncryption: {
|
||||
hasRootKey: this.rootKey != undefined,
|
||||
keyMode: KeyMode[this.keyMode],
|
||||
hasRootKeyWrapper: await this.hasRootKeyWrapper(),
|
||||
hasAccount: this.hasAccount(),
|
||||
hasRootKeyEncryptionSource: this.hasRootKeyEncryptionSource(),
|
||||
hasPasscode: this.hasPasscode(),
|
||||
getEncryptionSourceVersion: this.hasRootKeyEncryptionSource() && (await this.getEncryptionSourceVersion()),
|
||||
getUserVersion: this.getUserVersion(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import {
|
||||
DecryptedPayloadInterface,
|
||||
EncryptedPayloadInterface,
|
||||
ItemsKeyInterface,
|
||||
PayloadInterface,
|
||||
RootKeyInterface,
|
||||
} from '@standardnotes/models'
|
||||
import { EncryptionTypeSplit } from './EncryptionTypeSplit'
|
||||
|
||||
export interface AbstractKeySplit<T = EncryptedPayloadInterface | DecryptedPayloadInterface> {
|
||||
usesRootKey?: {
|
||||
items: T[]
|
||||
key: RootKeyInterface
|
||||
}
|
||||
usesItemsKey?: {
|
||||
items: T[]
|
||||
key: ItemsKeyInterface
|
||||
}
|
||||
usesRootKeyWithKeyLookup?: {
|
||||
items: T[]
|
||||
}
|
||||
usesItemsKeyWithKeyLookup?: {
|
||||
items: T[]
|
||||
}
|
||||
}
|
||||
|
||||
export type KeyedEncryptionSplit = AbstractKeySplit<DecryptedPayloadInterface>
|
||||
export type KeyedDecryptionSplit = AbstractKeySplit<EncryptedPayloadInterface>
|
||||
|
||||
export function CreateEncryptionSplitWithKeyLookup(
|
||||
payloadSplit: EncryptionTypeSplit<DecryptedPayloadInterface>,
|
||||
): KeyedEncryptionSplit {
|
||||
const result: KeyedEncryptionSplit = {}
|
||||
|
||||
if (payloadSplit.rootKeyEncryption) {
|
||||
result.usesRootKeyWithKeyLookup = { items: payloadSplit.rootKeyEncryption }
|
||||
}
|
||||
|
||||
if (payloadSplit.itemsKeyEncryption) {
|
||||
result.usesItemsKeyWithKeyLookup = { items: payloadSplit.itemsKeyEncryption }
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function CreateDecryptionSplitWithKeyLookup(
|
||||
payloadSplit: EncryptionTypeSplit<EncryptedPayloadInterface>,
|
||||
): KeyedDecryptionSplit {
|
||||
const result: KeyedDecryptionSplit = {}
|
||||
|
||||
if (payloadSplit.rootKeyEncryption) {
|
||||
result.usesRootKeyWithKeyLookup = { items: payloadSplit.rootKeyEncryption }
|
||||
}
|
||||
|
||||
if (payloadSplit.itemsKeyEncryption) {
|
||||
result.usesItemsKeyWithKeyLookup = { items: payloadSplit.itemsKeyEncryption }
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function FindPayloadInEncryptionSplit(uuid: Uuid, split: KeyedEncryptionSplit): DecryptedPayloadInterface {
|
||||
const inUsesItemsKey = split.usesItemsKey?.items.find((item: PayloadInterface) => item.uuid === uuid)
|
||||
if (inUsesItemsKey) {
|
||||
return inUsesItemsKey
|
||||
}
|
||||
|
||||
const inUsesRootKey = split.usesRootKey?.items.find((item) => item.uuid === uuid)
|
||||
if (inUsesRootKey) {
|
||||
return inUsesRootKey
|
||||
}
|
||||
|
||||
const inUsesItemsKeyWithKeyLookup = split.usesItemsKeyWithKeyLookup?.items.find((item) => item.uuid === uuid)
|
||||
if (inUsesItemsKeyWithKeyLookup) {
|
||||
return inUsesItemsKeyWithKeyLookup
|
||||
}
|
||||
|
||||
const inUsesRootKeyWithKeyLookup = split.usesRootKeyWithKeyLookup?.items.find((item) => item.uuid === uuid)
|
||||
if (inUsesRootKeyWithKeyLookup) {
|
||||
return inUsesRootKeyWithKeyLookup
|
||||
}
|
||||
|
||||
throw Error('Cannot find payload in encryption split')
|
||||
}
|
||||
|
||||
export function FindPayloadInDecryptionSplit(uuid: Uuid, split: KeyedDecryptionSplit): EncryptedPayloadInterface {
|
||||
const inUsesItemsKey = split.usesItemsKey?.items.find((item: PayloadInterface) => item.uuid === uuid)
|
||||
if (inUsesItemsKey) {
|
||||
return inUsesItemsKey
|
||||
}
|
||||
|
||||
const inUsesRootKey = split.usesRootKey?.items.find((item) => item.uuid === uuid)
|
||||
if (inUsesRootKey) {
|
||||
return inUsesRootKey
|
||||
}
|
||||
|
||||
const inUsesItemsKeyWithKeyLookup = split.usesItemsKeyWithKeyLookup?.items.find((item) => item.uuid === uuid)
|
||||
if (inUsesItemsKeyWithKeyLookup) {
|
||||
return inUsesItemsKeyWithKeyLookup
|
||||
}
|
||||
|
||||
const inUsesRootKeyWithKeyLookup = split.usesRootKeyWithKeyLookup?.items.find((item) => item.uuid === uuid)
|
||||
if (inUsesRootKeyWithKeyLookup) {
|
||||
return inUsesRootKeyWithKeyLookup
|
||||
}
|
||||
|
||||
throw Error('Cannot find payload in encryption split')
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DecryptedPayloadInterface, EncryptedPayloadInterface } from '@standardnotes/models'
|
||||
|
||||
export interface EncryptionTypeSplit<T = EncryptedPayloadInterface | DecryptedPayloadInterface> {
|
||||
rootKeyEncryption?: T[]
|
||||
itemsKeyEncryption?: T[]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { DecryptedPayloadInterface, EncryptedPayloadInterface } from '@standardnotes/models'
|
||||
import { ItemContentTypeUsesRootKeyEncryption } from '../Keys/RootKey/Functions'
|
||||
import { EncryptionTypeSplit } from './EncryptionTypeSplit'
|
||||
|
||||
export function SplitPayloadsByEncryptionType<T extends EncryptedPayloadInterface | DecryptedPayloadInterface>(
|
||||
payloads: T[],
|
||||
): EncryptionTypeSplit<T> {
|
||||
const usesRootKey: T[] = []
|
||||
const usesItemsKey: T[] = []
|
||||
|
||||
for (const item of payloads) {
|
||||
if (ItemContentTypeUsesRootKeyEncryption(item.content_type)) {
|
||||
usesRootKey.push(item)
|
||||
} else {
|
||||
usesItemsKey.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rootKeyEncryption: usesRootKey.length > 0 ? usesRootKey : undefined,
|
||||
itemsKeyEncryption: usesItemsKey.length > 0 ? usesItemsKey : undefined,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class StandardException {
|
||||
constructor(public readonly message: string, log = false) {
|
||||
if (log) {
|
||||
console.error('StandardException raised: ', message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { EncryptedPayloadInterface, ItemContent } from '@standardnotes/models'
|
||||
|
||||
export type EncryptedParameters = {
|
||||
uuid: string
|
||||
content: string
|
||||
items_key_id: string | undefined
|
||||
enc_item_key: string
|
||||
version: ProtocolVersion
|
||||
|
||||
/** @deprecated */
|
||||
auth_hash?: string
|
||||
}
|
||||
|
||||
export type DecryptedParameters<C extends ItemContent = ItemContent> = {
|
||||
uuid: string
|
||||
content: C
|
||||
}
|
||||
|
||||
export type ErrorDecryptingParameters = {
|
||||
uuid: string
|
||||
errorDecrypting: true
|
||||
waitingForKey?: boolean
|
||||
}
|
||||
|
||||
export function isErrorDecryptingParameters(
|
||||
x: EncryptedParameters | DecryptedParameters | ErrorDecryptingParameters,
|
||||
): x is ErrorDecryptingParameters {
|
||||
return (x as ErrorDecryptingParameters).errorDecrypting
|
||||
}
|
||||
|
||||
export function encryptedParametersFromPayload(payload: EncryptedPayloadInterface): EncryptedParameters {
|
||||
return {
|
||||
uuid: payload.uuid,
|
||||
content: payload.content,
|
||||
items_key_id: payload.items_key_id,
|
||||
enc_item_key: payload.enc_item_key as string,
|
||||
version: payload.version,
|
||||
auth_hash: payload.auth_hash,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Uuid, ProtocolVersion } from '@standardnotes/common'
|
||||
|
||||
export type ItemAuthenticatedData = {
|
||||
u: Uuid
|
||||
v: ProtocolVersion
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||
|
||||
/**
|
||||
* <= V003 optionally included key params content as last component in encrypted string
|
||||
* as a json stringified base64 representation. This data is attached but not included
|
||||
* in authentication hash.
|
||||
*/
|
||||
export type LegacyAttachedData = AnyKeyParamsContent & Record<string, unknown>
|
||||
@@ -0,0 +1,8 @@
|
||||
import { AnyKeyParamsContent } from '@standardnotes/common'
|
||||
import { ItemAuthenticatedData } from './ItemAuthenticatedData'
|
||||
|
||||
/** Data that is attached to items that are encrypted with a root key */
|
||||
export type RootKeyEncryptedAuthenticatedData = ItemAuthenticatedData & {
|
||||
/** The key params used to generate the root key that encrypts this item key */
|
||||
kp: AnyKeyParamsContent
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export async function ComputePrivateWorkspaceIdentifier(
|
||||
crypto: PureCryptoInterface,
|
||||
userphrase: string,
|
||||
name: string,
|
||||
): Promise<string | undefined> {
|
||||
const identifier = await crypto.hmac256(
|
||||
await crypto.sha256(name.trim().toLowerCase()),
|
||||
await crypto.sha256(userphrase.trim().toLowerCase()),
|
||||
)
|
||||
|
||||
if (identifier == undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return identifier
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export * from './Backups/BackupFile'
|
||||
export * from './Backups/BackupFileDecryptor'
|
||||
export * from './Backups/BackupFileType'
|
||||
export * from './Keys/ItemsKey/ItemsKey'
|
||||
export * from './Keys/ItemsKey/ItemsKeyMutator'
|
||||
export * from './Keys/ItemsKey/Registration'
|
||||
export * from './Keys/RootKey/Functions'
|
||||
export * from './Keys/RootKey/KeyParamsFunctions'
|
||||
export * from './Keys/RootKey/ProtocolVersionForKeyParams'
|
||||
export * from './Keys/RootKey/RootKey'
|
||||
export * from './Keys/RootKey/RootKeyParams'
|
||||
export * from './Keys/RootKey/ValidKeyParamsKeys'
|
||||
export * from './Keys/Utils/DecryptItemsKey'
|
||||
export * from './Keys/Utils/KeyRecoveryStrings'
|
||||
export * from './Operator/001/Operator001'
|
||||
export * from './Operator/002/Operator002'
|
||||
export * from './Operator/003/Operator003'
|
||||
export * from './Operator/004/Operator004'
|
||||
export * from './Operator/Functions'
|
||||
export * from './Operator/Operator'
|
||||
export * from './Operator/OperatorManager'
|
||||
export * from './Operator/OperatorWrapper'
|
||||
export * from './Service/Encryption/EncryptionProvider'
|
||||
export * from './Service/Encryption/EncryptionService'
|
||||
export * from './Service/Functions'
|
||||
export * from './Service/Items/ItemsEncryption'
|
||||
export * from './Service/RootKey/KeyMode'
|
||||
export * from './Service/RootKey/RootKeyEncryption'
|
||||
export * from './Split/EncryptionSplit'
|
||||
export * from './Split/EncryptionTypeSplit'
|
||||
export * from './Split/Functions'
|
||||
export * from './Types/EncryptedParameters'
|
||||
export * from './Types/ItemAuthenticatedData'
|
||||
export * from './Types/LegacyAttachedData'
|
||||
export * from './Types/RootKeyEncryptedAuthenticatedData'
|
||||
export * from './Workspace/PrivateWorkspace'
|
||||
export * from './Algorithm'
|
||||
export * from './StandardException'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './Domain'
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"references": [],
|
||||
"exclude": ["**/*.spec.ts", "dist", "node_modules"]
|
||||
}
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.27.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.27.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
# [3.27.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
### Features
|
||||
|
||||
* add @standardnotes/encryption package ([#1199](https://github.com/standardnotes/app/issues/1199)) ([e5771fc](https://github.com/standardnotes/app/commit/e5771fcbde827afd178edde3b7bd872734cd3338))
|
||||
|
||||
## [3.26.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.26.14",
|
||||
"version": "3.27.2",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -36,6 +36,7 @@
|
||||
"@react-navigation/native": "^6.0.10",
|
||||
"@react-navigation/stack": "^6.2.1",
|
||||
"@standardnotes/components-meta": "workspace:*",
|
||||
"@standardnotes/encryption": "workspace:^",
|
||||
"@standardnotes/filepicker": "^1.16.23",
|
||||
"@standardnotes/icons": "workspace:*",
|
||||
"@standardnotes/react-native-aes": "^1.4.3",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { MobileTheme } from '@Root/Style/MobileTheme'
|
||||
import { ComponentChecksumsType } from '@standardnotes/components-meta'
|
||||
import RawComponentChecksumsFile from '@standardnotes/components-meta/dist/zips/checksums.json'
|
||||
import { EncryptionService } from '@standardnotes/encryption'
|
||||
import { FeatureDescription, FeatureIdentifier, GetFeatures } from '@standardnotes/features'
|
||||
import {
|
||||
ComponentMutator,
|
||||
EncryptionService,
|
||||
isRightVersionGreaterThanLeft,
|
||||
PermissionDialog,
|
||||
SNApplication,
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.49](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.48](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.47](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.2.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.2.46",
|
||||
"version": "1.2.49",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.57](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.56](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.55](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.54](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-04)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web-server",
|
||||
"version": "1.2.54",
|
||||
"version": "1.2.57",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"author": "Standard Notes.",
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.33.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.33.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.33.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-05)
|
||||
|
||||
### Features
|
||||
|
||||
* add @standardnotes/encryption package ([#1199](https://github.com/standardnotes/app/issues/1199)) ([e5771fc](https://github.com/standardnotes/app/commit/e5771fcbde827afd178edde3b7bd872734cd3338))
|
||||
|
||||
# [3.32.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-07-04)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.32.0",
|
||||
"version": "3.33.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes",
|
||||
@@ -68,6 +68,7 @@
|
||||
"@reach/listbox": "^0.16.2",
|
||||
"@reach/tooltip": "^0.16.2",
|
||||
"@reach/visually-hidden": "^0.16.0",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/filepicker": "1.16.23",
|
||||
"@standardnotes/icons": "workspace:*",
|
||||
"@standardnotes/services": "^1.13.23",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
STRING_LOCAL_ENC_ENABLED,
|
||||
STRING_ENC_NOT_ENABLED,
|
||||
} from '@/Constants/Strings'
|
||||
import { BackupFile } from '@standardnotes/snjs'
|
||||
import { BackupFile } from '@standardnotes/encryption'
|
||||
import { ChangeEventHandler, MouseEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
|
||||
@@ -2,11 +2,11 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { parseFileName } from '@standardnotes/filepicker'
|
||||
import {
|
||||
ContentType,
|
||||
BackupFile,
|
||||
BackupFileDecryptedContextualPayload,
|
||||
NoteContent,
|
||||
EncryptedItemInterface,
|
||||
} from '@standardnotes/snjs'
|
||||
import { BackupFile } from '@standardnotes/encryption'
|
||||
|
||||
function sanitizeFileName(name: string): string {
|
||||
return name.trim().replace(/[.\\/:"?*|<>]/g, '_')
|
||||
|
||||
@@ -6165,7 +6165,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/config@npm:^2.4.3":
|
||||
"@standardnotes/config@npm:2.4.3, @standardnotes/config@npm:^2.4.3":
|
||||
version: 2.4.3
|
||||
resolution: "@standardnotes/config@npm:2.4.3"
|
||||
dependencies:
|
||||
@@ -6326,27 +6326,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@standardnotes/encryption@npm:^1.8.22":
|
||||
version: 1.8.22
|
||||
resolution: "@standardnotes/encryption@npm:1.8.22"
|
||||
dependencies:
|
||||
"@standardnotes/models": ^1.11.12
|
||||
"@standardnotes/responses": ^1.6.38
|
||||
"@standardnotes/services": ^1.13.22
|
||||
checksum: 7a571c4b257b7469054b37930b4157fa786b81558d0d7887fc5db0527c7eb5acd1d7ecdb90509d50652440017e708eb87d8820cf0df52b3c7a8628940d0e0e72
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@standardnotes/encryption@npm:^1.8.23":
|
||||
version: 1.8.23
|
||||
resolution: "@standardnotes/encryption@npm:1.8.23"
|
||||
"@standardnotes/encryption@^1.8.22, @standardnotes/encryption@^1.8.23, @standardnotes/encryption@workspace:*, @standardnotes/encryption@workspace:^, @standardnotes/encryption@workspace:packages/encryption":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@standardnotes/encryption@workspace:packages/encryption"
|
||||
dependencies:
|
||||
"@standardnotes/common": ^1.23.1
|
||||
"@standardnotes/config": 2.4.3
|
||||
"@standardnotes/models": ^1.11.13
|
||||
"@standardnotes/responses": ^1.6.39
|
||||
"@standardnotes/services": ^1.13.23
|
||||
checksum: b86df01dc7d76eb170bd5d7cbe17b04d4c77ff657fda19cfc66a4e72d143016407ffc1434063e19d7f80a7a953df113a60712cba4a22be4a7ab7f125311a77e5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
"@standardnotes/sncrypto-common": ^1.9.0
|
||||
"@standardnotes/utils": ^1.6.12
|
||||
"@types/jest": ^27.4.1
|
||||
"@types/node": ^18.0.0
|
||||
"@typescript-eslint/eslint-plugin": ^5.30.0
|
||||
eslint-plugin-prettier: ^4.2.1
|
||||
jest: ^27.5.1
|
||||
reflect-metadata: ^0.1.13
|
||||
ts-jest: ^27.1.3
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@standardnotes/eslint-config-extensions@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
@@ -6721,6 +6720,7 @@ __metadata:
|
||||
"@react-navigation/stack": ^6.2.1
|
||||
"@standardnotes/components-meta": "workspace:*"
|
||||
"@standardnotes/config": ^2.4.3
|
||||
"@standardnotes/encryption": "workspace:^"
|
||||
"@standardnotes/filepicker": ^1.16.23
|
||||
"@standardnotes/icons": "workspace:*"
|
||||
"@standardnotes/react-native-aes": ^1.4.3
|
||||
@@ -7215,6 +7215,7 @@ __metadata:
|
||||
"@reach/listbox": ^0.16.2
|
||||
"@reach/tooltip": ^0.16.2
|
||||
"@reach/visually-hidden": ^0.16.0
|
||||
"@standardnotes/encryption": "workspace:*"
|
||||
"@standardnotes/filepicker": 1.16.23
|
||||
"@standardnotes/icons": "workspace:*"
|
||||
"@standardnotes/services": ^1.13.23
|
||||
@@ -8223,6 +8224,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^18.0.0":
|
||||
version: 18.0.1
|
||||
resolution: "@types/node@npm:18.0.1"
|
||||
checksum: be14b251c54cc2b4ca78ac6eadf2fe5e831e487f2e17848f21d576295945b538271dcc674d0bba582b3f8d95b84f6826e99b6ba4710c76f165a8bdd4d4f0618e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/normalize-package-data@npm:^2.4.0":
|
||||
version: 2.4.1
|
||||
resolution: "@types/normalize-package-data@npm:2.4.1"
|
||||
@@ -8910,6 +8918,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:^5.30.0":
|
||||
version: 5.30.4
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:5.30.4"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": 5.30.4
|
||||
"@typescript-eslint/type-utils": 5.30.4
|
||||
"@typescript-eslint/utils": 5.30.4
|
||||
debug: ^4.3.4
|
||||
functional-red-black-tree: ^1.0.1
|
||||
ignore: ^5.2.0
|
||||
regexpp: ^3.2.0
|
||||
semver: ^7.3.7
|
||||
tsutils: ^3.21.0
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^5.0.0
|
||||
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 9b9290448b3009b93dc9bbc263049103f48c006d395351695486f7ab156f24b3f3e9e83a9f68a8cf73afc036c2d1092005446085f171afc9cdcb0b1b475443e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/experimental-utils@npm:^5.0.0":
|
||||
version: 5.30.0
|
||||
resolution: "@typescript-eslint/experimental-utils@npm:5.30.0"
|
||||
@@ -8948,6 +8979,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:5.30.4":
|
||||
version: 5.30.4
|
||||
resolution: "@typescript-eslint/scope-manager@npm:5.30.4"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": 5.30.4
|
||||
"@typescript-eslint/visitor-keys": 5.30.4
|
||||
checksum: 3da442dc113ee821c6b1c4510ee4cfd6f6f34838587785b7c486d262af913dca66229a47ebb9a63ad605f8edbe57a8387be24c817c8091783b57c33b7862cbcc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:5.30.0"
|
||||
@@ -8964,6 +9005,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:5.30.4":
|
||||
version: 5.30.4
|
||||
resolution: "@typescript-eslint/type-utils@npm:5.30.4"
|
||||
dependencies:
|
||||
"@typescript-eslint/utils": 5.30.4
|
||||
debug: ^4.3.4
|
||||
tsutils: ^3.21.0
|
||||
peerDependencies:
|
||||
eslint: "*"
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 552eb1a5b11787d3b98dc454a80153b05bcb6d58aeb97c861d6b006f3eb6af95d117a3f9a679b41a8b6d58ac0dceaaeafd23ce28d83881a363e51bbc1a088936
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@typescript-eslint/types@npm:5.30.0"
|
||||
@@ -8971,6 +9028,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:5.30.4":
|
||||
version: 5.30.4
|
||||
resolution: "@typescript-eslint/types@npm:5.30.4"
|
||||
checksum: 06181c33551850492ccfd48232f93083c6cf9205d26b26fe6e356b7a4ebb08beffd89ae3b84011da94ffd0e6948422d91d94df7005edeca1c8348117d4776715
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:5.30.0"
|
||||
@@ -8989,6 +9053,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:5.30.4":
|
||||
version: 5.30.4
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:5.30.4"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": 5.30.4
|
||||
"@typescript-eslint/visitor-keys": 5.30.4
|
||||
debug: ^4.3.4
|
||||
globby: ^11.1.0
|
||||
is-glob: ^4.0.3
|
||||
semver: ^7.3.7
|
||||
tsutils: ^3.21.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
checksum: 1aaa414993a4e35927e93f929d34a6e07cb8ec46c4ea9a58f018c36ff1fc3027ca5007e6abe922c5869557cd2d7f319e8c57cccd618517781979e669d3b704d0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:5.30.0, @typescript-eslint/utils@npm:^5.13.0":
|
||||
version: 5.30.0
|
||||
resolution: "@typescript-eslint/utils@npm:5.30.0"
|
||||
@@ -9005,6 +9087,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:5.30.4":
|
||||
version: 5.30.4
|
||||
resolution: "@typescript-eslint/utils@npm:5.30.4"
|
||||
dependencies:
|
||||
"@types/json-schema": ^7.0.9
|
||||
"@typescript-eslint/scope-manager": 5.30.4
|
||||
"@typescript-eslint/types": 5.30.4
|
||||
"@typescript-eslint/typescript-estree": 5.30.4
|
||||
eslint-scope: ^5.1.1
|
||||
eslint-utils: ^3.0.0
|
||||
peerDependencies:
|
||||
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
checksum: 0f680d366701c6ca5a4e1fc53247876e6f6acaee498400d32a7cb767e6187d0d75bc4488350cf6dc6e7c373ea023d62e395ea14ba56ad246b458d6853b213782
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:5.30.0":
|
||||
version: 5.30.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:5.30.0"
|
||||
@@ -9015,6 +9113,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:5.30.4":
|
||||
version: 5.30.4
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:5.30.4"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": 5.30.4
|
||||
eslint-visitor-keys: ^3.3.0
|
||||
checksum: ec39680a89b058e8350adc084c2a957e83161e87ac9732c9fef8fd3045ce5004e059b2ddb0c366192806e3998bf3263c8bbb2cc74a4f2ad3313154ed35dd479a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@uiw/react-codemirror@npm:4.5.1":
|
||||
version: 4.5.1
|
||||
resolution: "@uiw/react-codemirror@npm:4.5.1"
|
||||
@@ -17105,6 +17213,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-prettier@npm:^4.2.1":
|
||||
version: 4.2.1
|
||||
resolution: "eslint-plugin-prettier@npm:4.2.1"
|
||||
dependencies:
|
||||
prettier-linter-helpers: ^1.0.0
|
||||
peerDependencies:
|
||||
eslint: ">=7.28.0"
|
||||
prettier: ">=2.0.0"
|
||||
peerDependenciesMeta:
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
checksum: b9e839d2334ad8ec7a5589c5cb0f219bded260839a857d7a486997f9870e95106aa59b8756ff3f37202085ebab658de382b0267cae44c3a7f0eb0bcc03a4f6d6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint-plugin-promise@npm:^4.3.1":
|
||||
version: 4.3.1
|
||||
resolution: "eslint-plugin-promise@npm:4.3.1"
|
||||
@@ -31903,6 +32026,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reflect-metadata@npm:^0.1.13":
|
||||
version: 0.1.13
|
||||
resolution: "reflect-metadata@npm:0.1.13"
|
||||
checksum: 798d379a7b6f6455501145419505c97dd11cbc23857a386add2b9ef15963ccf15a48d9d15507afe01d4cd74116df8a213247200bac00320bd7c11ddeaa5e8fb4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"refractor@npm:^4.0.0":
|
||||
version: 4.7.0
|
||||
resolution: "refractor@npm:4.7.0"
|
||||
@@ -35978,7 +36108,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-jest@npm:^27.1.4":
|
||||
"ts-jest@npm:^27.1.3, ts-jest@npm:^27.1.4":
|
||||
version: 27.1.5
|
||||
resolution: "ts-jest@npm:27.1.5"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user