Skip to content

Commit

Permalink
Merge 559b9b1 into 35d3d7a
Browse files Browse the repository at this point in the history
  • Loading branch information
garbados committed Aug 5, 2021
2 parents 35d3d7a + 559b9b1 commit e016d4e
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 21 deletions.
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,18 @@ const crypt = new Crypt(password)
- `password`: A string. Make sure it's good! Or not.
- `salt`: A salt, either as a byte array or a string. If omitted or falsy, a random salt is generated. *Rather than bother carrying this with you, use `crypt.export()` and `Crypt.import()` to transport your credentials!*
- `opts`: Options!
- `opts.iterations`: The number of iterations to use to hash your password via [pbkdf2](https://en.wikipedia.org/wiki/PBKDF2). Defaults to 10,000.
- `opts.saltLength`: The length of the salt to be generated, in bytes. Defaults to 32.
- `opts.iterations`: The number of iterations to use to hash your password via [Argon2](https://en.wikipedia.org/wiki/Argon2). Defaults to 100.
- `opts.saltLength`: The length of the salt to be generated, in bytes. Defaults to 16.
- `opts.memorySize`: The number of kilobytes of RAM to use to generate a cryptographic key from a password. Defaults to 4096 KB. *Must be a power of 2.*
- `opts.parallelism`: The number of threads to use when generating a cryptographic key. Defaults to 1, as Crypt assumes it operates in a single-threaded environment.

#### A note on modifying default settings

Crypt's defaults have been selected to afford a cryptographic strength that does not impose significant performance penalties to applications doing a lot of encrypted reads and writes, such as those using [crypto-pouch](https://github.com/calvinmetcalf/crypto-pouch). If you are facing an attacker with significant resources, such as state actors, consider increasing the `iterations` and `memorySize` options. This will impose notable performance penalties, but as a rule slower cryptography means slower cracking. **You should only modify these settings if you know what you are doing!**

### async Crypt.new(password, [salt, [opts]])

An asynchronous version of Crypt's constructor. Unlike the synchronous constructor, using `.new()` awaits Crypt's setup phase, so that you can explicitly await any problems during setup rather than wait for them to surface during encryption.

### async Crypt.import(password, exportString) => new Crypt

Expand All @@ -76,8 +86,6 @@ Exports a string you can use to create a new Crypt instance with `Crypt.import()

If decryption fails, for example because your password is incorrect, an error will be thrown.

### async crypt.

## Development

First, get the source:
Expand All @@ -102,7 +110,22 @@ To see test coverage:
$ npm run cov
```

## Also: How To Securely Store A Password
## Regarding passwords

Passwords should be generally considered a form of vulnerability. An attacker that manages to solve your encryption, such as by exfiltrating encrypted values and then brute-forcing the decryption key, may gain access to the password you used to encrypt those values. As a result, I highly advise deriving a strong passcode from a memorable passphrase in a non-reversible way, such as by using Argon2 or another derivation function yourself. By using this passcode only for a specific app, you ensure that an attacker will not be able to discover your passphrase even if they crack the passcode.

You can even do this with Crypt itself:

```javascript
const globalCrypt = await Crypt.new('your_passphrase')
const passcode = await globalCrypt.encrypt('some_context_phrase') // like the name of the associated app or service
const contextCrypt = await Crypt.new(passcode)
// now you can use contextCrypt to encrypt your data
// with strong guarantees that even if an attacker cracks your crypto,
// they will not obtain your passphrase.
```

## How To Securely Store A Password

For a password-based encryption system, it makes sense to have a good reference on how to store passwords in a database. To this effect I have written [this gist](https://gist.github.com/garbados/29ca945d5964ef85e7936804c23edb9d#file-how_to_store_passwords-js) to demonstrate safe password obfuscation and verification. If you have any issue with the advice offered there, leave a comment!

Expand Down
52 changes: 36 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,76 @@
const { secretbox, hash, randomBytes } = require('tweetnacl')
const { decodeUTF8, encodeUTF8, encodeBase64, decodeBase64 } = require('tweetnacl-util')
const { pbkdf2, createSHA512 } = require('hash-wasm')
const { argon2id, createSHA512 } = require('hash-wasm')

const NO_PASSWORD = 'A password is required for encryption or decryption.'
const COULD_NOT_DECRYPT = 'Could not decrypt!'

const SALT_LENGTH = 32
const KEY_LENGTH = 32
const ITERATIONS = 1e4

const SALT_LENGTH = 16 // size of salt in bytes; argon2 authors recommend 16 (128 bits)
const MEMORY_SIZE = 2 ** 12 // 2 ** N kilobytes; increase N to raise strength
const ITERATIONS = 1e2 // 1 and N zeroes; increase N to raise strength
const PARALLELISM = 1 // how many threads to spawn. crypt assumes a single-threaded environment.

// convenience method for combining given opts with defaults
// istanbul ignore next // for some reason
function getDefaultOpts (opts = {}) {
function getOpts (opts = {}) {
return {
saltLength: opts.saltLength || SALT_LENGTH,
memorySize: opts.memorySize || MEMORY_SIZE,
iterations: opts.iterations || ITERATIONS,
saltLength: opts.saltLength || SALT_LENGTH
parallelism: opts.parallelism || PARALLELISM
}
}

module.exports = class Crypt {
// derive an encryption key from given parameters
static async deriveKey (password, salt, opts = {}) {
opts = getDefaultOpts(opts)
if (!salt) { salt = randomBytes(opts.saltLength) }
const key = await pbkdf2({
// parse opts
opts = getOpts(opts)
const { saltLength, ...keyOpts } = opts
// generate a random salt if one is not provided
if (!salt) { salt = randomBytes(saltLength) }
const key = await argon2id({
password,
salt,
iterations: opts.iterations,
...keyOpts,
hashLength: KEY_LENGTH,
hashFunction: createSHA512(),
outputType: 'binary'
})
return { key, salt }
}

// create a new Crypt instance from
static async import (password, exportString) {
// parse exportString: decodeBase64 =>
// parse exportString into its components
const fullMessage = decodeBase64(exportString)
const tempSalt = fullMessage.slice(0, SALT_LENGTH)
const tempSalt = fullMessage.slice(0, SALT_LENGTH) // temp crypt uses defaults
const exportBytes = fullMessage.slice(SALT_LENGTH)
const exportEncrypted = encodeUTF8(exportBytes)
const tempCrypt = new Crypt(password, tempSalt)
// create a temporary Crypt with the given salt
const tempCrypt = await Crypt.new(password, tempSalt)
// so we can decrypt and parse exportString's inner settings
const exportJson = await tempCrypt.decrypt(exportEncrypted)
const [saltString, opts] = JSON.parse(exportJson)
const salt = decodeBase64(saltString)
return new Crypt(password, salt, opts)
// return a new crypt with the imported settings
return Crypt.new(password, salt, opts)
}

// async constructor which awaits setup
static async new (...args) {
const crypt = new Crypt(...args)
await crypt._setup
return crypt
}

constructor (password, salt, opts = {}) {
if (!password) { throw new Error(NO_PASSWORD) }
this._raw_pass = password
this._pass = hash(decodeUTF8(password))
this._opts = getDefaultOpts(opts)
this._opts = getOpts(opts)
this._setup = Crypt.deriveKey(this._pass, salt, this._opts)
.then(({ key, salt: newSalt }) => {
this._key = key
Expand All @@ -59,8 +80,7 @@ module.exports = class Crypt {

async export () {
await this._setup
const tempCrypt = new Crypt(this._raw_pass)
await tempCrypt._setup
const tempCrypt = await Crypt.new(this._raw_pass)
const saltString = encodeBase64(this._salt)
const exportJson = JSON.stringify([saltString, this._opts])
const exportEncrypted = await tempCrypt.encrypt(exportJson)
Expand Down
8 changes: 8 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const PASSWORD = 'password'
const BENCHMARK = 1e4 // note: 1e4 = 1 and 4 zeroes (10,000)

describe('crypt', function () {
this.timeout(1000 * 10) // 10 seconds

it('should derive a key from a password', async function () {
let { key, salt } = await Crypt.deriveKey(PASSWORD)
key = encodeBase64(key)
Expand Down Expand Up @@ -75,4 +77,10 @@ describe('crypt', function () {
const decrypted = await crypt2.decrypt(encrypted)
assert.equal(decrypted, PLAINTEXT)
})

it('should create an instance asynchronously', async function () {
const crypt = await Crypt.new(PASSWORD)
assert(crypt._key)
assert(crypt._salt)
})
})

0 comments on commit e016d4e

Please sign in to comment.