Skip to content

Commit

Permalink
feat: add encrypt backup (#4166)
Browse files Browse the repository at this point in the history
* feat: add encrypt backup

* feat: add encrypt backup

* feat: add encrypt backup

* feat: add encrypt backup
  • Loading branch information
Jack-Works authored Sep 1, 2021
1 parent 219b320 commit e3dff63
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 2 deletions.
11 changes: 11 additions & 0 deletions packages/backup-format/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Backup format

## Backup file container

Binary format:

```
Magic header (MASK-BACKUP-V001): 16 bytes
Data: Arbitrary length
Checksum (SHA-256): 32 bytes
```
11 changes: 11 additions & 0 deletions packages/backup-format/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@masknet/backup-format",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"@dimensiondev/kit": "^0.0.0-20210712114527-dc8eac6",
"@msgpack/msgpack": "^2.7.1"
}
}
4 changes: 4 additions & 0 deletions packages/backup-format/src/BackupErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum BackupErrors {
UnknownFormat = '[@masknet/backup-format] Unknown format.',
WrongCheckSum = '[@masknet/backup-format] Bad checksum.',
}
38 changes: 38 additions & 0 deletions packages/backup-format/src/container/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { concatArrayBuffer, unreachable } from '@dimensiondev/kit'
import { BackupErrors } from '../BackupErrors'

const MAGIC_HEADER_Version0 = new Uint8Array([...'MASK-BACKUP-V000'].map((x) => x.charCodeAt(0)))
const CHECKSUM_LENGTH = 16

/** @internal */
export enum SupportedVersions {
Version0 = 0,
}
function getMagicHeader(version: SupportedVersions) {
if (version === 0) return MAGIC_HEADER_Version0
unreachable(version)
}

/** @internal */
export async function createContainer(version: SupportedVersions, data: ArrayBuffer) {
const checksum = await crypto.subtle.digest({ name: 'SHA-256' }, data)
return concatArrayBuffer(getMagicHeader(version), data, checksum)
}

/** @internal */
export async function parseEncryptedJSONContainer(version: SupportedVersions, _container: ArrayBuffer) {
const container = new Uint8Array(_container)

for (const [index, value] of getMagicHeader(version).entries()) {
if (container[index] !== value) throw new TypeError(BackupErrors.UnknownFormat)
}

const data = container.slice(MAGIC_HEADER_Version0.length, -CHECKSUM_LENGTH)
const sum = new Uint8Array(await crypto.subtle.digest({ name: 'SHA-256' }, data))

for (const [index, value] of container.slice(-CHECKSUM_LENGTH).entries()) {
if (sum[index] !== value) throw new TypeError(BackupErrors.WrongCheckSum)
}

return data
}
2 changes: 2 additions & 0 deletions packages/backup-format/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './v3-EncryptedJSON'
export * from './BackupErrors'
51 changes: 51 additions & 0 deletions packages/backup-format/src/v3-EncryptedJSON/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { decode, encode } from '@msgpack/msgpack'
import { createContainer, parseEncryptedJSONContainer } from '../container'
import { BackupErrors } from '../BackupErrors'
import { SupportedVersions } from '../container'
export async function encryptBackup(password: BufferSource, binaryBackup: Uint8Array) {
const [pbkdf2IV, AESKey] = await createAESFromPassword(password)
const AESParam: AesGcmParams = { name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(16)) }

const encrypted = new Uint8Array(await crypto.subtle.encrypt(AESParam, AESKey, binaryBackup))
const container = encode([pbkdf2IV, AESParam.iv, encrypted])
return createContainer(SupportedVersions.Version0, container)
}
export async function decryptBackup(password: BufferSource, data: ArrayBuffer) {
const container = await parseEncryptedJSONContainer(SupportedVersions.Version0, data)

const _ = decode(container)
if (!Array.isArray(_) || _.length !== 3) throw new TypeError(BackupErrors.UnknownFormat)
if (!_.every((x): x is Uint8Array => x instanceof Uint8Array)) throw new TypeError(BackupErrors.UnknownFormat)
const [pbkdf2IV, encryptIV, encrypted] = _

const aes = await getAESFromPassword(password, pbkdf2IV)

const AESParam: AesGcmParams = { name: 'AES-GCM', iv: encryptIV }
const decryptedBackup = await crypto.subtle.decrypt(AESParam, aes, encrypted)
return decryptedBackup
}

async function createAESFromPassword(password: BufferSource) {
const pbkdf = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
const iv = crypto.getRandomValues(new Uint8Array(16))
const aes = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: iv, iterations: 10000, hash: 'SHA-256' },
pbkdf,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt'],
)
return [iv, aes] as const
}

async function getAESFromPassword(password: BufferSource, iv: Uint8Array) {
const pbkdf = await crypto.subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
const aes = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: iv, iterations: 10000, hash: 'SHA-256' },
pbkdf,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt'],
)
return aes
}
10 changes: 10 additions & 0 deletions packages/backup-format/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src/",
"outDir": "./dist/",
"tsBuildInfoFile": "./dist/.tsbuildinfo"
},
"include": ["./src", "./src/**/*.json"],
"references": []
}
6 changes: 4 additions & 2 deletions packages/empty/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"name": "null",
"name": "@masknet/null",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
"types": "./dist/index.d.ts",
"dependencies": {},
"devDependencies": {}
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ packages:
- 'packages/plugins/example'
- 'packages/plugins/Wallet'
- 'packages/external-plugin-previewer'
- 'packages/backup-format'

0 comments on commit e3dff63

Please sign in to comment.