├── .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 | 6 | 7 |
maxogdenGitHub/maxogden
finnpGitHub/finnp
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 [![Dependency Status](https://david-dm.org/finnp/minilock.svg)](https://david-dm.org/finnp/minilock) [![Build Status](https://travis-ci.org/finnp/minilock.svg?branch=master)](https://travis-ci.org/finnp/minilock) 2 | [![NPM](https://nodei.co/npm/minilock.png)](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 | --------------------------------------------------------------------------------