feat: add @standardnotes/encryption package (#1199)
* feat: add @standardnotes/encryption package * fix: mobile dependency on encryption package * fix: order of build & lint in pr workflows * fix: web dependency on encryption package * fix: remove encryption package composite configuration * fix: import order
This commit is contained in:
4
.github/workflows/pr.components.yml
vendored
4
.github/workflows/pr.components.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/pr.yml
vendored
4
.github/workflows/pr.yml
vendored
@@ -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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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.
BIN
.yarn/cache/@types-node-npm-18.0.1-35e22b3e26-be14b251c5.zip
vendored
Normal file
BIN
.yarn/cache/@types-node-npm-18.0.1-35e22b3e26-be14b251c5.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-eslint-plugin-npm-5.30.4-08f53c0ede-9b9290448b.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-eslint-plugin-npm-5.30.4-08f53c0ede-9b9290448b.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-scope-manager-npm-5.30.4-8b6cf23765-3da442dc11.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-scope-manager-npm-5.30.4-8b6cf23765-3da442dc11.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-type-utils-npm-5.30.4-f2696bf1f1-552eb1a5b1.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-type-utils-npm-5.30.4-f2696bf1f1-552eb1a5b1.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-types-npm-5.30.4-c748bd84f1-06181c3355.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-types-npm-5.30.4-c748bd84f1-06181c3355.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-typescript-estree-npm-5.30.4-7c97ea55f3-1aaa414993.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-typescript-estree-npm-5.30.4-7c97ea55f3-1aaa414993.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-utils-npm-5.30.4-6bd4c125c1-0f680d3667.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-utils-npm-5.30.4-6bd4c125c1-0f680d3667.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/@typescript-eslint-visitor-keys-npm-5.30.4-19f4a2caf4-ec39680a89.zip
vendored
Normal file
BIN
.yarn/cache/@typescript-eslint-visitor-keys-npm-5.30.4-19f4a2caf4-ec39680a89.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/eslint-plugin-prettier-npm-4.2.1-ba8e1240f1-b9e839d233.zip
vendored
Normal file
BIN
.yarn/cache/eslint-plugin-prettier-npm-4.2.1-ba8e1240f1-b9e839d233.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/reflect-metadata-npm-0.1.13-c525998e20-798d379a7b.zip
vendored
Normal file
BIN
.yarn/cache/reflect-metadata-npm-0.1.13-c525998e20-798d379a7b.zip
vendored
Normal file
Binary file not shown.
2
packages/encryption/.eslintignore
Normal file
2
packages/encryption/.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
10
packages/encryption/.eslintrc
Normal file
10
packages/encryption/.eslintrc
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
361
packages/encryption/CHANGELOG.md
Normal file
361
packages/encryption/CHANGELOG.md
Normal file
@@ -0,0 +1,361 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.8.24](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.23...@standardnotes/encryption@1.8.24) (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/encryption@1.8.22...@standardnotes/encryption@1.8.23) (2022-06-29)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.22](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.21...@standardnotes/encryption@1.8.22) (2022-06-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.21](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.20...@standardnotes/encryption@1.8.21) (2022-06-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.20](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.19...@standardnotes/encryption@1.8.20) (2022-06-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.19](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.18...@standardnotes/encryption@1.8.19) (2022-06-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.18](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.17...@standardnotes/encryption@1.8.18) (2022-06-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.17](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.16...@standardnotes/encryption@1.8.17) (2022-06-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.16](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.15...@standardnotes/encryption@1.8.16) (2022-06-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.15](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.14...@standardnotes/encryption@1.8.15) (2022-06-10)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.14](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.13...@standardnotes/encryption@1.8.14) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.13](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.12...@standardnotes/encryption@1.8.13) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.12](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.11...@standardnotes/encryption@1.8.12) (2022-06-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.11](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.10...@standardnotes/encryption@1.8.11) (2022-06-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.10](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.9...@standardnotes/encryption@1.8.10) (2022-06-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.9](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.8...@standardnotes/encryption@1.8.9) (2022-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.8](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.7...@standardnotes/encryption@1.8.8) (2022-06-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.7](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.6...@standardnotes/encryption@1.8.7) (2022-06-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.6](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.5...@standardnotes/encryption@1.8.6) (2022-05-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.5](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.4...@standardnotes/encryption@1.8.5) (2022-05-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.4](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.3...@standardnotes/encryption@1.8.4) (2022-05-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.3](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.2...@standardnotes/encryption@1.8.3) (2022-05-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.2](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.1...@standardnotes/encryption@1.8.2) (2022-05-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.8.1](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.8.0...@standardnotes/encryption@1.8.1) (2022-05-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.8.0](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.12...@standardnotes/encryption@1.8.0) (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/encryption@1.7.11...@standardnotes/encryption@1.7.12) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.11](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.10...@standardnotes/encryption@1.7.11) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.10](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.9...@standardnotes/encryption@1.7.10) (2022-05-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.9](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.8...@standardnotes/encryption@1.7.9) (2022-05-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.8](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.7...@standardnotes/encryption@1.7.8) (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/encryption@1.7.6...@standardnotes/encryption@1.7.7) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.6](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.5...@standardnotes/encryption@1.7.6) (2022-05-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.5](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.4...@standardnotes/encryption@1.7.5) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.4](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.3...@standardnotes/encryption@1.7.4) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.3](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.2...@standardnotes/encryption@1.7.3) (2022-05-16)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.2](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.1...@standardnotes/encryption@1.7.2) (2022-05-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.7.1](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.7.0...@standardnotes/encryption@1.7.1) (2022-05-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.7.0](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.12...@standardnotes/encryption@1.7.0) (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/encryption@1.6.11...@standardnotes/encryption@1.6.12) (2022-05-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.11](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.10...@standardnotes/encryption@1.6.11) (2022-05-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.10](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.9...@standardnotes/encryption@1.6.10) (2022-05-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.9](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.8...@standardnotes/encryption@1.6.9) (2022-05-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.8](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.7...@standardnotes/encryption@1.6.8) (2022-05-09)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.7](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.6...@standardnotes/encryption@1.6.7) (2022-05-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.6](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.5...@standardnotes/encryption@1.6.6) (2022-05-06)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.5](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.4...@standardnotes/encryption@1.6.5) (2022-05-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.4](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.2...@standardnotes/encryption@1.6.4) (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/encryption@1.6.2...@standardnotes/encryption@1.6.3) (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/encryption@1.6.1...@standardnotes/encryption@1.6.2) (2022-05-03)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.6.1](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.6.0...@standardnotes/encryption@1.6.1) (2022-05-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.6.0](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.5.2...@standardnotes/encryption@1.6.0) (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/encryption@1.5.1...@standardnotes/encryption@1.5.2) (2022-04-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.5.1](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.5.0...@standardnotes/encryption@1.5.1) (2022-04-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.5.0](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.13...@standardnotes/encryption@1.5.0) (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/encryption@1.4.12...@standardnotes/encryption@1.4.13) (2022-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.12](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.11...@standardnotes/encryption@1.4.12) (2022-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.11](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.10...@standardnotes/encryption@1.4.11) (2022-04-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.10](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.9...@standardnotes/encryption@1.4.10) (2022-04-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.9](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.8...@standardnotes/encryption@1.4.9) (2022-04-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.8](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.7...@standardnotes/encryption@1.4.8) (2022-04-22)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.7](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.6...@standardnotes/encryption@1.4.7) (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/encryption@1.4.5...@standardnotes/encryption@1.4.6) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.5](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.4...@standardnotes/encryption@1.4.5) (2022-04-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.4](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.3...@standardnotes/encryption@1.4.4) (2022-04-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.3](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.2...@standardnotes/encryption@1.4.3) (2022-04-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.2](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.1...@standardnotes/encryption@1.4.2) (2022-04-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.4.1](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.4.0...@standardnotes/encryption@1.4.1) (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/encryption@1.3.0...@standardnotes/encryption@1.4.0) (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/encryption@1.2.6...@standardnotes/encryption@1.3.0) (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/encryption@1.2.5...@standardnotes/encryption@1.2.6) (2022-04-15)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.2.5](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.2.4...@standardnotes/encryption@1.2.5) (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/encryption@1.2.3...@standardnotes/encryption@1.2.4) (2022-04-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.2.3](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.2.2...@standardnotes/encryption@1.2.3) (2022-04-12)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.2.2](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.2.1...@standardnotes/encryption@1.2.2) (2022-04-11)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.2.1](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.2.0...@standardnotes/encryption@1.2.1) (2022-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
# [1.2.0](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.1.4...@standardnotes/encryption@1.2.0) (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/encryption@1.1.3...@standardnotes/encryption@1.1.4) (2022-04-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.1.3](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.1.2...@standardnotes/encryption@1.1.3) (2022-03-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.1.2](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.1.1...@standardnotes/encryption@1.1.2) (2022-03-31)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.1.1](https://github.com/standardnotes/snjs/compare/@standardnotes/encryption@1.1.0...@standardnotes/encryption@1.1.1) (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))
|
||||
11
packages/encryption/jest.config.js
Normal file
11
packages/encryption/jest.config.js
Normal file
@@ -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',
|
||||
},
|
||||
}
|
||||
};
|
||||
4
packages/encryption/linter.tsconfig.json
Normal file
4
packages/encryption/linter.tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
45
packages/encryption/package.json
Normal file
45
packages/encryption/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.9.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
46
packages/encryption/src/Domain/Algorithm.ts
Normal file
46
packages/encryption/src/Domain/Algorithm.ts
Normal file
@@ -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,
|
||||
}
|
||||
23
packages/encryption/src/Domain/Backups/BackupFile.ts
Normal file
23
packages/encryption/src/Domain/Backups/BackupFile.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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)[]
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
247
packages/encryption/src/Domain/Backups/BackupFileDecryptor.ts
Normal file
247
packages/encryption/src/Domain/Backups/BackupFileDecryptor.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import {
|
||||
AnyKeyParamsContent,
|
||||
ContentType,
|
||||
ProtocolVersion,
|
||||
leftVersionGreaterThanOrEqualToRight,
|
||||
compareVersions,
|
||||
} from '@standardnotes/common'
|
||||
import { BackupFile, BackupFileType } from './BackupFile'
|
||||
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'
|
||||
|
||||
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
|
||||
}
|
||||
46
packages/encryption/src/Domain/Keys/ItemsKey/ItemsKey.ts
Normal file
46
packages/encryption/src/Domain/Keys/ItemsKey/ItemsKey.ts
Normal file
@@ -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>)
|
||||
3
packages/encryption/src/Domain/Keys/ItemsKey/index.ts
Normal file
3
packages/encryption/src/Domain/Keys/ItemsKey/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './ItemsKey'
|
||||
export * from './ItemsKeyMutator'
|
||||
export * from './Registration'
|
||||
51
packages/encryption/src/Domain/Keys/RootKey/Functions.ts
Normal file
51
packages/encryption/src/Domain/Keys/RootKey/Functions.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
95
packages/encryption/src/Domain/Keys/RootKey/RootKey.ts
Normal file
95
packages/encryption/src/Domain/Keys/RootKey/RootKey.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
82
packages/encryption/src/Domain/Keys/RootKey/RootKeyParams.ts
Normal file
82
packages/encryption/src/Domain/Keys/RootKey/RootKeyParams.ts
Normal file
@@ -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',
|
||||
]
|
||||
95
packages/encryption/src/Domain/Keys/Utils/DecryptItemsKey.ts
Normal file
95
packages/encryption/src/Domain/Keys/Utils/DecryptItemsKey.ts
Normal file
@@ -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.',
|
||||
}
|
||||
215
packages/encryption/src/Domain/Operator/001/Operator001.ts
Normal file
215
packages/encryption/src/Domain/Operator/001/Operator001.ts
Normal file
@@ -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'
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
296
packages/encryption/src/Domain/Operator/002/Operator002.ts
Normal file
296
packages/encryption/src/Domain/Operator/002/Operator002.ts
Normal file
@@ -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'
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
111
packages/encryption/src/Domain/Operator/003/Operator003.ts
Normal file
111
packages/encryption/src/Domain/Operator/003/Operator003.ts
Normal file
@@ -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'
|
||||
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',
|
||||
})
|
||||
})
|
||||
})
|
||||
319
packages/encryption/src/Domain/Operator/004/Operator004.ts
Normal file
319
packages/encryption/src/Domain/Operator/004/Operator004.ts
Normal file
@@ -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'
|
||||
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
30
packages/encryption/src/Domain/Operator/Functions.ts
Normal file
30
packages/encryption/src/Domain/Operator/Functions.ts
Normal file
@@ -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
|
||||
}
|
||||
86
packages/encryption/src/Domain/Operator/Operator.ts
Normal file
86
packages/encryption/src/Domain/Operator/Operator.ts
Normal file
@@ -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>
|
||||
}
|
||||
34
packages/encryption/src/Domain/Operator/OperatorManager.ts
Normal file
34
packages/encryption/src/Domain/Operator/OperatorManager.ts
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
52
packages/encryption/src/Domain/Operator/OperatorWrapper.ts
Normal file
52
packages/encryption/src/Domain/Operator/OperatorWrapper.ts
Normal file
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/encryption/src/Domain/Operator/index.ts
Normal file
9
packages/encryption/src/Domain/Operator/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './001/Operator001'
|
||||
export * from './002/Operator002'
|
||||
export * from './003/Operator003'
|
||||
export * from './004/Operator004'
|
||||
export * from './Operator'
|
||||
export * from '../Types/EncryptedParameters'
|
||||
export * from '../Types/ItemAuthenticatedData'
|
||||
export * from '../Types/LegacyAttachedData'
|
||||
export * from '../Types/RootKeyEncryptedAuthenticatedData'
|
||||
@@ -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/EncryptionTypeSplit'
|
||||
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()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
24
packages/encryption/src/Domain/Service/Functions.ts
Normal file
24
packages/encryption/src/Domain/Service/Functions.ts
Normal file
@@ -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]
|
||||
}
|
||||
250
packages/encryption/src/Domain/Service/Items/ItemsEncryption.ts
Normal file
250
packages/encryption/src/Domain/Service/Items/ItemsEncryption.ts
Normal file
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
10
packages/encryption/src/Domain/Service/RootKey/KeyMode.ts
Normal file
10
packages/encryption/src/Domain/Service/RootKey/KeyMode.ts
Normal file
@@ -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'
|
||||
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/encryption/src/Domain/Service/index.ts
Normal file
3
packages/encryption/src/Domain/Service/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from '../Backups/BackupFile'
|
||||
export * from './Encryption/EncryptionService'
|
||||
export * from './RootKey/KeyMode'
|
||||
109
packages/encryption/src/Domain/Split/EncryptionSplit.ts
Normal file
109
packages/encryption/src/Domain/Split/EncryptionSplit.ts
Normal file
@@ -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')
|
||||
}
|
||||
27
packages/encryption/src/Domain/Split/EncryptionTypeSplit.ts
Normal file
27
packages/encryption/src/Domain/Split/EncryptionTypeSplit.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { DecryptedPayloadInterface, EncryptedPayloadInterface } from '@standardnotes/models'
|
||||
import { ItemContentTypeUsesRootKeyEncryption } from '../Keys/RootKey/Functions'
|
||||
|
||||
export interface EncryptionTypeSplit<T = EncryptedPayloadInterface | DecryptedPayloadInterface> {
|
||||
rootKeyEncryption?: T[]
|
||||
itemsKeyEncryption?: T[]
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
7
packages/encryption/src/Domain/StandardException.ts
Normal file
7
packages/encryption/src/Domain/StandardException.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class StandardException {
|
||||
constructor(public readonly message: string, log = false) {
|
||||
if (log) {
|
||||
console.error('StandardException raised: ', message)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
packages/encryption/src/Domain/Types/EncryptedParameters.ts
Normal file
41
packages/encryption/src/Domain/Types/EncryptedParameters.ts
Normal file
@@ -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
|
||||
}
|
||||
18
packages/encryption/src/Domain/Workspace/PrivateWorkspace.ts
Normal file
18
packages/encryption/src/Domain/Workspace/PrivateWorkspace.ts
Normal file
@@ -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
|
||||
}
|
||||
15
packages/encryption/src/Domain/index.ts
Normal file
15
packages/encryption/src/Domain/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export * from './Algorithm'
|
||||
export * from './Split/EncryptionSplit'
|
||||
export * from './Split/EncryptionTypeSplit'
|
||||
export * from './Operator'
|
||||
export * from './Keys/RootKey/KeyParamsFunctions'
|
||||
export * from './Keys/RootKey/RootKey'
|
||||
export * from './Keys/RootKey/RootKeyParams'
|
||||
export * from './Keys/RootKey/Functions'
|
||||
export * from './Service'
|
||||
export * from './Service/Encryption/EncryptionProvider'
|
||||
export * from './Split/EncryptionSplit'
|
||||
export * from './Workspace/PrivateWorkspace'
|
||||
export * from './Keys/ItemsKey'
|
||||
export * from './Keys/Utils/DecryptItemsKey'
|
||||
export * from './Keys/Utils/KeyRecoveryStrings'
|
||||
1
packages/encryption/src/index.ts
Normal file
1
packages/encryption/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './Domain'
|
||||
13
packages/encryption/tsconfig.json
Normal file
13
packages/encryption/tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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, '_')
|
||||
|
||||
168
yarn.lock
168
yarn.lock
@@ -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