├── .gitignore
├── test
├── minilock.png
├── test.js
├── deriveKey.js
├── chunkSize.js
└── encryptDecrypt.js
├── .travis.yml
├── collaborators.md
├── index.js
├── lib
├── getkeypair.js
├── collectTemp.js
├── encryptBodyStream.js
├── util.js
├── decrypt.js
├── decryptChunks.js
├── parseHeader.js
└── encrypt.js
├── package.json
└── readme.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/test/minilock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/finnp/minilock/HEAD/test/minilock.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '0.12'
4 | - '5.6'
5 | sudo: false
6 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | require('./encryptDecrypt')
2 | require('./deriveKey')
3 | require('./chunkSize')
4 |
--------------------------------------------------------------------------------
/collaborators.md:
--------------------------------------------------------------------------------
1 | ## Collaborators
2 |
3 | minilock is only possible due to the excellent work of the following collaborators:
4 |
5 |
8 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var encrypt = require('./lib/encrypt')
2 | var decrypt = require('./lib/decrypt')
3 | var util = require('./lib/util')
4 | var getKeyPair = require('./lib/getkeypair.js')
5 |
6 | module.exports.encryptStream = encrypt
7 | module.exports.decryptStream = decrypt.decryptStream
8 |
9 | module.exports.getKeyPair = getKeyPair
10 |
11 | module.exports.publicKeyFromId = util.publicKeyFromId
12 | module.exports.idFromPublicKey = util.idFromPublicKey
13 | module.exports.getMiniLockID = util.idFromPublicKey
14 |
15 |
--------------------------------------------------------------------------------
/lib/getkeypair.js:
--------------------------------------------------------------------------------
1 | var scrypt = require('scrypt-async')
2 | var nacl = require('tweetnacl')
3 | var BLAKE2s = require('blake2s-js')
4 |
5 | module.exports = getKeyPair
6 |
7 | function getKeyPair (key, salt, callback) {
8 | var keyHash = new BLAKE2s(32)
9 | keyHash.update(nacl.util.decodeUTF8(key))
10 |
11 | getScryptKey(keyHash.digest(), nacl.util.decodeUTF8(salt), function (keyBytes) {
12 | callback(nacl.box.keyPair.fromSecretKey(keyBytes))
13 | })
14 | }
15 |
16 | function getScryptKey (key, salt, callback) {
17 | scrypt(key, salt, 17, 8, 32, 1000, function scryptCb (keyBytes) {
18 | callback(nacl.util.decodeBase64(keyBytes))
19 | }, 'base64')
20 | }
21 |
--------------------------------------------------------------------------------
/lib/collectTemp.js:
--------------------------------------------------------------------------------
1 | // duplex stream that collects input chunks to disk, waits for the last to arrive,
2 | // then starts writing to the output
3 |
4 | var fs = require('fs')
5 | var duplexify = require('duplexify')
6 |
7 | module.exports = collectTemp
8 |
9 | function collectTemp (tempFileName) {
10 | var duplex = duplexify()
11 | var fileWrite = fs.createWriteStream(tempFileName)
12 | duplex.setWritable(fileWrite)
13 | fileWrite.on('finish', function () {
14 | var fileRead = fs.createReadStream(tempFileName)
15 | fileRead.on('end', function () {
16 | fs.unlink(tempFileName)
17 | })
18 | duplex.setReadable(fileRead)
19 | })
20 | return duplex
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/lib/encryptBodyStream.js:
--------------------------------------------------------------------------------
1 | var stream = require('stream')
2 |
3 | module.exports = encryptBodyStream
4 |
5 | function encryptBodyStream (encryptor, hash) {
6 | var encryptChunks = new stream.Transform()
7 | encryptChunks._transform = transform
8 | encryptChunks._flush = flush
9 |
10 | function transform (chunk, enc, cb) {
11 | var encryptedChunk = encryptor.encryptChunk(new Uint8Array(chunk || []), false)
12 | this.push(new Buffer(encryptedChunk))
13 | if (hash) hash.update(encryptedChunk)
14 | cb()
15 | }
16 |
17 | function flush (cb) {
18 | // encrypting one last empty chunk
19 | var encryptedChunk = encryptor.encryptChunk(new Uint8Array([]), true)
20 | if (hash) hash.update(encryptedChunk)
21 | encryptor.clean()
22 | this.push(new Buffer(encryptedChunk))
23 | cb()
24 | }
25 | return encryptChunks
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minilock",
3 | "version": "1.0.0",
4 | "description": "encrypt and decrypt with minilock",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "node test/test.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/finnp/minilock.git"
12 | },
13 | "keywords": [
14 | "encryption",
15 | "decryption",
16 | "cryptography"
17 | ],
18 | "dependencies": {
19 | "blake2s-js": "1.0.3",
20 | "block-stream2": "1.0.0",
21 | "bs58": "2.0.1",
22 | "debug": "2.2.0",
23 | "duplexify": "3.4.2",
24 | "nacl-stream": "0.3.3",
25 | "pumpify": "1.3.3",
26 | "scrypt-async": "1.0.1",
27 | "tweetnacl": "0.13.1"
28 | },
29 | "author": "Finn Pauls",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/finnp/minilock/issues"
33 | },
34 | "homepage": "https://github.com/finnp/minilock#readme",
35 | "devDependencies": {
36 | "hasha": "^1.0.1",
37 | "tape": "^4.0.1"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | var Base58 = require('bs58')
2 | var BLAKE2s = require('blake2s-js')
3 | var os = require('os')
4 | var nacl = require('tweetnacl')
5 | var path = require('path')
6 |
7 | module.exports.publicKeyFromId = publicKeyFromId
8 | module.exports.idFromPublicKey = idFromPublicKey
9 | module.exports.hex = hex
10 | module.exports.temporaryFilename = temporaryFilename
11 |
12 | function publicKeyFromId (id) {
13 | // The last byte is the checksum, slice it off
14 | return new Uint8Array(Base58.decode(id).slice(0, 32))
15 | }
16 |
17 | function idFromPublicKey (publicKey) {
18 | var hash = new BLAKE2s(1)
19 | hash.update(publicKey)
20 |
21 | // The last byte is the checksum.
22 | var checksum = new Buffer([hash.digest()[0]])
23 |
24 | var fullBuf = Buffer.concat([new Buffer(publicKey), checksum])
25 | return Base58.encode(fullBuf)
26 | }
27 |
28 | function hex (data) {
29 | return new Buffer(data).toString('hex')
30 | }
31 |
32 | function temporaryFilename () {
33 | return path.resolve(os.tmpdir(),
34 | '.mlck-' + hex(nacl.randomBytes(32)) + '.tmp')
35 | }
36 |
--------------------------------------------------------------------------------
/lib/decrypt.js:
--------------------------------------------------------------------------------
1 | var duplexify = require('duplexify')
2 | var pumpify = require('pumpify')
3 | var getKeyPair = require('./getkeypair')
4 | var util = require('./util')
5 | var parseHeaderStream = require('./parseHeader')
6 | var decryptChunksStream = require('./decryptChunks')
7 |
8 | module.exports.decryptStream = decryptStream
9 |
10 | function decryptStream (email, passphrase) {
11 | var stream = duplexify()
12 | getKeyPair(passphrase, email, function (keyPair) {
13 | var ourId = util.idFromPublicKey(keyPair.publicKey)
14 | var parseHeader = parseHeaderStream(keyPair, ourId)
15 | var decryptChunks = decryptChunksStream()
16 | parseHeader.on('decryptInfo', function (decryptInfo) {
17 | decryptChunks.setDecryptInfo(decryptInfo)
18 | })
19 | var transform = pumpify([
20 | parseHeader,
21 | decryptChunks
22 | ])
23 | parseHeader.on('sender', function (sender) {
24 | stream.emit('sender', sender)
25 | })
26 | decryptChunks.on('fileName', function (fileName) {
27 | stream.emit('fileName', fileName)
28 | })
29 | stream.setWritable(transform)
30 | stream.setReadable(transform)
31 | })
32 | return stream
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/test/deriveKey.js:
--------------------------------------------------------------------------------
1 | // based on: https://github.com/kaepora/miniLock/blob/b417c5164611cab8a79ece538ad6b47452797144/test/tests/deriveKey.js
2 | // Key derivation test.
3 | var minilock = require('../')
4 | var test = require('tape')
5 | var Base58 = require('bs58')
6 | var nacl = require('tweetnacl')
7 |
8 | var passphrase = 'This passphrase is supposed to be good enough for miniLock. :-)'
9 |
10 | test('deriveKey', function (t) {
11 | minilock.getKeyPair(passphrase, 'miniLockScrypt..', function (keys) {
12 | t.deepEqual(Object.keys(keys).length, 2, 'sessionKeys is filled')
13 | t.deepEqual(typeof (keys), 'object', 'Type check')
14 | t.deepEqual(typeof (keys.publicKey), 'object', 'Public key type check')
15 | t.deepEqual(typeof (keys.secretKey), 'object', 'Secret key type check')
16 | t.deepEqual(keys.publicKey.length, 32, 'Public key length')
17 | t.deepEqual(keys.secretKey.length, 32, 'Secret key length')
18 | t.deepEqual(
19 | Base58.encode(keys.publicKey),
20 | 'EWVHJniXUFNBC9RmXe45c8bqgiAEDoL3Qojy2hKt4c4e',
21 | 'Public key Base58 representation'
22 | )
23 | t.deepEqual(
24 | nacl.util.encodeBase64(keys.secretKey),
25 | '6rcsdGAhF2rIltBRL+gwvQTQT7JMyei/d2JDrWoo0yw=',
26 | 'Secret key Base64 representation'
27 | )
28 | t.deepEqual(
29 | minilock.getMiniLockID(keys.publicKey),
30 | '22d9pyWnHVGQTzCCKYEYbL4YmtGfjMVV3e5JeJUzLNum8A',
31 | 'miniLock ID from public key'
32 | )
33 | })
34 | t.end()
35 | })
36 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # minilock [](https://david-dm.org/finnp/minilock) [](https://travis-ci.org/finnp/minilock)
2 | [](https://nodei.co/npm/minilock/)
3 |
4 | WIP
5 |
6 | This module is based on the core of the [minilock-cli](https://www.npmjs.com/package/minilock-cli) module.
7 |
8 | It's a node implementation of the [miniLock](https://github.com/kaepora/miniLock) encryption.
9 |
10 | ## example
11 |
12 | ```js
13 | var minilock = require('minilock')
14 | minilock.encryptStream(alice.email, alice.passphrase, bob.id, function (err, encrypt) {
15 | minilock.decryptStream(bob.email, bob.passphrase, function (err, decrypt) {
16 | var stream = fs.createReadStream('test.js')
17 | .pipe(encrypt)
18 | .pipe(decrypt)
19 | .pipe(fs.createWriteStream('test-copy.js'))
20 | decrypt.on('sender', function (minilockID) {
21 | console.log('sender was', minilockID)
22 | })
23 | })
24 | })
25 | ```
26 |
27 | ## api
28 | Note: The streaming interfaces will load the entire input stream into memory.
29 |
30 | ### encryptStream(email, passphrase, toid[, opts])
31 |
32 | Options
33 | * `fileName` fileName for the encryption (will be empty by default)
34 | * `chunkSize` chunkSize to use
35 |
36 | ### decryptStream(email, passphrase)
37 |
38 | emits the sender's miniLockID with a 'sender' event
39 |
40 | ### publicKeyFromId(id)
41 |
42 | returns a Uint8Array
43 |
44 | ### idFromPublicKey(publicKey)
45 |
46 | publicKey as Uint8Array
--------------------------------------------------------------------------------
/test/chunkSize.js:
--------------------------------------------------------------------------------
1 | var minilock = require('../')
2 | var test = require('tape')
3 | var hasha = require('hasha')
4 | var fs = require('fs')
5 | var path = require('path')
6 |
7 | var alice = {
8 | email: 'test@test.de', // salt
9 | passphrase: 'happy careful but neighbour round develop therefore', // key
10 | id: '6dZ3gQinFhGH1FS7UwxU8Q29xNceBS78ZGdD7FwfKHC9g' // publickey
11 | }
12 |
13 | var bob = {
14 | email: 'bob@cat.org',
15 | passphrase: 'wheel mention steam open sheep drop scissors',
16 | id: 'cvoPZ4NCbQ4QxrgV3x2HUcSu6nH4odY4DDeR8HwXTyzN2'
17 | }
18 |
19 | var TEST_FILE = path.join(__dirname, 'minilock.png')
20 |
21 | test('encrypt alice to bob with too small chunkSize', function (t) {
22 | t.plan(2)
23 | var encrypt = minilock.encryptStream(alice.email, alice.passphrase, bob.id, {chunkSize: 255})
24 | encrypt.on('error', function (err) {
25 | if (err) t.ok(err, 'error')
26 | t.ok(err.message.indexOf('size') > -1, 'error message')
27 | })
28 | encrypt.on('data', function () {
29 | t.fail('data event')
30 | })
31 | encrypt.write('test')
32 | encrypt.end()
33 | })
34 |
35 | test('encrypt/decrypt alice to bob with sufficient chunkSize', function (t) {
36 | t.plan(3)
37 | var encrypt = minilock.encryptStream(alice.email, alice.passphrase, bob.id, {chunkSize: 256})
38 | var decrypt = minilock.decryptStream(bob.email, bob.passphrase)
39 | encrypt.on('error', function (e) {
40 | t.fail(e.message)
41 | })
42 | decrypt.on('sender', function (id) {
43 | t.equal(id, alice.id, 'correct sender id')
44 | })
45 | decrypt.on('fileName', function (fileName) {
46 | t.equal(fileName, '', 'empty filename')
47 | })
48 | hasha.fromFile(TEST_FILE, function (err, originalHash) {
49 | if (err) t.fail(err)
50 | var stream = fs.createReadStream(TEST_FILE)
51 | .pipe(encrypt)
52 | .pipe(decrypt)
53 | hasha.fromStream(stream, function (err, hash) {
54 | if (err) t.fail(err)
55 | t.equal(originalHash, hash, 'correct file hash')
56 | })
57 | })
58 | })
59 |
--------------------------------------------------------------------------------
/lib/decryptChunks.js:
--------------------------------------------------------------------------------
1 | var nacl = require('tweetnacl')
2 | var naclStream = require('nacl-stream')
3 | var debug = require('debug')
4 | var stream = require('stream')
5 | var BLAKE2s = require('blake2s-js')
6 | var util = require('./util')
7 |
8 | module.exports = decryptChunks
9 |
10 | function decryptChunks () {
11 | var transform = new stream.Transform()
12 | var originalFilename = null
13 | var buffer = new Buffer(0)
14 | var hash = new BLAKE2s(32)
15 | var decryptInfo = null
16 | var decryptor = null
17 |
18 | transform._transform = function (chunk, enc, cb) {
19 | buffer = Buffer.concat([buffer, chunk])
20 | var push = function (chunk) {
21 | if (!originalFilename && chunk.length > 0) {
22 | // The very first chunk is the original filename.
23 | originalFilename = chunk.toString()
24 | // Strip out any trailing null characters.
25 | this.emit('fileName', (originalFilename + '\0').slice(0, originalFilename.indexOf('\0')))
26 | } else {
27 | this.push(chunk)
28 | }
29 | }.bind(this)
30 | // Decrypt as many chunks as possible.
31 | if (!decryptor) return cb(new Error('setDecryptInfo not called yet'))
32 | decryptChunk(buffer, decryptor, push, hash)
33 | cb()
34 | }
35 | transform._flush = function (cb) {
36 | if (nacl.util.encodeBase64(hash.digest()) !== decryptInfo.fileInfo.fileHash) {
37 | // The 32-byte BLAKE2 hash of the ciphertext must match the value in
38 | // the header.
39 | return cb(new Error('integrity check failed'))
40 | }
41 | cb()
42 | }
43 | transform.setDecryptInfo = function (_decryptInfo) {
44 | decryptInfo = _decryptInfo
45 | decryptor = naclStream.stream.createDecryptor(
46 | nacl.util.decodeBase64(decryptInfo.fileInfo.fileKey),
47 | nacl.util.decodeBase64(decryptInfo.fileInfo.fileNonce),
48 | 0x100000)
49 | }
50 | return transform
51 | }
52 |
53 | function decryptChunk (chunk, decryptor, output, hash) {
54 | while (true) {
55 | var length = chunk.length >= 4 ? chunk.readUIntLE(0, 4, true) : 0
56 | if (chunk.length < 4 + 16 + length) break
57 |
58 | var encrypted = new Uint8Array(chunk.slice(0, 4 + 16 + length))
59 | var decrypted = decryptor.decryptChunk(encrypted, false)
60 |
61 | chunk = chunk.slice(4 + 16 + length)
62 |
63 | if (decrypted) {
64 | debug('Decrypted chunk ' + util.hex(decrypted))
65 |
66 | output(new Buffer(decrypted))
67 | }
68 |
69 | if (hash) hash.update(encrypted)
70 | }
71 |
72 | return chunk
73 | }
74 |
--------------------------------------------------------------------------------
/test/encryptDecrypt.js:
--------------------------------------------------------------------------------
1 | var minilock = require('../')
2 | var fs = require('fs')
3 | var test = require('tape')
4 | var hasha = require('hasha')
5 | var path = require('path')
6 |
7 | var alice = {
8 | email: 'test@test.de', // salt
9 | passphrase: 'happy careful but neighbour round develop therefore', // key
10 | id: '6dZ3gQinFhGH1FS7UwxU8Q29xNceBS78ZGdD7FwfKHC9g' // publickey
11 | }
12 |
13 | var bob = {
14 | email: 'bob@cat.org',
15 | passphrase: 'wheel mention steam open sheep drop scissors',
16 | id: 'cvoPZ4NCbQ4QxrgV3x2HUcSu6nH4odY4DDeR8HwXTyzN2'
17 | }
18 |
19 | var TEST_FILE = path.join(__dirname, 'minilock.png')
20 |
21 | test('encrypt/decrypt alice to alice', function (t) {
22 | t.plan(3)
23 |
24 | var encrypt = minilock.encryptStream(alice.email, alice.passphrase, alice.id)
25 | var decrypt = minilock.decryptStream(alice.email, alice.passphrase)
26 | decrypt.on('sender', function (id) {
27 | t.equal(id, alice.id, 'correct sender id')
28 | })
29 | decrypt.on('fileName', function (fileName) {
30 | t.equal(fileName, '', 'empty filename')
31 | })
32 | hasha.fromFile(TEST_FILE, function (err, originalHash) {
33 | if (err) t.fail(err)
34 | var stream = fs.createReadStream(TEST_FILE)
35 | .pipe(encrypt)
36 | .pipe(decrypt)
37 | hasha.fromStream(stream, function (err, hash) {
38 | if (err) t.fail(err)
39 | t.equal(originalHash, hash, 'correct file hash')
40 | })
41 | })
42 | })
43 |
44 | test('encrypt/decrypt alice to bob', function (t) {
45 | t.plan(3)
46 | var encrypt = minilock.encryptStream(alice.email, alice.passphrase, bob.id)
47 | var decrypt = minilock.decryptStream(bob.email, bob.passphrase)
48 | decrypt.on('sender', function (id) {
49 | t.equal(id, alice.id, 'correct sender id')
50 | })
51 | decrypt.on('fileName', function (fileName) {
52 | t.equal(fileName, '', 'empty filename')
53 | })
54 | hasha.fromFile(TEST_FILE, function (err, originalHash) {
55 | if (err) t.fail(err)
56 | var stream = fs.createReadStream(TEST_FILE)
57 | .pipe(encrypt)
58 | .pipe(decrypt)
59 | hasha.fromStream(stream, function (err, hash) {
60 | if (err) t.fail(err)
61 | t.equal(originalHash, hash, 'correct file hash')
62 | })
63 | })
64 | })
65 |
66 | test('encrypt/decrypt alice to bob with fileName', function (t) {
67 | t.plan(3)
68 | var encrypt = minilock.encryptStream(alice.email, alice.passphrase, bob.id, {fileName: 'test.jpg'})
69 | var decrypt = minilock.decryptStream(bob.email, bob.passphrase)
70 | decrypt.on('sender', function (id) {
71 | t.equal(id, alice.id, 'correct sender id')
72 | })
73 | decrypt.on('fileName', function (fileName) {
74 | t.equal(fileName, 'test.jpg', 'correct fileName')
75 | })
76 | hasha.fromFile(TEST_FILE, function (err, originalHash) {
77 | if (err) t.fail(err)
78 | var stream = fs.createReadStream(TEST_FILE)
79 | .pipe(encrypt)
80 | .pipe(decrypt)
81 | hasha.fromStream(stream, function (err, hash) {
82 | if (err) t.fail(err)
83 | t.equal(originalHash, hash, 'correct file hash')
84 | })
85 | })
86 | })
87 |
88 |
--------------------------------------------------------------------------------
/lib/parseHeader.js:
--------------------------------------------------------------------------------
1 | var nacl = require('tweetnacl')
2 | var stream = require('stream')
3 | var util = require('./util')
4 |
5 | module.exports = parseHeader
6 |
7 | function parseHeader (keyPair, ourId) {
8 | var transform = new stream.Transform()
9 | var buffer = new Buffer(0)
10 | var headerLength = -1
11 | var header = null
12 | var decryptInfo = null
13 | transform._transform = function (chunk, enc, cb) {
14 | if (!header) {
15 | buffer = Buffer.concat([buffer, chunk])
16 | // parse header
17 | if (headerLength < 0 && buffer.length >= 12) {
18 | // header length + magic number
19 | var magicNumber = buffer.slice(0, 8).toString()
20 | if (magicNumber !== 'miniLock') return cb(new Error('incorrect magic number'))
21 | headerLength = buffer.readUIntLE(8, 4, true)
22 |
23 | if (headerLength > 0x3fffffff) return cb(new Error('header too long'))
24 | buffer = new Buffer(buffer.slice(12))
25 | }
26 | if (headerLength > -1) {
27 | // Look for the JSON opening brace.
28 | if (buffer.length > 0 && buffer[0] !== 0x7b) return cb(new Error('JSON opening bracket missing'))
29 |
30 | if (buffer.length >= headerLength) {
31 | // Read the header and parse the JSON object.
32 | header = JSON.parse(buffer.slice(0, headerLength).toString())
33 | if (header.version !== 1) return cb(new Error('unsupported version'))
34 | if (!validateKey(header.ephemeral)) return cb(new Error('could not validate key'))
35 |
36 | decryptInfo = extractDecryptInfo(header, keyPair.secretKey)
37 | if (!decryptInfo || decryptInfo.recipientID !== ourId) return cb(new Error('Not a recipient'))
38 | this.emit('sender', decryptInfo.senderID)
39 | this.emit('decryptInfo', decryptInfo)
40 | buffer = buffer.slice(headerLength)
41 | // emit what is left of the buffer, and then clear it
42 | this.emit(buffer)
43 | buffer = false
44 | }
45 | }
46 | } else {
47 | this.push(chunk)
48 | }
49 | cb()
50 | }
51 | return transform
52 | }
53 |
54 | function validateKey (key) {
55 | var keyRegex = /^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
56 | if (!key) return false
57 | if (!(key.length >= 40 && key.length <= 50)) return false
58 | if (!keyRegex.test(key)) return false
59 |
60 | return nacl.util.decodeBase64(key).length === 32
61 | }
62 |
63 | function extractDecryptInfo (header, secretKey) {
64 | var decryptInfo = null
65 |
66 | var ephemeral = nacl.util.decodeBase64(header.ephemeral)
67 |
68 | for (var i in header.decryptInfo) {
69 | var nonce = nacl.util.decodeBase64(i)
70 |
71 | decryptInfo = nacl.util.decodeBase64(header.decryptInfo[i])
72 | decryptInfo = nacl.box.open(decryptInfo, nonce, ephemeral, secretKey)
73 |
74 | if (decryptInfo) {
75 | decryptInfo = JSON.parse(nacl.util.encodeUTF8(decryptInfo))
76 |
77 | decryptInfo.fileInfo = nacl.util.decodeBase64(decryptInfo.fileInfo)
78 | decryptInfo.fileInfo = nacl.box.open(decryptInfo.fileInfo, nonce, util.publicKeyFromId(decryptInfo.senderID), secretKey)
79 |
80 | decryptInfo.fileInfo = JSON.parse(
81 | nacl.util.encodeUTF8(decryptInfo.fileInfo)
82 | )
83 | break
84 | }
85 | }
86 |
87 | return decryptInfo
88 | }
89 |
--------------------------------------------------------------------------------
/lib/encrypt.js:
--------------------------------------------------------------------------------
1 | var nacl = require('tweetnacl')
2 | var naclStream = require('nacl-stream')
3 | var BLAKE2s = require('blake2s-js')
4 | var debug = require('debug')('minilock')
5 | var duplexify = require('duplexify')
6 | var pumpify = require('pumpify')
7 | var stream = require('stream')
8 | var block = require('block-stream2')
9 | var getKeyPair = require('./getkeypair')
10 | var util = require('./util')
11 | var createEncryptBodyStream = require('./encryptBodyStream')
12 | var collectTemp = require('./collectTemp')
13 |
14 | var MIN_ENCRYPTION_CHUNK_SIZE = 256
15 |
16 | function hex (data) {
17 | return new Buffer(data).toString('hex')
18 | }
19 |
20 | module.exports = encryptStream
21 |
22 | function encryptStream (email, passphrase, toId, opts) {
23 | var stream = duplexify()
24 | getKeyPair(passphrase, email, function (keyPair) {
25 | encryptStreamWithKeyPair(keyPair, toId, opts, function (err, transform) {
26 | if (err) return stream.destroy(err)
27 | transform.on('error', function (err) {
28 | stream.destroy(err)
29 | })
30 | stream.setReadable(transform)
31 | stream.setWritable(transform)
32 | })
33 | })
34 | return stream
35 | }
36 |
37 | function encryptStreamWithKeyPair (keyPair, toIds, opts, cb) {
38 | opts = opts || {}
39 | if (!Array.isArray(toIds)) toIds = [toIds]
40 | var fromId = util.idFromPublicKey(keyPair.publicKey)
41 | debug('Our miniLock ID is ' + fromId)
42 |
43 | var senderInfo = {
44 | id: fromId,
45 | secretKey: keyPair.secretKey
46 | }
47 |
48 | var fileKey = nacl.randomBytes(32)
49 | var fileNonce = nacl.randomBytes(16)
50 |
51 | debug('Using file key ' + hex(fileKey))
52 | debug('Using file nonce ' + hex(fileNonce))
53 |
54 | var chunkSize = MIN_ENCRYPTION_CHUNK_SIZE
55 | if (opts.chunkSize) {
56 | if (opts.chunkSize >= MIN_ENCRYPTION_CHUNK_SIZE) {
57 | chunkSize = opts.chunkSize
58 | } else {
59 | return cb(new Error('chunk size too small'))
60 | }
61 | }
62 |
63 | var encryptor = naclStream.stream.createEncryptor(fileKey, fileNonce, chunkSize)
64 |
65 | var hash = new BLAKE2s(32)
66 |
67 | var filenameBuffer = new Buffer(256).fill(0)
68 | filenameBuffer.write(opts.fileName || '')
69 |
70 | var ciphertextStream = createEncryptBodyStream(encryptor, hash)
71 |
72 | ciphertextStream.write(filenameBuffer)
73 |
74 | var addHeader = new stream.Transform()
75 |
76 | var first = false
77 | addHeader._transform = function (chunk, enc, cb) {
78 | if (!first) {
79 | // This is the 32-byte BLAKE2 hash of all the ciphertext.
80 | var fileHash = hash.digest()
81 | debug('File hash is ' + hex(fileHash))
82 |
83 | var fileInfo = {
84 | fileKey: nacl.util.encodeBase64(fileKey),
85 | fileNonce: nacl.util.encodeBase64(fileNonce),
86 | fileHash: nacl.util.encodeBase64(fileHash)
87 | }
88 | var header = makeHeader(toIds, senderInfo, fileInfo)
89 |
90 | var headerLength = new Buffer(4)
91 | headerLength.writeUInt32LE(header.length)
92 |
93 | debug('Header length is ' + hex(headerLength))
94 |
95 | var outputHeader = Buffer.concat([
96 | // The file always begins with the magic bytes 0x6d696e694c6f636b.
97 | new Buffer('miniLock'), headerLength, new Buffer(header)
98 | ])
99 | this.push(outputHeader)
100 | }
101 | cb(null, chunk)
102 | }
103 |
104 | cb(null, pumpify([
105 | block({size: chunkSize, zeroPadding: false}),
106 | ciphertextStream,
107 | collectTemp(util.temporaryFilename()),
108 | addHeader
109 | ]))
110 | }
111 |
112 | function makeHeader (ids, senderInfo, fileInfo) {
113 | var ephemeral = nacl.box.keyPair()
114 | var header = {
115 | version: 1,
116 | ephemeral: nacl.util.encodeBase64(ephemeral.publicKey),
117 | decryptInfo: {}
118 | }
119 |
120 | debug('Ephemeral public key is ' + hex(ephemeral.publicKey))
121 | debug('Ephemeral secret key is ' + hex(ephemeral.secretKey))
122 |
123 | ids.forEach(function (id, index) {
124 | debug('Adding recipient ' + id)
125 |
126 | var nonce = nacl.randomBytes(24)
127 | var publicKey = util.publicKeyFromId(id)
128 |
129 | debug('Using nonce ' + hex(nonce))
130 |
131 | var decryptInfo = {
132 | senderID: senderInfo.id,
133 | recipientID: id,
134 | fileInfo: fileInfo
135 | }
136 |
137 | decryptInfo.fileInfo = nacl.util.encodeBase64(nacl.box(
138 | nacl.util.decodeUTF8(JSON.stringify(decryptInfo.fileInfo)),
139 | nonce,
140 | publicKey,
141 | senderInfo.secretKey
142 | ))
143 |
144 | decryptInfo = nacl.util.encodeBase64(nacl.box(
145 | nacl.util.decodeUTF8(JSON.stringify(decryptInfo)),
146 | nonce,
147 | publicKey,
148 | ephemeral.secretKey
149 | ))
150 |
151 | header.decryptInfo[nacl.util.encodeBase64(nonce)] = decryptInfo
152 | })
153 |
154 | return JSON.stringify(header)
155 | }
156 |
--------------------------------------------------------------------------------