├── .babelrc ├── .github └── CODEOWNERS ├── .gitignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── blob.js ├── buffer.js ├── crypto.js ├── file.js ├── header.js ├── index.js ├── metadata.js └── util.js └── tests ├── blob.test.js ├── crypto.test.js ├── encrypt-decrypt.test.js ├── fixtures └── corrupted.seco ├── header.test.js ├── integration.test.js └── metadata.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-flow"], 3 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 4 | } 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github/ @633kh4ck @mvayngrib 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 12 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.0.0 / 2020-10-23 2 | ------------------ 3 | 4 | - Require Node 12+ ([#6](https://github.com/exodusmovement/secure-container/pull/6)) 5 | - Add user-friendly `encrypt`/`decrypt` methods ([#4](https://github.com/exodusmovement/secure-container/pull/4)) 6 | - Add browser/react native support ([#5](https://github.com/exodusmovement/secure-container/pull/5)) 7 | 8 | 1.0.0 / 2017-05-19 9 | ------------------ 10 | - Add docs 11 | - Remove runtime type checks 12 | 13 | 0.0.2 / 2016-05-29 14 | ------------------ 15 | - fix `files` field in `package` 16 | 17 | 0.0.1 / 2016-05-29 18 | ------------------ 19 | - initial release 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2022 Exodus Movement, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the “Software”), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | secure-container 2 | ================ 3 | 4 | [![Build Status](https://travis-ci.org/ExodusMovement/secure-container.svg?branch=master)](https://travis-ci.org/ExodusMovement/secure-container) 5 | 6 | 7 | Install 8 | ------- 9 | 10 | npm i --save secure-container 11 | 12 | 13 | 14 | API 15 | ----- 16 | 17 | ### Main Module 18 | 19 | This is the main module most users should use; other modules are for advanced users only. 20 | 21 | ```js 22 | import * as seco from 'secure-container' 23 | // OR 24 | const seco = require('secure-container') 25 | ``` 26 | 27 | #### `seco.encrypt()` 28 | 29 | `encrypt(data, options)` 30 | 31 | - `data` (String | Buffer) Data to encrypt 32 | - `options` (Object) 33 | - `header` (Object) 34 | - `appName` (String) Name of your app 35 | - `appVersion` (String) Version of your app 36 | - `passphrase` (String | Buffer) Passphrase used to encrypt the data 37 | - `metadata` (Object) 38 | - `blobKey` (Buffer) 39 | 40 | _Note:_ Must set either `passphrase` or `metadata` & `blobKey`. 41 | 42 | Returns an Object that contains: 43 | 44 | - `encryptedData` (Buffer) The encrypted data 45 | - `blobKey` (Buffer) 46 | - `metadata` (Object) 47 | 48 | #### `seco.decrypt()` 49 | 50 | `decrypt(encryptedData, passphrase)` 51 | 52 | - `encryptedData` (Buffer) Data to decrypt 53 | - `passphrase` (String | Buffer) Passphrase to decrypt the data 54 | 55 | Returns an Object that contains: 56 | 57 | - `data` (Buffer) The file data 58 | - `header` (Object) The header for the secure-container 59 | - `blobKey` (Buffer) 60 | - `metadata` (Object) 61 | 62 | ### `header` module 63 | 64 | ```js 65 | import * as header from 'secure-container/lib/header' 66 | // OR 67 | const header = require('secure-container/lib/header') 68 | ``` 69 | 70 | #### `header.create(data)` 71 | 72 | Create a header object. 73 | 74 | - `data` (Object) 75 | - `appName` (String) Name of your app 76 | - `appVersion` (String) Version of your app 77 | 78 | Returns an Object. 79 | 80 | #### `header.serialize(headerObj)` 81 | 82 | Serialize a header object. `headerObj` is a header object made with `create()`. Returns a Buffer. 83 | 84 | #### `header.decode(buffer)` 85 | 86 | Decodes a header buffer and returns the Object. 87 | 88 | ### `metadata` module 89 | 90 | ```js 91 | import * as metadata from 'secure-container/lib/metadata' 92 | // OR 93 | const metadata = require('secure-container/lib/metadata') 94 | ``` 95 | 96 | #### `metadata.create()` 97 | 98 | Create a metadata object. Returns an Object. 99 | 100 | #### `metadata.encryptBlobKey(metadata, passphrase, blobKey)` 101 | 102 | - `metadata` (Object) Metadata created with `metadata.create()`. 103 | - `passphrase` (String | Buffer) 104 | - `blobKey` (Buffer) 105 | 106 | Mutates `metadata` object; returns `undefined`. 107 | 108 | #### `metadata.serialize(metadata)` 109 | 110 | Serialize a metadata object. Returns a Buffer. 111 | 112 | #### `metadata.decode(buffer)` 113 | 114 | Takes a metadata buffer, decodes it, and returns an object. 115 | 116 | #### `metadata.decryptBlobKey(metadata, passphrase)` 117 | 118 | - `metadata` (Object) Metadata with an encrypted blobKey. 119 | - `passphrase` (String | Buffer) 120 | 121 | Returns `blobKey` as a buffer. 122 | 123 | ### `blob` module 124 | 125 | ```js 126 | import * as blob from 'secure-container/lib/blob' 127 | // OR 128 | const blob = require('secure-container/lib/blob') 129 | ``` 130 | 131 | #### `blob.encrypt(data, metadata, blobKey)` 132 | 133 | - `data` (Buffer) Data or message to encrypt. 134 | - `metadata` (Object) Metadata object. 135 | - `blobKey` (Buffer) 136 | 137 | Mutates `metadata`. Returns an object: 138 | 139 | - `blob` (Buffer) Encrypted data. 140 | - `blobKey` (Buffer) The `blobKey` you passed in. 141 | 142 | #### `blob.decrypt(blob, metadata, blobKey)` 143 | 144 | - `blob` (Buffer) Encrypted data. 145 | - `metadata` (Object) Metadata object. 146 | - `blobKey` (Buffer) 147 | 148 | Returns the decrypted data as a buffer. 149 | 150 | ### `file` module 151 | 152 | ```js 153 | import * as file from 'secure-container/lib/file' 154 | // OR 155 | const file = require('secure-container/lib/file') 156 | ``` 157 | 158 | #### `file.computeChecksum(metadata, blob)` 159 | 160 | - `metadata` (Buffer) Metadata as a Buffer 161 | - `blob` (Buffer) Encrypted blob 162 | 163 | Returns a `sha256` checksum as a buffer. 164 | 165 | #### `file.encode(fileObj)` 166 | 167 | - `fileObj` (Object) 168 | - `header` (Buffer) Serialized header 169 | - `checksum` (Buffer) Checksum from `file.computeChecksum()` 170 | - `metadata` (Buffer) Metadata as a Buffer 171 | - `blob` (Buffer) Encrypted blob 172 | 173 | Returns a buffer. 174 | 175 | #### `file.decode(fileBuffer)` 176 | 177 | The opposite of `file.encode()`. Takes a buffer and returns an object. 178 | 179 | File Format Description 180 | ----------- 181 | 182 | This is the documentation for the binary structure of secure containers. 183 | 184 | For clarity, we have split the documentation into four sections: `header`, `checksum`, `metadata`, and `blob`. 185 | 186 | ### Header 187 | 188 | Size | Label | Description | 189 | ---- | ----- | ----------- | 190 | 4 | `magic` | The magic header indicating the file type. Always `SECO`. 191 | 4 | `version` | File format version. Currently `0`, stored as `UInt32BE`. 192 | 4 | `reserved` | Reserved for future use. 193 | 1 | `versionTagLength` | Length of `versionTag` as `UInt8`. 194 | `versionTagLength` | `versionTag` | Should be `'seco-v0-scrypt-aes'`. 195 | 1 | `appNameLength` | Length of `appName` as `UInt8`. 196 | `appNameLength` | `appName` | Name of the application writing the file. 197 | 1 | `appVersionLength` | Length of `appVersion` as `UInt8`. 198 | `appVersionLength` | `appVersion` | Version of the application writing the file. 199 | 200 | ### Checksum 201 | 202 | 32-byte `sha256` checksum of the following data: 203 | 204 | 1. The `metadata`. 205 | 1. Byte-length of the `blob`, stored as `UInt32BE`. 206 | 1. The `blob`. 207 | 208 | ### Metadata 209 | 210 | Size | Label | Description | 211 | ---- | ----- | ----------- | 212 | 32 | `salt` | Scrypt salt. 213 | 4 | `n` | Scrypt `n` parameter. 214 | 4 | `r` | Scrypt `r` parameter. 215 | 4 | `p` | Scrypt `p` parameter. 216 | 32 | `cipher` | Currently `aes-256-gcm` stored as a zero-terminated C-string. 217 | 12 | `iv` | `blobKey`'s `iv`. 218 | 16 | `authTag` | `blobKey`'s `authTag`. 219 | 32 | `key` | `blobKey`'s `key`. 220 | 12 | `iv` | The `blob`'s `iv`. 221 | 16 | `authTag` | The `blob`'s `authTag`. 222 | 223 | ### Blob 224 | 225 | Size | Label | Description | 226 | ---- | ----- | ----------- | 227 | 4 | `blobLength` | Length of `blob` as `UInt32BE`. 228 | `blobLength` | `blob` | Encrypted data. 229 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-container", 3 | "description": "Secure container.", 4 | "version": "2.0.0", 5 | "author": "Exodus Movement, Inc.", 6 | "bugs": { 7 | "url": "https://github.com/exodusmovement/secure-container/issues" 8 | }, 9 | "contributors": [ 10 | "JP Richardson", 11 | "Kirill Fomichev" 12 | ], 13 | "dependencies": { 14 | "@exodus/scryptsy": "^2.2.0", 15 | "browserify-aes": "^1.2.0", 16 | "create-hash": "^1.2.0", 17 | "randombytes": "^2.1.0", 18 | "varstruct": "^5.2.0" 19 | }, 20 | "devDependencies": { 21 | "@babel/cli": "^7.12.1", 22 | "@babel/core": "^7.12.3", 23 | "@babel/plugin-transform-modules-commonjs": "^7.12.1", 24 | "@babel/preset-flow": "^7.12.1", 25 | "@babel/register": "^7.12.1", 26 | "babel-eslint": "^6.0.4", 27 | "standard": "^7.1.0", 28 | "tap-spec": "^5.0.0", 29 | "tape": "^4.5.1", 30 | "tape-promise": "^1.1.0" 31 | }, 32 | "engines": { 33 | "node": ">= 12.0.0" 34 | }, 35 | "files": [ 36 | "lib/" 37 | ], 38 | "homepage": "https://github.com/exodusmovement/secure-container#readme", 39 | "keywords": [ 40 | "secure" 41 | ], 42 | "license": "MIT", 43 | "main": "./lib/index.js", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/exodusmovement/secure-container.git" 47 | }, 48 | "scripts": { 49 | "build": "babel src --out-dir lib", 50 | "lint": "standard", 51 | "prepare": "npm test", 52 | "test": "npm run build && npm run lint && npm run unit", 53 | "unit": "find ./tests -name *.test.js -exec tape -r @babel/register {} \\; | tap-spec" 54 | }, 55 | "standard": { 56 | "ignore": "lib/", 57 | "parser": "babel-eslint" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/blob.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as scCrypto from './crypto' 3 | 4 | export function encrypt (message: Buffer, metadata: Object, blobKey: Buffer) { 5 | const { authTag, iv, blob } = scCrypto.aesEncrypt(blobKey, message) 6 | metadata.blob = { authTag, iv } 7 | return { blob, blobKey } 8 | } 9 | 10 | export function decrypt (blob: Buffer, metadata: Object, blobKey: Buffer): Buffer { 11 | const message = scCrypto.aesDecrypt(blobKey, blob, metadata.blob) 12 | return message 13 | } 14 | -------------------------------------------------------------------------------- /src/buffer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export function fromUInt32BE (num: number): Buffer { 4 | let buf = Buffer.alloc(4) 5 | buf.writeUInt32BE(num) 6 | return buf 7 | } 8 | -------------------------------------------------------------------------------- /src/crypto.js: -------------------------------------------------------------------------------- 1 | import randomBytes from 'randombytes' 2 | import createHash from 'create-hash' 3 | import aes from 'browserify-aes' 4 | import scrypt from '@exodus/scryptsy' 5 | 6 | // http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf 8.2.2 RBG-based Construction (about initialization vectors) 7 | export const IV_LEN_BYTES = 12 // <-- always 12, any other value will error, not sure why it won't allow higher... probably concat with freefield? 8 | 9 | export function createScryptParams (params = {}) : Object { 10 | return { salt: randomBytes(32), n: 16384, r: 8, p: 1, ...params } 11 | } 12 | 13 | // always returns 32 byte key 14 | export function stretchPassphrase (passphrase: string | Buffer, { salt, n, r, p } = createScryptParams()) : Object { 15 | const key = scrypt(passphrase, salt, n, r, p, 32) 16 | return { key, salt } 17 | } 18 | 19 | export function aesEncrypt (key: Buffer, message: Buffer) : Object { 20 | const iv = randomBytes(IV_LEN_BYTES) 21 | const cipher = aes.createCipheriv('aes-256-gcm', key, iv) 22 | const blob = Buffer.concat([cipher.update(message), cipher.final()]) 23 | const authTag = cipher.getAuthTag() 24 | return { authTag, blob, iv } 25 | } 26 | 27 | export function aesDecrypt (key: Buffer, blob: Buffer, { iv, authTag }) : Buffer { 28 | const decipher = aes.createDecipheriv('aes-256-gcm', key, iv) 29 | decipher.setAuthTag(authTag) 30 | const message = Buffer.concat([decipher.update(blob), decipher.final()]) 31 | return message 32 | } 33 | 34 | export function boxEncrypt (passphrase: string | Buffer, message: Buffer, scryptParams) { 35 | const { key, salt } = stretchPassphrase(passphrase, scryptParams) 36 | const { authTag, blob, iv } = aesEncrypt(key, message) 37 | return { authTag, blob, iv, salt } 38 | } 39 | 40 | export function boxDecrypt (passphrase: string | Buffer, blob: Buffer, { iv, authTag }, scryptParams) { 41 | scryptParams = { ...createScryptParams(), ...scryptParams } 42 | const { key } = stretchPassphrase(passphrase, scryptParams) 43 | const message = aesDecrypt(key, blob, { iv, authTag }) 44 | return message 45 | } 46 | 47 | export function sha256 (message: Buffer): Buffer { 48 | return createHash('sha256').update(message).digest() 49 | } 50 | -------------------------------------------------------------------------------- /src/file.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import varstruct, { Buffer as Buf, VarBuffer, UInt32BE } from 'varstruct' 3 | import { fromUInt32BE } from './buffer' 4 | import * as scCrypto from './crypto' 5 | import { vsf } from './util' 6 | 7 | import { HEADER_LEN_BYTES } from './header' 8 | import { METADATA_LEN_BYTES } from './metadata' 9 | 10 | export const struct = varstruct(vsf([ 11 | ['header', Buf(HEADER_LEN_BYTES)], 12 | ['checksum', Buf(32)], 13 | ['metadata', Buf(METADATA_LEN_BYTES)], 14 | ['blob', VarBuffer(UInt32BE)] 15 | ])) 16 | 17 | export function decode (fileContents: Buffer): Object { 18 | return struct.decode(fileContents) 19 | } 20 | 21 | export function encode (fileContents: Object): Buffer { 22 | return struct.encode(fileContents) 23 | } 24 | 25 | export function computeChecksum (metadata: Buffer, blob: Buffer): Buffer { 26 | return scCrypto.sha256(Buffer.concat([metadata, fromUInt32BE(blob.byteLength), blob])) 27 | } 28 | 29 | export function checkContents (fileContents: Buffer): boolean { 30 | let fileObj = decode(fileContents) 31 | return fileObj.checksum.equals(computeChecksum(fileObj.metadata, fileObj.blob)) 32 | } 33 | -------------------------------------------------------------------------------- /src/header.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | // import assert from 'assert' 3 | import varstruct, { 4 | Bound, 5 | UInt8, 6 | UInt32BE, 7 | VarString 8 | } from 'varstruct' 9 | import { vsf } from './util' 10 | 11 | export const HEADER_LEN_BYTES = 224 12 | export const HEADER_VERSION_TAG = 'seco-v0-scrypt-aes' 13 | export const MAGIC = Buffer.from('SECO', 'utf8') 14 | 15 | export function checkMagic (magic) { 16 | if (!magic.equals(MAGIC)) throw new RangeError('Invalid secure container magic.') 17 | } 18 | 19 | export const struct = varstruct(vsf([ 20 | ['magic', Bound(varstruct.Buffer(4), checkMagic)], 21 | ['version', UInt32BE], // should be all 0's for now 22 | ['reserved', UInt32BE], // should be all 0's for now 23 | ['versionTag', VarString(UInt8)], 24 | ['appName', VarString(UInt8, 'utf-8')], 25 | ['appVersion', VarString(UInt8, 'utf-8')] 26 | ])) 27 | 28 | export function decode (headerBlob: Buffer): Object { 29 | if (headerBlob.byteLength > HEADER_LEN_BYTES) console.warn(`header greater than ${HEADER_LEN_BYTES} bytes, are you sure this is the header?`) 30 | return struct.decode(headerBlob) 31 | } 32 | 33 | export function encode (header: Object): Buffer { 34 | return struct.encode(header) 35 | } 36 | 37 | export function serialize (header: Object) { 38 | let buf = Buffer.alloc(HEADER_LEN_BYTES) 39 | encode(header).copy(buf) 40 | return buf 41 | } 42 | 43 | // TODO: fetch parent module and include this info by default 44 | export function create ({ appName, appVersion } = { appName: '', appVersion: '' }): Object { 45 | return { 46 | magic: MAGIC, 47 | version: 0, 48 | reserved: 0, 49 | versionTag: HEADER_VERSION_TAG, 50 | appName, 51 | appVersion 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* flow */ 2 | import randomBytes from 'randombytes' 3 | import * as conBlob from './blob' 4 | import * as conHeader from './header' 5 | import * as conMetadata from './metadata' 6 | import * as conFile from './file' 7 | 8 | type BufOrStr = Buffer | string 9 | 10 | // options: passphrase, blobKey, metdata 11 | export function encrypt (data: BufOrStr, options = {}) { 12 | if (!options.header) console.warn('seco: should pass options.header.') 13 | let header = conHeader.create(options.header) 14 | 15 | let blobKey 16 | let metadata 17 | if (options.passphrase) { 18 | blobKey = randomBytes(32) 19 | metadata = conMetadata.create() 20 | conMetadata.encryptBlobKey(metadata, options.passphrase, blobKey) 21 | } else if (options.metadata && options.blobKey) { 22 | blobKey = options.blobKey 23 | metadata = options.metadata 24 | } else { 25 | throw new Error('Must set either passphrase or (metadata and blobKey)') 26 | } 27 | 28 | data = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8') 29 | let { blob: encBlob } = conBlob.encrypt(data, metadata, blobKey) 30 | 31 | const headerBuf = conHeader.serialize(header) 32 | const mdBuf = conMetadata.serialize(metadata) 33 | 34 | let fileObj = { 35 | header: headerBuf, 36 | checksum: conFile.computeChecksum(mdBuf, encBlob), 37 | metadata: mdBuf, 38 | blob: encBlob 39 | } 40 | const encryptedData = conFile.encode(fileObj) 41 | 42 | return { encryptedData, blobKey, metadata } 43 | } 44 | 45 | export function decrypt (encryptedData: Buffer, passphrase: BufOrStr) { 46 | const fileObj = conFile.decode(encryptedData) 47 | 48 | const checksum = conFile.computeChecksum(fileObj.metadata, fileObj.blob) 49 | if (!fileObj.checksum.equals(checksum)) throw new Error('seco checksum does not match; data may be corrupted') 50 | 51 | let metadata = conMetadata.decode(fileObj.metadata) 52 | let blobKey = conMetadata.decryptBlobKey(metadata, passphrase) 53 | let header = conHeader.decode(fileObj.header) 54 | let data = conBlob.decrypt(fileObj.blob, metadata, blobKey) 55 | 56 | return { data, blobKey, metadata, header } 57 | } 58 | -------------------------------------------------------------------------------- /src/metadata.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import varstruct, { UInt32BE, Buffer as Buf } from 'varstruct' 3 | import { createScryptParams, IV_LEN_BYTES, boxEncrypt, boxDecrypt } from './crypto' 4 | import { vsf, CStr } from './util' 5 | 6 | export const METADATA_LEN_BYTES = 256 7 | 8 | export const struct = varstruct(vsf([ 9 | ['scrypt', [ 10 | ['salt', Buf(32)], 11 | ['n', UInt32BE], 12 | ['r', UInt32BE], 13 | ['p', UInt32BE] 14 | ]], 15 | ['cipher', CStr(32)], 16 | ['blobKey', [ 17 | ['iv', Buf(IV_LEN_BYTES)], 18 | ['authTag', Buf(16)], 19 | ['key', Buf(32)] 20 | ]], 21 | ['blob', [ 22 | ['iv', Buf(IV_LEN_BYTES)], 23 | ['authTag', Buf(16)] 24 | ]] 25 | ])) 26 | 27 | export function decode (metadataBlob: Buffer): Object { 28 | if (metadataBlob.byteLength > METADATA_LEN_BYTES) console.warn('metadata greater than `${METADATA_LEN_BYTES}` bytes, are you sure this is the SECO metadata?') 29 | return struct.decode(metadataBlob) 30 | } 31 | 32 | export function encode (metadataObject): Buffer { 33 | return struct.encode(metadataObject) 34 | } 35 | 36 | export function serialize (metadata: Object): Buffer { 37 | let buf = Buffer.alloc(METADATA_LEN_BYTES) 38 | encode(metadata).copy(buf) 39 | return buf 40 | } 41 | 42 | export function create (scryptParams = createScryptParams()) : Object { 43 | return { 44 | scrypt: scryptParams, 45 | cipher: 'aes-256-gcm', 46 | blobKey: { 47 | iv: Buffer.alloc(IV_LEN_BYTES), 48 | authTag: Buffer.alloc(16), 49 | key: Buffer.alloc(32) 50 | }, 51 | blob: { 52 | iv: Buffer.alloc(IV_LEN_BYTES), 53 | authTag: Buffer.alloc(16) 54 | } 55 | } 56 | } 57 | 58 | export function encryptBlobKey (metadata: Object, passphrase: string | Buffer, blobKey: Buffer) { 59 | const { authTag, blob, iv, salt } = boxEncrypt(passphrase, blobKey, metadata.scrypt) 60 | metadata.scrypt.salt = salt 61 | metadata.blobKey = { authTag, iv, key: blob } 62 | } 63 | 64 | export function decryptBlobKey (metadata: Object, passphrase: string | Buffer): Buffer { 65 | const blobKey = boxDecrypt(passphrase, metadata.blobKey.key, metadata.blobKey, metadata.scrypt) 66 | return blobKey 67 | } 68 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import varstruct, { Buffer as Buf } from 'varstruct' 3 | 4 | export function vsf (fields: Array): Array { 5 | return fields.map(fields => ({ 6 | name: fields[0], 7 | type: Array.isArray(fields[1]) ? varstruct(vsf(fields[1])) : fields[1] 8 | })) 9 | } 10 | 11 | // zero-terminated C-string (Buffer) 12 | export function CStr (length: number, encoding = 'utf8') { 13 | let bufferCodec = Buf(length) 14 | 15 | function encode (value: string, buffer: ?Buffer, offset: ?number): Buffer { 16 | let buf = Buffer.alloc(length) 17 | buf.write(value, encoding) 18 | return bufferCodec.encode(buf, buffer, offset) 19 | } 20 | 21 | function decode (buffer: Buffer, offset, end): string { 22 | let buf = bufferCodec.decode(buffer, offset, end) 23 | let i = 0 24 | for (; i < buf.length; i++) if (buf[i] === 0) break 25 | return buf.slice(0, i).toString(encoding) 26 | } 27 | 28 | const encodingLength = () => length 29 | 30 | // TODO: submit pr on varstruct if 'bytes' is undefined 31 | encode.bytes = decode.bytes = length 32 | 33 | return { encode, decode, encodingLength } 34 | } 35 | -------------------------------------------------------------------------------- /tests/blob.test.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import test from 'tape' 3 | import * as metadata from '../src/metadata' 4 | import * as blob from '../src/blob' 5 | 6 | test('encrypt / decrypt ', (t) => { 7 | const blobKey = crypto.randomBytes(32) 8 | const message = 'we will attack at dawn!' 9 | let md = metadata.create() 10 | 11 | const { blobKey: newBlobKey, blob: secretBlob } = blob.encrypt(new Buffer(message), md, blobKey) 12 | t.deepEqual(newBlobKey, blobKey, 'keys are the same') 13 | 14 | const actualMessage = blob.decrypt(secretBlob, md, blobKey) 15 | t.is(actualMessage.toString('utf8'), message, 'secret messages are the same') 16 | 17 | t.end() 18 | }) 19 | -------------------------------------------------------------------------------- /tests/crypto.test.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import test from 'tape' 3 | import * as scCrypto from '../src/crypto' 4 | 5 | test('stretchPassphrase should return 32 bytes', (t) => { 6 | t.plan(3) 7 | 8 | const passphrase = 'super secret' 9 | const inputSalt = Buffer.from('b231f5603df27d48457c1f773e673aff1f43f4001786f458e91cceb45d2837e7', 'hex') 10 | 11 | var scryptParams = { salt: inputSalt, n: 16384, r: 8, p: 1 } 12 | 13 | var expectedKey = Buffer.from('b451dbfb31c7dc5b45238e1a446a6ad7ae16b9a71235678e9a52089c321ec4cf', 'hex') 14 | var { key, salt } = scCrypto.stretchPassphrase(passphrase, scryptParams) 15 | 16 | t.is(key.toString('hex'), expectedKey.toString('hex'), 'keys are the same') 17 | t.is(key.byteLength, 32, '32 byte key') 18 | t.is(salt.toString('hex'), inputSalt.toString('hex'), 'salts are the same') 19 | 20 | t.end() 21 | }) 22 | 23 | test('stretchPassphrase will accept a buffer passphrase', (t) => { 24 | t.plan(2) 25 | 26 | const passphrase = Buffer.from('super secret', 'utf8') 27 | const salt = Buffer.from('b231f5603df27d48457c1f773e673aff1f43f4001786f458e91cceb45d2837e7', 'hex') 28 | 29 | var scryptParams = { salt, n: 16384, r: 8, p: 1 } 30 | 31 | var expectedKey = Buffer.from('b451dbfb31c7dc5b45238e1a446a6ad7ae16b9a71235678e9a52089c321ec4cf', 'hex') 32 | var { key } = scCrypto.stretchPassphrase(passphrase, scryptParams) 33 | t.is(key.toString('hex'), expectedKey.toString('hex'), 'keys are the same') 34 | t.is(key.byteLength, 32, '32 byte key') 35 | 36 | t.end() 37 | }) 38 | 39 | test('aesEncrypt / aesDecrypt', (t) => { 40 | t.plan(3) 41 | 42 | const key = crypto.randomBytes(32) 43 | const message = new Buffer('we will attack at midnight!') 44 | 45 | const { blob, authTag, iv } = scCrypto.aesEncrypt(key, message) 46 | t.true(Buffer.isBuffer(iv), 'iv is a buffer') 47 | t.true(Buffer.isBuffer(authTag), 'authTag is a buffer') 48 | 49 | const decryptedMessage = scCrypto.aesDecrypt(key, blob, { iv, authTag }) 50 | 51 | t.is(decryptedMessage.toString('utf8'), message.toString('utf8'), 'messages are the same') 52 | 53 | t.end() 54 | }) 55 | 56 | test('boxEncrypt / boxDecrypt', (t) => { 57 | t.plan(1) 58 | 59 | const passphrase = 'open sesame' 60 | const message = new Buffer('The secret launch code is 1234.') 61 | 62 | const { authTag, blob, iv, salt } = scCrypto.boxEncrypt(passphrase, message) 63 | const actualMessage = scCrypto.boxDecrypt(passphrase, blob, { iv, authTag }, { salt }) 64 | 65 | t.is(message.toString('utf8'), actualMessage.toString('utf8'), 'messages are the same') 66 | 67 | t.end() 68 | }) 69 | 70 | test('createScryptParams', (t) => { 71 | t.plan(1) 72 | 73 | let params = scCrypto.createScryptParams({ n: 16 }) 74 | t.is(params.n, 16, 'var is set') 75 | 76 | t.end() 77 | }) 78 | -------------------------------------------------------------------------------- /tests/encrypt-decrypt.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import fs from 'fs' 3 | import { encrypt, decrypt } from '../src' 4 | 5 | test('encrypt / decrypt', (t) => { 6 | let secretMessage = Buffer.from('Hello, lets meet at 10 PM to plan our secret mission!', 'utf8') 7 | const passphrase = Buffer.from('opensesame', 'utf8') 8 | 9 | const { encryptedData } = encrypt(secretMessage, { passphrase }) 10 | 11 | const { data } = decrypt(encryptedData, passphrase) 12 | 13 | t.is(data.toString('utf8'), secretMessage.toString('utf8'), 'verify content is the same') 14 | 15 | t.end() 16 | }) 17 | 18 | test('encrypt / decrypt (with blobkey)', (t) => { 19 | let secretMessage = Buffer.from('Hello, lets meet at 10 PM to plan our secret mission!', 'utf8') 20 | const passphrase = Buffer.from('opensesame', 'utf8') 21 | 22 | const { metadata, blobKey } = encrypt(secretMessage, { passphrase }) 23 | 24 | const { encryptedData } = encrypt(secretMessage, { metadata, blobKey }) 25 | 26 | const { data } = decrypt(encryptedData, passphrase) 27 | 28 | t.is(data.toString('utf8'), secretMessage.toString('utf8'), 'verify content is the same') 29 | 30 | t.end() 31 | }) 32 | 33 | test('decrypt returns valid blobKey and metadata', (t) => { 34 | let secretMessage = Buffer.from('Hello, lets meet at 10 PM to plan our secret mission!', 'utf8') 35 | let secretMessage2 = Buffer.from('Hello, lets meet at 10 AM to plan our secret mission!', 'utf8') 36 | const passphrase = Buffer.from('opensesame', 'utf8') 37 | 38 | const { encryptedData } = encrypt(secretMessage, { passphrase }) 39 | 40 | const { data, metadata, blobKey } = decrypt(encryptedData, passphrase) 41 | t.is(data.toString('utf8'), secretMessage.toString('utf8'), 'verify content is the same') 42 | 43 | const { encryptedData: encryptedData2 } = encrypt(secretMessage2, { metadata, blobKey }) 44 | 45 | const { data: data2 } = decrypt(encryptedData2, passphrase) 46 | t.is(data2.toString('utf8'), secretMessage2.toString('utf8'), 'verify content is the same') 47 | 48 | t.end() 49 | }) 50 | 51 | test('decrypt verifies checksum', (t) => { 52 | const testFile = 'tests/fixtures/corrupted.seco' 53 | const buf = fs.readFileSync(testFile) 54 | 55 | try { 56 | decrypt(buf, 'opensesame') 57 | } catch (err) { 58 | t.assert(err) 59 | t.ok(err.message.match(/seco checksum does not match; data may be corrupted/)) 60 | t.end() 61 | } 62 | }) 63 | 64 | test('decrypt returns header', (t) => { 65 | let secretMessage = Buffer.from('Hi, lets meet at 10 PM to plan our secret mission!', 'utf8') 66 | const passphrase = Buffer.from('opensesame') 67 | const header = { 68 | appName: 'test', 69 | appVersion: '1.0.0' 70 | } 71 | 72 | const { encryptedData } = encrypt(secretMessage, { passphrase, header }) 73 | 74 | const result = decrypt(encryptedData, passphrase) 75 | 76 | t.is(result.header.appName, header.appName, 'appName is returned') 77 | t.is(result.header.appVersion, header.appVersion, 'appVersion is returned') 78 | 79 | t.end() 80 | }) 81 | -------------------------------------------------------------------------------- /tests/fixtures/corrupted.seco: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExodusMovement/secure-container/4c005ec38f191cc0c5a8353970dad0c2b0d5f59b/tests/fixtures/corrupted.seco -------------------------------------------------------------------------------- /tests/header.test.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import test from 'tape' 3 | import * as header from '../src/header' 4 | 5 | test('encode / decode header', (t) => { 6 | t.plan(1) 7 | 8 | var obj = { 9 | magic: header.MAGIC, 10 | version: 0, 11 | reserved: 0, 12 | versionTag: 'seco-test-1', 13 | appName: 'Exodus', 14 | appVersion: '1.0.0' 15 | } 16 | 17 | var obj2 = header.decode(header.encode(obj)) 18 | t.deepEqual(obj, obj2, 'verify objects are the same') 19 | 20 | t.end() 21 | }) 22 | 23 | test('create()', (t) => { 24 | t.plan(6) 25 | 26 | let headerObj: Object = header.create({ appName: 'Exodus', appVersion: '1.0.0' }) 27 | 28 | t.deepEqual(headerObj.magic, header.MAGIC, 'magic the same') 29 | t.is(headerObj.version, 0, 'version is 0') 30 | t.is(headerObj.reserved, 0, 'reserved is 0') 31 | t.is(headerObj.versionTag, 'seco-v0-scrypt-aes', 'versionTag is set') 32 | t.is(headerObj.appName, 'Exodus', 'appName is set') 33 | t.is(headerObj.appVersion, '1.0.0', 'appVersion is set') 34 | 35 | t.end() 36 | }) 37 | -------------------------------------------------------------------------------- /tests/integration.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import crypto from 'crypto' 3 | import * as header from '../src/header' 4 | import { encrypt as encryptBlob, decrypt as decryptBlob } from '../src/blob' 5 | import * as metadata from '../src/metadata' 6 | import * as file from '../src/file' 7 | import * as scCrypto from '../src/crypto' 8 | 9 | test('integration', (t) => { 10 | // t.plan(1) 11 | 12 | // -- ENCRYPTION --- 13 | 14 | const headerObj = header.create({ appName: 'Exodus', appVersion: 'v1.0.0' }) 15 | const headerBuf = header.serialize(headerObj) 16 | 17 | // includes a random scrypt.. may need to change 18 | let metadataObj = metadata.create() 19 | 20 | const dataToEncrypt = { 21 | superSecret: 'this is a secret message', 22 | agent: 'James Bond' 23 | } 24 | const message = new Buffer(JSON.stringify(dataToEncrypt), 'utf8') 25 | 26 | const secretKey = crypto.randomBytes(32) 27 | const passphrase = 'open sesame' 28 | 29 | const { blob } = encryptBlob(message, metadataObj, secretKey) 30 | metadata.encryptBlobKey(metadataObj, passphrase, secretKey) 31 | 32 | const metadataBuf = metadata.serialize(metadataObj) 33 | 34 | const fileObj = { 35 | header: headerBuf, 36 | checksum: file.computeChecksum(metadataBuf, blob), 37 | metadata: metadataBuf, 38 | blob 39 | } 40 | 41 | const fileBuf = file.encode(fileObj) 42 | 43 | // -- DECRYPTION -- 44 | 45 | const decFileObj = file.decode(fileBuf) 46 | const decTotalBuf = fileBuf.slice(header.HEADER_LEN_BYTES + 32) 47 | const decMetadata = metadata.decode(decFileObj.metadata) 48 | 49 | t.deepEqual(scCrypto.sha256(decTotalBuf), fileObj.checksum, 'checksums equal') 50 | t.true(file.checkContents(fileBuf), 'checksum is ok') 51 | 52 | const decSecretKey = metadata.decryptBlobKey(decMetadata, passphrase) 53 | t.deepEqual(decSecretKey, secretKey, 'secret keys are the same') 54 | 55 | const decMessage = decryptBlob(decFileObj.blob, decMetadata, decSecretKey) 56 | const decData = JSON.parse(decMessage.toString('utf8')) 57 | 58 | t.deepEqual(dataToEncrypt, decData, 'secret data is the same') 59 | 60 | t.end() 61 | }) 62 | -------------------------------------------------------------------------------- /tests/metadata.test.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import test from 'tape' 3 | import * as metadata from '../src/metadata' 4 | import { IV_LEN_BYTES } from '../src/crypto' 5 | 6 | test('encode / decode metadata', (t) => { 7 | t.plan(1) 8 | 9 | var obj = { 10 | scrypt: { 11 | salt: crypto.randomBytes(32), 12 | n: 16384, 13 | r: 8, 14 | p: 1 15 | }, 16 | cipher: 'aes-256-gcm', 17 | blobKey: { 18 | iv: crypto.randomBytes(IV_LEN_BYTES), 19 | authTag: Buffer.alloc(16), 20 | key: Buffer.alloc(32) 21 | }, 22 | blob: { 23 | iv: crypto.randomBytes(IV_LEN_BYTES), 24 | authTag: Buffer.alloc(16) 25 | } 26 | } 27 | 28 | var obj2 = metadata.decode(metadata.encode(obj)) 29 | t.deepEqual(obj2, obj, 'verify objects are the same') 30 | 31 | t.end() 32 | }) 33 | 34 | test('encryptBlobKey / decryptBlobKey ', (t) => { 35 | t.plan(2) 36 | 37 | const blobKey = crypto.randomBytes(32) 38 | const passphrase = 'open sesame!' 39 | let md = metadata.create() 40 | 41 | t.doesNotThrow(() => metadata.encryptBlobKey(md, passphrase, blobKey)) 42 | const actualBlobKey = metadata.decryptBlobKey(md, passphrase) 43 | 44 | t.deepEqual(actualBlobKey, blobKey, 'blob keys are the same') 45 | 46 | t.end() 47 | }) 48 | --------------------------------------------------------------------------------