diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..0eacc60
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,3 @@
+## 2017-11-13 Release 0.1.0
+
+Initial release.
diff --git a/LICENSE b/LICENSE
index d179904..e49d64d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017 C2FO Lab
+Copyright (c) 2017 C2FO
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 4454820..4849b5a 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,740 @@
-# IPFSecret
-Symmetric cryptography wrapper for IPFS files with web gateway support
+[![npm](https://img.shields.io/npm/v/ipfsecret.svg)]() [![license](https://img.shields.io/github/license/c2fo-lab/ipfsecret.svg)]() [![npm](https://img.shields.io/npm/dw/ipfsecret.svg)]() [![node](https://img.shields.io/node/v/ipfsecret.svg)]() [![GitHub last commit](https://img.shields.io/github/last-commit/c2fo-lab/ipfsecret.svg)]()
+
+ * [Introduction](#introduction)
+ * [Install](#install)
+ * [Quickstart](#quickstart)
+ * [Warnings](#warnings)
+ * [Testing](#testing)
+ * [API](#api)
+ * [Command-line web gateways](#command-line-web-gateways)
+ * [Implementation](#implementation)
+ * [Passphrase file](#passphrase-file)
+ * [Security](#security)
+ * [Troubleshooting](#troubleshooting)
+ * [Acknowledgements](#acknowledgements)
+ * [Todo](#todo)
+ * [See also](#see-also)
+
+
+
+
+
+# Introduction
+
+IPFSecret lets you encrypt and decrypt IPFS files with a secret passphrase.
+
+If you are new to IPFS, there are guides for [getting started](https://ipfs.io/docs/getting-started/) and [running the daemon with the right port](https://github.com/ipfs/js-ipfs-api#running-the-daemon-with-the-right-port).
+
+# Install
+
+ λ npm install -g ipfsecret
+
+# Quickstart
+
+## Add a file
+
+ λ ipfsecret add README.md
+ Passphrase?
+ Confirm passphrase:
+ QmeVMn7oLoSC7ShLeLPu2tMrGq4TEr9icSxPbKxAF7CJ7Q
+
+## Get a file
+
+ λ ipfsecret get QmeVMn7oLoSC7ShLeLPu2tMrGq4TEr9icSxPbKxAF7CJ7Q
+ Passphrase?
+ λ head -n1 ipfsecret-decrypted-QmeVMn7oLoSC7ShLeLPu2tMrGq4TEr9icSxPbKxAF7CJ7Q
+ * [Introduction](#introduction)
+ λ
+
+## Add a directory
+
+ λ ipfsecret add -r node_modules/webcrypto-crypt
+ Passphrase?
+ Confirm passphrase:
+ QmfSi8rg7ismstGwDKoEwKySCr8Ptt7XHV1xMkB27DAARv
+
+## Get a directory
+
+ λ ipfsecret get QmfSi8rg7ismstGwDKoEwKySCr8Ptt7XHV1xMkB27DAARv
+ Passphrase?
+ λ ls ipfsecret-decrypted-QmfSi8rg7ismstGwDKoEwKySCr8Ptt7XHV1xMkB27DAARv/node_modules/webcrypto-crypt/
+ CHANGES.md README.md bin examples lib test
+ LICENSE SIGNED.txt dist index.js package.json
+
+## Add a file, include web interface
+
+ λ ipfsecret add -w README.md
+ Passphrase?
+ Confirm passphrase:
+ http://127.0.0.1:8080/ipfs/QmNndRMk9sQzYGmszosGCYUiDm9WBKGy512bLZNoxcAcPm/ipfsecret.html
+
+## Add a directory, include web interface
+
+ λ ipfsecret add -wr node_modules/webcrypto-crypt
+ Passphrase?
+ Confirm passphrase:
+ http://localhost:8080/ipfs/QmWCGgmQdT1xNd44kUwEqY61aX551kRMwrXdvBPBGA1Qto/ipfsecret.html
+
+# Warnings
+
+* File contents added via IPFSecret are encrypted but file and directory names are stored in clear text.
+* There are [problems with symmetric algorithms](http://web.archive.org/web/20150916184759/http://www.informit.com/articles/article.aspx?p=102212).
+* Depending on [pinning](https://discuss.ipfs.io/t/replication-on-ipfs-or-the-backing-up-content-model/372/2) activity, your encrypted data could be distributed to many nodes across the IPFS network.
+* Entities with access to your IPFSecret multihashes that then guess, learn, or crack the relevant passphrase will be able to decrypt your data.
+* Unknown bugs in this code or its dependency tree (e.g., the [webcrypto-crypt](https://c2fo-lab.github.io/webcrypto-crypt) package) could render encryption ineffective.
+* Downloading large files over IPFS web gateways can be slow and in the case of decryption there is currently no feedback on download progress.
+* [Private Networks](https://github.com/ipfs/go-ipfs/blob/master/docs/experimental-features.md#private-networks), [public-key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) tools, or something like [Decentralized Could](https://decentralized.cloud/) or [Firefox Send](https://send.firefox.com/) may better fit your needs.
+
+# Testing
+
+## Node.js
+
+Optionally run the relevant daemon using the [```--offline```](https://github.com/ipfs/go-ipfs/pull/2696#issuecomment-242664950) option for the duration of the tests. Tests assume the API settings present in [```lib/config.js```](https://github.com/c2fo-lab/ipfsecret/blob/master/lib/config.json#L56-L60).
+
+ λ npm run test
+
+## Web browsers
+
+As part of the install process, IPFSecret adds a multihash for browser testing and extracts it to ```.examples/```. The password will be ```justtesting``` and you can access it over the local web gateway using the same multihash.
+
+## Tested environments
+
+| **OS** | **Environment** | **Version** |
+| :-------- | :------- | :------- |
+| Mac Sierra | Chrome | 62 |
+| Mac Sierra | Firefox | 57 |
+| Mac Sierra | Node | 8.9.1 |
+| Mac Sierra | Safari | 11.0 |
+
+### Help
+
+#### Commands
+
+ λ ipfsecret --help
+ Usage: ipfsecret [options]
+
+ Commands:
+ get Retrieve & decrypt encrypted files from IPFS
+ add Encrypt & add files to IPFS
+ list List known HTTPS gateways
+
+ Options:
+ --debug, -d Print debugging info to stderr [boolean] [default: false]
+ --api, -a Specify IPFS API configuration [string] [default: "/ip4/127.0.0.1/tcp/5001"]
+ --gateway, -g Use this HTTP(S) gateway when returning gateway address [string] [default: false]
+ --version, -v Display version and exit [boolean] [default: false]
+ --help Show help [boolean]
+
+##### add
+
+ λ ipfsecret add -h
+ File or directory required
+ Usage: ipfsecret add [options] [file|dir]
+
+ Options:
+ --debug, -d Print debugging info to stderr [boolean] [default: false]
+ --api, -a Specify IPFS API configuration [string] [default: "/ip4/127.0.0.1/tcp/5001"]
+ --gateway, -g Use this HTTP(S) gateway when returning gateway address [string] [default: false]
+ --version, -v Display version and exit [boolean] [default: false]
+ --help Show help [boolean]
+ --web, -w Add web interface [boolean] [default: "false"]
+ --naked, -n With --web, return naked hash vs URL [boolean] [default: "false"]
+ --recursive, -r Add as directory, recursively [boolean] [default: "false"]
+ --hidden, -H When adding directory, include hidden files [boolean] [default: "false"]
+
+##### get
+
+ λ ipfsecret get --help
+ Multihash required
+ Usage: ipfsecret get [multihash]
+
+ Options:
+ --debug, -d Print debugging info to stderr [boolean] [default: false]
+ --api, -a Specify IPFS API configuration [string] [default: "/ip4/127.0.0.1/tcp/5001"]
+ --gateway, -g Use this HTTP(S) gateway when returning gateway address [string] [default: false]
+ --version, -v Display version and exit [boolean] [default: false]
+ --help Show help [boolean]
+ --output, -o Path where output should be stored [string]
+
+#### example of specifying output paths
+
+ λ ipfsecret get -o decrypted.md QmXCsAFuP7Jv2bePvcZEmHeygSHLYfVEB9rtkvhaKF5pL9
+ Passphrase?
+ λ head -n1 decrypted.md
+ * [Purpose](#purpose)
+
+ λ ipfsecret get -o decrypted-dir-test QmfSi8rg7ismstGwDKoEwKySCr8Ptt7XHV1xMkB27DAARv
+ Passphrase?
+ λ ls decrypted-dir-test/node_modules/webcrypto-crypt/
+ CHANGES.md README.md bin examples lib test
+ LICENSE SIGNED.txt dist index.js package.json
+
+# API
+
+Please assume the following lines precede these examples:
+
+```javascript
+const IPFSecret = require('ipfsecret'),
+ ipfsecret = new IPFSecret();
+```
+
+## ipfsecret.add(path, passphrase)
+
+Wraps [ipfs.files.add](https://github.com/ipfs/interface-ipfs-core/tree/master/API/files) to require a ```path``` to add to IPFS and a ```passphrase``` for encryption. Encrypts each file found along the path using [webcrypto-crypt](https://c2fo-lab.github.io/webcrypto-crypt) and appends the ```.wcrypt``` suffix before adding.
+
+### Add file
+
+```javascript
+ipfsecret.add('./README.md', 'justtesting')
+ .then(results => {
+ console.log(results);
+ })
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+Example:
+
+ [ { path: 'README.md.wcrypt',
+ hash: 'QmcrMFv4f4yef5EpdM9mUTGiE4msi2VKLmTPbGenRaiKLd',
+ size: 20871 } ]
+
+### Add directory
+
+```javascript
+ipfsecret.add('./test', 'justtesting')
+ .then(results => {
+ console.log(results);
+ })
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+Example:
+
+ [ { path: 'ipfsecret/test/add-indexed.js.wcrypt',
+ hash: 'QmPm5MGHooEiDuiJjDg5dFRvaNgyTNYuM7Lx4gqj5k77cB',
+ size: 8013 },
+ { path: 'ipfsecret/test/add.js.wcrypt',
+ hash: 'Qmcdj2aRshwSkVnEUSAANcHEmbyjb1tnt9Cg5Ubh2bxvav',
+ size: 6900 },
+ { path: 'ipfsecret/test/get.js.wcrypt',
+ hash: 'QmXYeFbCBL8vgYKsvy4K98kYNvqDHMXVeEogR6zQzdedg7',
+ size: 5322 }...
+
+0 byte files are skipped during encryption. Symbolic links encountered are resolved and encrypted as separate files.
+
+### Other options
+
+If the caller passes in an Object instead of ```passphrase```, ```ipfsecret.add``` recognizes the following key:value pairs:
+
+```javascript
+ const options = {
+ directory: 'mydirname', // Use this wrapping dir, default 'ipfsecret'
+ hidden: false, // Include hidden files. Default true
+ passphrase: 'justtesting', // Use value to encrypt, always required
+ root: true, // Return just multihash. Default false
+ suffix: 'myfilesuffix', // Use this suffix, default 'wcrypt'
+ wcrypt: {config:{crypto:tagLength:112}} // Pass options to webcrypto-crypt (see below)
+ };
+```
+
+#### Root multihash only
+
+The multihash is returned as a [Buffer](https://nodejs.org/api/buffer.html):
+
+```javascript
+const bs58 = require('bs58');
+
+ipfsecret.add('./test', {passphrase:'justtesting', root:true})
+ .then(hash => {
+ console.log(bs58.encode(hash));
+ })
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+Example:
+
+ Qmf22oxRTsz6CPWf2xDZeF729xdBx7ULDQsdtqpXYh8UKV
+
+#### Custom file suffix
+
+```javascript
+ipfsecret.add('./test', {passphrase:'justtesting', suffix: 'shazam'})
+ .then(results => {
+ console.log(results);
+ })
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+Example:
+
+ [ { path: 'ipfsecret/test/add-indexed.js.shazam',
+ hash: 'QmY4EWxfvBVAUVzpbB7wXhktWLFwE4xunx1jiFbpiNWiKM',
+ size: 8013 },
+ { path: 'ipfsecret/test/add.js.shazam',
+ hash: 'QmRGKq3VHsuX4kyerYE1atNBC7wCu2gRpmxXqocKdXTQsT',
+ size: 6900 },
+ { path: 'ipfsecret/test/get.js.shazam',
+ hash: 'QmRU2qKJBScmedhcDgGGPCvtpWVnasQEJdkF9UvVEM9L7N',
+ size: 5322 },...
+
+#### Custom wrapping dir
+
+```javascript
+ipfsecret.add('./test', {passphrase:'justtesting', directory:'shazam'})
+ .then(results => {
+ console.log(results);
+ })
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+Example:
+
+ [ { path: 'shazam/test/add-indexed.js.wcrypt',
+ hash: 'QmTWSp9uyPG2DuxFuw3XBhuqJd7cq4zemz4iJqxMw6693r',
+ size: 8013 },
+ { path: 'shazam/test/add.js.wcrypt',
+ hash: 'QmXC348Z6PZGUCSz4tMGaRuhcQ7cgiAxhD8h4gbqhCoAEn',
+ size: 6900 },
+ { path: 'shazam/test/get.js.wcrypt',
+ hash: 'QmcU8eu2fWFfUsM1SkKq8zSrmDa9hWB6hze6yVzNtDKE2G',
+ size: 5322 }...
+
+#### Custom cryptography settings
+
+To pass custom settings to [webcrypto-crypt](https://c2fo-lab.github.io/webcrypto-crypt), use the ```wcrypt``` options attribute:
+
+```javascript
+ipfsecret.add('./test', {
+ passphrase:'justtesting',
+ root: true,
+ wcrypt: {
+ config: {
+ crypto: {
+ tagLength: 112
+ },
+ derive: {
+ hash: 'SHA-256',
+ length: 192,
+ iterations: 5000
+ },
+ paranoid: true
+ }
+ }
+})
+ .then(hash => {
+ console.log(hash);
+ })
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+ QmesGPZmiAgGhQSvEF7m7AdotmMygaPsJHvUH9STRjttwB
+ λ head -n1 QmesGPZmiAgGhQSvEF7m7AdotmMygaPsJHvUH9STRjttwB/test/add-index.js.wcrypt
+ WCRYPT00.01.125000112192SHA-256....
+
+## ipfsecret.get(hash, passphrase)
+
+Wraps [ipfs.files.get](https://github.com/ipfs/interface-ipfs-core/tree/master/API/files) to require a multihash for retrieval and a ```passphrase``` for decryption. If ```passphrase``` is accepted, returns a promise that resolves to an object stream of decrypted file objects.
+
+Any files encountered during an ```ipfsecret.get``` operation that could not be decrypted will not be retrieved.
+
+Consider the following IPFS directory, encrypted with the passphrase, "学年別漢字配当表":
+
+ λ ipfs ls QmZdNHJpzvTYqS3ba6EW4tryGaaEjHYyqY4ji6jZGK6Les/assets/fonts
+ QmX6KnVkx6L3uXsLcTfi5d8SUwc4F1DLTEMeYgeRWYscoc 34016 roboto-v16-latin-300.woff.wcrypt
+ QmYWKumL3aj54gRyG11dLRKp9eon9kCZzDM4MDMdzg7nQg 26475 roboto-v16-latin-300.woff2.wcrypt
+ QmXAVPjgS2FiCpGXz7pA5Prh5WjHJtan1H1L7PSNNZLMCz 37008 roboto-v16-latin-300italic.woff.wcrypt
+ QmaX7JAakBrgw5GAaMSuqxGyyntQsmxHc3jHLDDuuMsxSt 29439 roboto-v16-latin-300italic.woff2.wcrypt
+ QmPPoCqdCCxNEqyPJmsEqasDUes9uP49UAzyJ6S4JVEnJb 34001 roboto-v16-latin-700.woff.wcrypt
+ QmeFr49dWjcRJm9LM6vzfVEHZaZB8JwbgA5fXkq68vL4gn 26494 roboto-v16-latin-700.woff2.wcrypt
+ QmSsuY2teXgFRk5dXx4P2CBe4VG4QmmjJsAXVMRofpKmvP 35974 roboto-v16-latin-700italic.woff.wcrypt
+ Qmb3LegLP2kNLmvjUPsWkt82mVPX9BVGCbgTajj5KidrRG 28344 roboto-v16-latin-700italic.woff2.wcrypt
+ QmU5ehUCXbbMcoEDHgdp55T51kqi1X1Qjh3VeeZAz94xdN 33648 roboto-v16-latin-regular.woff.wcrypt
+ QmUWp3DJNujDFXvQaxdR8qm3ueYnGd6tnnsHuVP4qmiQVL 26516 roboto-v16-latin-regular.woff2.wcrypt
+ λ
+
+```javascript
+const fs = require('fs'),
+ path = require('path'),
+ hash = 'QmZdNHJpzvTYqS3ba6EW4tryGaaEjHYyqY4ji6jZGK6Les';
+
+ipfsecret.get(hash, '学年別漢字配当表')
+ .then((stream) => {stream.on('data', (obj) => {
+ if (obj.content) {
+ var filename = path.basename(obj.path),
+ writeable = fs.createWriteStream(filename);
+ obj.content.pipe(writeable);
+ obj.content.on('finish', () => {
+ console.log('Wrote ' + filename);
+ });
+ obj.content.on('error', (err) => {
+ console.error('Error: ' + err);
+ });
+ }
+ });})
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+Example:
+
+ Wrote roboto-v16-latin-300.woff
+ Wrote roboto-v16-latin-300.woff2
+ Wrote roboto-v16-latin-300italic.woff
+ Wrote roboto-v16-latin-300italic.woff2
+ Wrote roboto-v16-latin-700.woff
+ Wrote roboto-v16-latin-700.woff2
+ Wrote roboto-v16-latin-700italic.woff
+ Wrote roboto-v16-latin-700italic.woff2
+ Wrote roboto-v16-latin-regular.woff
+ Wrote roboto-v16-latin-regular.woff2
+ λ ls roboto-v16-latin-*
+ roboto-v16-latin-300.woff roboto-v16-latin-300italic.woff roboto-v16-latin-700.woff roboto-v16-latin-700italic.woff roboto-v16-latin-regular.woff
+ roboto-v16-latin-300.woff2 roboto-v16-latin-300italic.woff2 roboto-v16-latin-700.woff2 roboto-v16-latin-700italic.woff2 roboto-v16-latin-regular.woff2
+ λ file roboto-v16-latin-300.woff
+ roboto-v16-latin-300.woff: Web Open Font Format, flavor 65536, length 18972, version 1.1
+
+### Other options
+
+If the caller passes in an Object instead of a passphrase, ```ipfsecret.get``` recognizes the following key:value pairs:
+
+```javascript
+const options = {
+ passphrase: 'justtesting', // Use value to encrypt data, always required
+ wcrypt: {config:{crypto:tagLength:112}} // Pass options to cryptography package
+};
+```
+
+## ipfsecret.addIndexed(path, passphrase)
+
+Otherwise identical to ```ipfsecret.add```, ```ipfsecret.addIndexed``` adds HTML, JavaScript, CSS, and fonts that together provide a browser interface for listing and decrypting ipfsecret-encrypted files. This is an alias for ```ipfsecret.add(path, {passphrase: p, indexed: true}```.
+
+### Add directory and include browser interface files
+
+```javascript
+ipfsecret.addIndexed('./test', 'justtesting')
+ .then(results => {
+ console.log(results);
+ })
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+Example:
+
+ [ { path: 'ipfsecret/ipfsecret/styles/normalize.css',
+ hash: 'QmbfTL2ZsgZzhH5Cz6pcnChUkrDsozHnjnqSfYAbCex7W2',
+ size: 7730 },
+ { path: 'ipfsecret/ipfsecret/styles/fonts.css',
+ hash: 'Qmc8XfJ4w1LjV6uK4q2ENzREdTELLpnGWdoKfnjkXHH1Vm',
+ size: 1992 },
+ { path: 'ipfsecret/ipfsecret/styles/milligram.css',
+ hash: 'QmaFP6KqC1LrkBadFcxgqSKnxqhjByiALpjSeAoYtzMDth',
+ size: 8729 },
+ { path: 'ipfsecret/ipfsecret/js/ipfsecret.js',
+ hash: 'QmTWa8Hx5G5M72xb3A3bbdmkvvpSM8UZ5ct8oqLZSgRfYn',
+ size: 76326 },
+ { path: 'ipfsecret/ipfsecret/fonts/roboto-v16-latin-300.woff',
+ hash: 'QmVeWJjZaoXLitapfpDHVWxm7BwqqiT6FfeVdDHxMCkCXn',
+ size: 18986 },
+ { path: 'ipfsecret/ipfsecret/fonts/roboto-v16-latin-300.woff2',
+ hash: 'QmNuucbg8ojSkDvN2x4HJBjiLqN9vdcKz5VPee6RroASgX',
+ size: 14707 },
+ ...
+
+### Other options
+
+#### Content overrides
+
+In addition to all the options supported by ```ipfsecret.add```, you may override ```ipfsecret.addIndexed```'s CSS, font directory, and JavaScript build by passing in overriding options:
+
+```javascript
+ipfsecret.addIndexed('./test', {
+ baseCss: './myBase.css',
+ dialogCss: './myDialog.css',
+ fontCss: './myFonts.css',
+ mainCss: './myStyles.css',
+ mapCss: './myStyles.css.map',
+ fontsDir: './myFontsDirectory',
+ jsPath: './myJavaScriptBuild.js',
+ passphrase: 'justtesting',
+})
+ .then(results => {
+ console.log(results);
+ })
+ .catch(err => {
+ console.error(err);
+ });
+```
+
+## ipfsecret.getGatewayList()
+
+Return an array of currently known HTTP/S gateways; used by the command-line utility.
+
+```javascript
+console.log(ipfsecret.getGatewayList());
+```
+
+ [ 'https://gateway.ipfs.io',
+ 'https://earth.i.ipfs.io',
+ 'https://mercury.i.ipfs.io',
+ 'https://gateway.ipfsstore.it:8443',
+ 'https://scrappy.i.ipfs.io',
+ 'https://chappy.i.ipfs.io' ]
+
+## IPFSecret.DEBUG = true | false
+
+If set to ```true```, send debugging statements to stderr. Default ```false```.
+
+## IPFSecret.version
+
+Returns the current version of this library, e.g., ```0.1.2```.
+
+## Custom API endpoint
+
+When instantiating a new IPFSecret object, you may optionally pass in ```options``` to override the default IPFS connection parameters:
+
+```javascript
+ const ipfsecret = new IPFSecret({
+ host: '192.168.1.233', // default 'localhost'
+ port: 5002, // default 5001
+ proto: 'http' // default 'http'
+ });
+```
+
+# Command-line web gateways
+
+## List known web gateways
+
+ λ ipfsecret list
+ 0 - https://gateway.ipfs.io
+ 1 - https://earth.i.ipfs.io
+ 2 - https://mercury.i.ipfs.io
+ 3 - https://gateway.ipfsstore.it:8443
+ 4 - https://scrappy.i.ipfs.io
+ 5 - https://chappy.i.ipfs.io
+
+## Specify a web gateway (instead of localhost) when outputting result
+
+ λ ipfsecret add -g3 -wr node_modules/webcrypto-crypt/
+ Passphrase?
+ Confirm passphrase:
+ https://gateway.ipfsstore.it:8443/ipfs/QmWqwUpf4jBdTb8BRxaLUvPQj3JMUtMQe1Kxo2sm3noHE7/ipfsecret.html
+
+## Specify a custom gateway when outputting hash
+
+ λ ipfsecret add -g "https://mygateway.io:8088" -wr node_modules/webcrypto-crypt/
+ Passphrase?
+ Confirm passphrase:
+ https://mygateway.io:8088/ipfs/QmYYoHfaAcvkRQy6bubTChkXPi2c9d1r6vBatWeRXMr7p5/ipfsecret.htmlλ
+
+# Implementation
+
+## Cryptography
+
+IPFSecret's encryption depends on WebCrypto's [SubtleCrypto](https://www.w3.org/TR/WebCryptoAPI/#subtlecrypto-interface) interface, specifically the algorithms [PBKDF2](https://www.w3.org/TR/WebCryptoAPI/#pbkdf2) and [AES-GCM](https://www.w3.org/TR/WebCryptoAPI/#aes-gcm). It then marshals the encrypted chunks into IPFS objects and adds these to IPFS as new files. Decryption is carried out via the same SubtleCrypto interface, with the caller supplying the correct passphrase along with the relevant multihash to decrypt the files as they are retrieved. See [webcrypto-crypt](https://c2fo-lab.github.io/webcrypto-crypt) for more information.
+
+Decrypting IPFS files in the web browser is currently supported but encrypting in the browser is not supported.
+
+## Web browsers
+
+### HTML page
+
+
+
+
+
+By default, IPFSecret publishes the index using the filename ```ipfsecret.html```. This can be overridden to specify any filename but be aware ```index.html``` [has special meaning](https://github.com/ipfs/ipfs/issues/167#issuecomment-329969289) for IPFS gateways.
+
+### Markup and styling
+
+When calling ```addIndexed()``` from JavaScript or specifying the ```--web``` argument to the ```ipfsecret``` command-line utility, IPFSecret will generate unencrypted HTML pages that contain a directory listing and buttons to decrypt any file by providing the passphrase. The HTML is currently styled using [Milligram](http://milligram.io/) but as noted in the [Content overrides](#content-overrides) section, you may also specify your own CSS and accompanying fonts.
+
+### Decryption
+
+IPFSecret currently accesses encrypted IPFS files from the web browser as [Blobs](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and decrypts them via [webcrypto-crypt](https://c2fo-lab.github.io/webcrypto-crypt).
+
+### Decrypted file size limits
+
+[Filesaver.js documentation](https://github.com/eligrey/FileSaver.js/#supported-browsers) shows the download size limits of the various browsers. If IPFSecret detects that an encrypted file is larger than the current browser's limit, instead of decrypting it will display a message suggesting other options. Encrypted files can always be downloaded from the gateway and later decrypted using Node.js or the command-line.
+
+### Encrypted file modified times
+
+The ```Date modified``` field populated in the web interface reflects the modified time of the original, unencrypted file, versus the time that the encrypted version was created.
+
+### Long download times & decryption times
+
+IPFSecret currently relies on the browser's [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) to access files as blobs, and there is currently no support in the API for showing download progress. In the case of accessing large files over slow IPFS web gateways, you may end up waiting a good while for the encrypted file to download before decryption can begin, without knowing how much longer the download will take to complete. Decryption in the browser can also be slow, particularly with blobs approaching 500MB or more. There is currently a [Todo](#todo) item to migrate these operations to the [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) which may improve performance in future releases.
+
+### Offline mode
+
+IPFSecret web pages try to detect whether or not the current browser is connected to the internet and, if not connected, attempt to redirect to the local IPFS HTTP gateway. Decryption should work in the browser whether or not the user is connected to the internet, provided the multihashes in question are locally accessible.
+
+# Passphrase file
+
+See [.wcryptpass](https://c2fo-lab.github.io/webcrypto-crypt#wcryptpass)
+
+# Security
+
+Security Mail: labs@c2fo.com
+PGP key fingerprint: [````E838 B51C C63F 7ED6 0980 9535 4D46 5218 A674 6F81````](https://pgp.mit.edu/pks/lookup?search=0xE838B51CC63F7ED6098095354D465218A6746F81)
+Keyserver: [pgp.mit.edu](https://pgp.mit.edu)
+
+# Troubleshooting
+
+IPFSecret is not fast. For large directories and files, you may just need to wait it out...
+
+ λ osxinfo
+ User : alfonz
+ Time: : Sun Nov 12 13:54:49 2017
+ Model : MacBookPro10,1
+ Processor : Intel(R) Core(TM) i7-3615QM CPU @ 2.30GHz
+ OS : Darwin
+ Release : 16.7.0
+ Disk : 80.10% of 249.78 GB
+ Memory : 10254 MB of 17180 MB
+ Shell : /bin/bash
+ Terminal : screen
+ Memory : 10254 MB of 17180 MB
+ Graphics : Intel HD Graphics 4000 @ 1536 MB
+ Graphics : NVIDIA GeForce GT 650M @ 1024 MB
+ Packages : no packages found
+ Uptime : 1 day 04:37
+
+ λ ipfs --version
+ ipfs version 0.4.11
+
+ λ ipfs init
+ ...
+
+ λ ipfs daemon --offline &
+ [1] 12178
+ λ Initializing daemon...
+ Swarm not listening, running in offline mode.
+ API server listening on /ip4/127.0.0.1/tcp/5001
+ Gateway (readonly) server listening on /ip4/127.0.0.1/tcp/8080
+ Daemon is ready
+
+ λ git log | head -n4 && du -sh
+ commit bebc6082da0a9f5d47a1ea2edc099bf671058bd4
+ Author: Linus Torvalds
+ Date: Sun Nov 12 10:46:13 2017 -0800
+
+ 2.9G .
+
+ λ time ipfsecret add -wrH .
+ Passphrase?
+ Confirm passphrase:
+ http://127.0.0.1:8080/ipfs/QmdScWuXY97CDsqUpxCgF6RjtYnu8qzf66EaWpTdZNfvdN/ipfsecret.html
+ real 4m32.701s
+ user 3m53.192s
+ sys 0m20.093s
+ λ
+
+ λ cd ..
+ λ time ipfsecret -o decrypted-linux get QmdScWuXY97CDsqUpxCgF6RjtYnu8qzf66EaWpTdZNfvdN
+ Passphrase?
+
+ real 3m27.587s
+ user 7m20.771s
+ sys 0m29.577s
+
+ λ du -sh decrypted-linux
+ 2.9G decrypted-linux
+
+ λ diff -Nur ./decrypted-linux ./linux
+ λ # but some 0 byte files and empty dirs will cause differences (diff -r)
+
+ λ ls -lh ubuntu.vdi
+ -rw------- 1 alfonz staff 2.7G Jul 27 19:06 ubuntu.vdi
+
+ λ time ipfsecret add ubuntu.vdi
+ Passphrase?
+ Confirm passphrase:
+ Qmd4tq3Q7PVzNcfDojdx7koRsEfVMnitvAFSR4EDhirUUj
+ real 0m47.569s
+ user 0m27.784s
+ sys 0m6.920s
+
+ λ time ipfsecret get -o ubuntu.vdi.decrypted Qmd4tq3Q7PVzNcfDojdx7koRsEfVMnitvAFSR4EDhirUUj
+ Passphrase?
+
+ real 2m10.282s
+ user 2m4.236s
+ sys 0m10.424s
+
+ λ diff ubuntu.vdi.decrypted ubuntu.vdi
+ λ
+
+## Timeouts
+
+You may encounter timeouts after connecting to the API server. In these cases you can try adding the content again:
+
+ λ ipfsecret add -wr linux/
+ Passphrase?
+ Confirm passphrase:
+ { Error: connect ETIMEDOUT 127.0.0.1:5001
+ at Object._errnoException (util.js:1041:11)
+ at _exceptionWithHostPort (util.js:1064:20)
+ at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1153:14)
+ code: 'ETIMEDOUT',
+ errno: 'ETIMEDOUT',
+ syscall: 'connect',
+ address: '127.0.0.1',
+ port: 5001 }
+ λ ipfsecret add -wr linux/
+ Passphrase?
+ Confirm passphrase:
+ http://localhost:8080/ipfs/Qma7w6RchCgv7E8yqeqeU9viPE97RGMuih8yvE2NL4555q/ipfsecret.htmlλ
+
+## File signature warnings
+
+When decrypting directories that contain the web user interface, the unencrypted web interface files may trigger this warning:
+
+ Error: Error: Invalid file signature webcrypto-crypt0.1.15
+
+## Invalid index entries
+
+Directories that contained no files or contained only 0-byte files will currently result in invalid links in the HTML indices.
+
+## Out of memory
+
+If you hand ```ipfsecret add``` a very large directory, you may encounter a fatal error like:
+
+ FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
+
+In these cases you might consider writing several directories as separate operations.
+
+# Acknowledgements
+
+* Decryption via WebCrypto in the browser uses a modified version of the [binary-split](https://github.com/maxogden/binary-split) function.
+* Modal box implementation from [modalbox](https://github.com/CristianDeveloper/modalbox).
+
+# Todo
+
+* Add automated tests for browser-based decryption.
+* Fix UI navigation when user specifies ```.``` or ```../../../```, etc .
+* Start ```ipfs --daemon``` if it's not already running for duration of tests.
+* Support embed and decrypt Base64 and hexadecimal encoded data in HTML files.
+* Support streaming decryption through the forthcoming [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API).
+* Rewrite install.sh in node for portability
+* Test on Win and Linux.
+* Support [progress bar on the command line](https://github.com/ipfs/js-ipfs/pull/1036).
+
+# See also
+
+* [IPFS](https://ipfs.io/)
+* [webcrypto-crypt](https://c2fo-lab.github.io/webcrypto-crypt)
diff --git a/SIGNED.txt b/SIGNED.txt
new file mode 100644
index 0000000..f39d9c8
--- /dev/null
+++ b/SIGNED.txt
@@ -0,0 +1,46 @@
+-----BEGIN PGP SIGNED MESSAGE-----
+Hash: SHA512
+
+assets/css/dialog.css,4.0K,821334ef1c259e0b2539aa9714873f7dee017b91a8e14c98d29fd7b3976af8e5
+assets/css/fonts.css,4.0K,312db64124154f579839d22282c4b0a51aa7d76ff98f872c80aa91633ec07805
+assets/js/ipfsecret-decrypt.js,12K,2ee2604b4421962246296865716b67e56e9466404001ad113548f0723491f466
+bin/browserify.js,4.0K,9495034fc074de5349f143d1d83e281db37aa7248a46cda77df92aa4f04d37ce
+bin/get-fonts.js,4.0K,8f72986813ad44102ef243d6e32d81306bbc17ef96c7d6c40757b001a2076b70
+bin/install.sh,4.0K,19381a15ade1693b201dcd94862d909c6744ffb61f16299204e4892c22174b84
+bin/ipfsecret.js,16K,314fed67f48ce583dd416f3d8c7ed6ca2d4d2dac8ab9dc7c4f62403cdebeb6c6
+CHANGES.md,4.0K,f59d6ddd25386d72f42c963926fdb29543ca3765ef4e70e8b5343a2401529699
+index.js,4.0K,afde30d82fbcd36601fae1364a9cb898ca01751c812047314f194f895264c5eb
+lib/config.json,4.0K,3f08d9ae0f9469f6aa11b875af7f6187a7454a9b893ad2cf3c52fc2af4b6dc84
+lib/crypto/encrypt.js,8.0K,59e6f3b585e1d3e82b59fe9ed7fae641e1fb90a3de9f15bf103ac8ba4aa4db66
+lib/crypto/index.js,4.0K,eb8ca9af7eb250f8c63a9a72eae20af5b01a03a92e49718a58f827bf2b40ea8d
+lib/indexer/html.js,12K,bd452f1ab2f901b311f19686bd34202cfca9b74f02bb8d22f49bf99c40ef13f0
+lib/indexer/index.js,8.0K,358a8f1a968ba04b4cbf3d56d532febd82f03445e8920191ffb684874d28c482
+lib/indexer/static/css.js,4.0K,9d941c5475d6117dffcba5161e03d0655fd1f76d6a6ebf67b2d0ea2e3ea88e26
+lib/indexer/static/fonts.js,4.0K,abc6825e203dc046ceb6d5c6cfe37c59affc37a573a683e4303d8aff952a9791
+lib/indexer/static/index.js,4.0K,1edb54ee440d7d5599670774a8960fd979ab9aa91a661630c8e41eaad3ab838f
+lib/indexer/static/js.js,4.0K,040ce7e679921bbd34c45cc3a6ee8395cffeb480e2b1cef4619095889aa45f66
+lib/util/hashes.js,4.0K,bc69991c6ad7dc3df4b62174430e240a23bc33fdc4337fef7658ad27aecc53ca
+lib/util/index.js,4.0K,e467f524bf11e541533d3e4b9080f8e368e17e4d916e97b9e2fe70718f964bcd
+lib/util/options.js,4.0K,2cee630d705b258747a9bfeee33c446eae9a306e7ec1922f439e72eadd2d550f
+lib/util/paths.js,4.0K,f3f07d47290e1fb20b24c1c583ba37df8d0dc39b7820a168a5e51ac559234895
+LICENSE,4.0K,a6f5c7d3a2606ed237064349fd7fe8fff0646f71512ef8e6842cd5e1923976c0
+package-lock.json,200K,108bd55d237d85497ba07dc469c191b3b08f2117b7111ebd8f4846f79d7d49a6
+package.json,4.0K,494cf44f1c6d6ee856a2950a4bc9f13ecf6fabbff2ccd8028cf4347ffb51cb32
+README.md,28K,b1e0b1c4d86b2cc5e56a2c16428dfde9116cf0facd93d1f874d663c4ff592863
+test/.wcryptpass,4.0K,a8f0fb0fd1a98f68b8442c9bd8b019a0e0ca81c579255bcf2c2bac76673733f9
+test/add-index.js,16K,b8d58c8c757778596d5834004c2cb9a57c82c89086f3b9d14a4c15b88870653d
+test/add.js,8.0K,fc254bde104d358c4103d7e49a096b46d495f62773e3cb6431c06a732c5bc7e1
+test/get.js,8.0K,9db651c795b649d60cd62b73a4369a94ed28ef2c5cd43d39dd397875836a5de8
+test/misc.js,4.0K,bddab3f4415b26521eaf6200915ae26be1db978115f5ebfc4b7991aa27034175
+-----BEGIN PGP SIGNATURE-----
+Version: Keybase OpenPGP v2.0.76
+Comment: https://keybase.io/crypto
+
+wsBcBAABCgAGBQJaCgCsAAoJEFXbw7aEigbJXloH/iCbTFa9HWgBeOR5DJUf55fs
+kmdRl7889FodPuC9H9D+18G8Sp1Mfnobb9FBcCHicy1RCEV3FnhWNT8lvG8kGbcT
+Z3+3KqJ+SBY6r9eSWDKlQMDLm4mKuKXVA3RkqMKgmssN1qF2jhBHXIKrTog2D8W5
+ktjNkNuLlpVz32o71zdaClkZCcMzuam9U1SMp7B49nk/MjvD0lj9tJEFo+ZR8JG9
+RUz57HPX4ojnYXoRrGr3h+SYatKi6GfmsmOd9BB8duyOn2sV1OiE5UWPrHN9bF2n
+/sJTz1fQ2EkblmWkkPF86wLNcBmc2oawEqULHfLQNu8Gq6nwRzjpHIieQ/1+9Qs=
+=gnOX
+-----END PGP SIGNATURE-----
diff --git a/assets/css/dialog.css b/assets/css/dialog.css
new file mode 100644
index 0000000..d5df76e
--- /dev/null
+++ b/assets/css/dialog.css
@@ -0,0 +1,55 @@
+/**
+ * Modal box Css
+ * @Version 1.0
+ * @Author: Cristian Marian
+ * @E-mail: dev.cristian99@gmail.com
+ */
+.modal-sec-overlay {
+ display: none;
+ position: fixed;
+ z-index: 1;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0,0,0,0.1);
+}
+.modal-box {
+ background-color: #fefefe;
+ margin: 15% auto;
+ padding: 20px;
+}
+.modal-small {width: 375px;}
+.close-btn {
+ background-color: #FFF;
+ color: #aaa;
+ float: right;
+ font-size: 15px;
+ font-weight: bold;
+ border: none;
+}
+.close-btn:hover,
+.close-btn:focus {
+ outline: none;
+ color: black;
+ text-decoration: none;
+ cursor: pointer;
+}
+.modal-title {
+ font-size: 18px;
+ padding: 5px;
+ font-family: sans-serif;
+ border-bottom: 1px solid #CCC;
+}
+.modal-content {
+ padding: 5px;
+ font-size: 12px;
+ font-family: sans-serif;
+}
+
+/* Modal Animation */
+@keyframes open-modal-animation{
+ from { margin: -15% auto; }
+ to { margin: 15% auto; }
+}
diff --git a/assets/css/fonts.css b/assets/css/fonts.css
new file mode 100644
index 0000000..53cc8bb
--- /dev/null
+++ b/assets/css/fonts.css
@@ -0,0 +1,45 @@
+/* roboto-300 - latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 300;
+ src: local('Roboto Light'), local('Roboto-Light'),
+ url('../fonts/roboto-v18-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url('../fonts/roboto-v18-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* roboto-300italic - latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: italic;
+ font-weight: 300;
+ src: local('Roboto Light Italic'), local('Roboto-LightItalic'),
+ url('../fonts/roboto-v18-latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url('../fonts/roboto-v18-latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* roboto-regular - latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Roboto'), local('Roboto-Regular'),
+ url('../fonts/roboto-v18-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url('../fonts/roboto-v18-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* roboto-700 - latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Roboto Bold'), local('Roboto-Bold'),
+ url('../fonts/roboto-v18-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url('../fonts/roboto-v18-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
+/* roboto-700italic - latin */
+@font-face {
+ font-family: 'Roboto';
+ font-style: italic;
+ font-weight: 700;
+ src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'),
+ url('../fonts/roboto-v18-latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
+ url('../fonts/roboto-v18-latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
+}
diff --git a/assets/js/ipfsecret-decrypt.js b/assets/js/ipfsecret-decrypt.js
new file mode 100644
index 0000000..5f06dc5
--- /dev/null
+++ b/assets/js/ipfsecret-decrypt.js
@@ -0,0 +1,209 @@
+const BlobToBuffer = require('blob-to-buffer'),
+ { detect } = require('detect-browser'),
+ browser = detect(),
+ Config = require('../../lib/config.json'),
+ isOnline = require('is-online'),
+ toArrayBuffer = require('to-arraybuffer'),
+ FileSaver = require('file-saver'),
+ Path = require('path'),
+ Wcrypt = require('webcrypto-crypt'),
+ matcher = Buffer.from(Wcrypt.delimiter);
+
+document.addEventListener("DOMContentLoaded", function(event) {
+
+ const blobSupport = {
+ firefox: {minVer: 20, maxSize: 800*1024*1024},
+ chrome: {minvVer: 1, maxSize: 500*1024*1024},
+ safari: {minVer: 10.1, maxSize: 100*1000*1024*1024},
+ opera: {minVer: 15, maxSize: 500*1024*1024}
+ };
+
+ const byId = document.getElementById.bind(document),
+ close = byId('close-modal'),
+ content = byId('modal-box'),
+ duration = '0.5s',
+ open = byId('open-modal'),
+ p = (location.pathname).split('/'),
+ prefix = p.slice(0, p.length - 1).join('/') + '/',
+ hash = p[2],
+ opener = "open-modal-animation",
+ modal = byId('mymodal');
+
+ close.onclick = () => {modal.style.display = 'none';}
+ window.onclick = (e) => {if (e.target==modal) modal.style.display="none";};
+
+ function checkBlob(size) {
+ let broken = false;
+ switch (browser && browser.name) {
+ case 'chrome':
+ case 'firefox':
+ case 'opera':
+ case 'safari':
+ if (
+ // unsupported browser version
+ (parseFloat(browser.version)
+ <
+ parseFloat(blobSupport[browser.name].minVer))
+ ||
+ // unsupported blob size for this browser
+ (size > parseInt(blobSupport[browser.name].maxSize))
+ )
+ {
+ broken = true;
+ alert(Config.ipfs.ui.browserDecryptNotSupported);
+ }
+ break;
+ default:
+ alert(browser.name + ' not supported');
+ };
+ return broken;
+ }
+
+ function checkOnlineStatus() {
+ isOnline().then(online => {
+ if (!online && ((location.hostname !== 'localhost') &&
+ (location.hostname !== '127.0.0.1'))) {
+ alert(Config.ipfs.ui.offlineMode);
+ location.href = `${ Config.ipfs.proto }://` +
+ `${ Config.ipfs.host }:${ Config.ipfs.gatewayPort }` +
+ `${ location.pathname }`;
+ }
+ });
+ }
+ checkOnlineStatus();
+ setInterval(checkOnlineStatus, parseInt(Config.ipfs.ui
+ .offlineMonitorInterval));
+
+ //stackoverflow.com/a/18650828
+ function format(bytes, decimals) {
+ if(bytes === 0) return '0 B';
+ if (!bytes) return '';
+ const k = 1024,
+ dm = (decimals || 2) ,
+ sizes = ['B','KB','MB','GB','TB','PB','EB','ZB','YB'],
+ i = Math.floor(Math.log(bytes) / Math.log(k)),
+ re = /^(\d+)\.(\d)$/,
+ val = parseFloat((bytes/Math.pow(k, i)).toFixed(dm));
+ let text = val.toString();
+ if (text.match(re)) text = text + '0';
+ return text + ' ' + sizes[i];
+ }
+
+ function download() {
+ const filename = decodeURI(byId('filename').getAttribute('value')),
+ size = parseInt(byId('filesize').value),
+ done = format(size, 2),
+ f = Path.basename(filename).replace(/\.[^\.]+$/,'');
+
+ let broken = checkBlob(size);
+ if (!broken) {
+ fetch(filename).then(function (res) {
+ byId('progress').innerHTML = Config.ipfs.ui.downloading + '...';
+ res.blob()
+ .then(blob => {handleBlob(blob, size, done, f);})
+ .catch(err => {alert(err);});
+ });
+ }
+ }
+
+ function first(buf, offset) {
+ if (offset >= buf.length) return -1;
+ for (var i = offset; i < buf.length; i++) {
+ if (buf[i] === matcher[0]) {
+ let full = true;
+ for (var j = i, k = 0; j < i + matcher.length; j++, k++) {
+ if (buf[j] !== matcher[k]) {
+ full = false;
+ break;
+ }
+ }
+ if (full) return j - matcher.length;
+ }
+ }
+ let idx = i + matcher.length - 1;
+ return idx;
+ }
+
+ function handleBlob(blob, size, done, f) {
+ let buffered, byteCount = 0, file, needHeader = true, wcrypt;
+ BlobToBuffer(blob, (err, buf) => {
+ if (err) console.error(err);
+ let last = 0,
+ offset = 0;
+ byId('form-fields').setAttribute('style', 'display:none')
+ while (true) {
+ let idx = first(buf, offset - matcher.length + 1);
+ if (idx !== -1 && idx < buf.length) {
+ if (needHeader) {
+ needHeader = false;
+ wcrypt = readHeader(buf, last, idx);
+ }
+ else {
+ wcrypt.rawDecrypt(buf.slice(last, idx))
+ .then(data => {
+ setTimeout(() => {
+ byteCount = byteCount + data.length;
+ if (!file) file = Buffer.from(data);
+ else file = Buffer.concat([file, data]);
+ let percent = `${ ((byteCount / size) * 100).toFixed(2) }%`;
+ let complete = `(${ format(byteCount, 2) } of ${ done })`;
+ byId('progress').innerHTML = Config.ipfs.ui.decrypting +
+ ` ${ percent }
${ complete }`;
+ if (byteCount === size) {
+ const ab = toArrayBuffer(file);
+ const b = new Blob([new Uint8Array(ab)]);
+ FileSaver.saveAs(b, f);
+ modal.style.display = 'none';
+ }
+ }, Config.ipfs.ui.decryptUpdateDelay);
+ }).catch((err) => {console.error(err);});
+ }
+ offset = idx + matcher.length;
+ last = offset;
+ }
+ else {
+ buffered = buf.slice(last);
+ break;
+ }
+ }
+ });
+ }
+
+ function handleClicks(e) {
+ let target = e.target || e.srcElement;
+ if (target.getAttribute('value') == Config.ipfs.ui.decrypt)
+ showModal(target);
+ else if (target.getAttribute('value') == Config.ipfs.ui.download)
+ download();
+ }
+
+ function readHeader(buf, last, idx) {
+ try {
+ let passphrase = byId('passphrase').value;
+ byId('passphrase').value = null;
+ let data = Wcrypt.parseHeader(buf.slice(last, idx));
+ data.material.passphrase = passphrase;
+ return new Wcrypt.cipher(data);
+ }
+ catch (err) {console.error(err);}
+ }
+
+ function showModal(target) {
+ let filename = prefix + target.parentNode.parentNode
+ .querySelector('td:nth-child(1) > a').getAttribute('href'),
+ filesize = target.parentNode.parentNode
+ .querySelector('td:nth-child(2)').getAttribute('bytes'),
+ decrypted = decodeURI(Path.basename(filename)
+ .replace(/\.[^\.]+$/,''));
+ byId('form-fields').setAttribute('style', 'display:block')
+ byId('progress').innerHTML = '';
+ byId('modal-title').innerHTML = decrypted;
+ byId('filename').setAttribute('value', filename);
+ byId('filesize').setAttribute('value', filesize);
+ modal.style.display = 'block';
+ content.style.animation = opener;
+ }
+
+ document.body.addEventListener('click', handleClicks,false);
+
+});
diff --git a/bin/browserify.js b/bin/browserify.js
new file mode 100755
index 0000000..0cba3c9
--- /dev/null
+++ b/bin/browserify.js
@@ -0,0 +1,65 @@
+#!/bin/sh
+':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@"
+
+var async = require('async'),
+ browserify = require('browserify'),
+ path = require('path'),
+ crypto = require('crypto'),
+ distJs = 'ipfsecret.js',
+ distFolder = './dist',
+ exampleFolder = './examples',
+ ipfsecretDir = 'assets/js',
+ ipfsecretSrc = 'assets/js' + path.sep + 'ipfsecret-decrypt.js',
+ wcryptSrc = require.resolve('webcrypto-crypt/index.js'),
+ fs = require('fs'),
+ mkdirp = require('mkdirp');
+
+async.series([
+
+ function(callback) {
+ var folders = [distFolder, exampleFolder];
+ folders.forEach(f => {
+ mkdirp(f, function (err) {
+ if (err) {
+ console.error('ERROR creating ' + f +
+ ': ' + err.message);
+ process.exit(0);
+ }
+ });
+ });
+ callback();
+ },
+
+ function(callback) {
+ var b = browserify([wcryptSrc, ipfsecretSrc],
+ {standalone: 'IPFSecret'}).transform('babelify',
+ {presets: ['es2015']});
+ b.ignore('node-webcrypto-ossl');
+ var jsStream = b.bundle();
+ var js = '';
+ jsStream.on('data', function (buf) {
+ js += buf;
+ });
+ jsStream.on('end', function () {
+ fs.writeFile(distFolder + path.sep + distJs, js, function(err) {
+ if(err) {
+ console.error('ERROR creating ' + distJs + ': ' +
+ err.message);
+ process.exit(0);
+ }
+ const hash = crypto.createHash('sha512');
+ hash.update(js);
+ fs.writeFile(ipfsecretDir + path.sep + 'sha512.txt',
+ hash.digest('base64'), function(err) {
+ if(err) {
+ console.error('ERROR creating sha512.txt: ' +
+ err.message);
+ process.exit(0);
+ }
+ callback();
+ });
+ });
+ });
+ }
+
+]);
diff --git a/bin/get-fonts.js b/bin/get-fonts.js
new file mode 100755
index 0000000..660a592
--- /dev/null
+++ b/bin/get-fonts.js
@@ -0,0 +1,44 @@
+#!/bin/sh
+':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@"
+
+const AdmZip = require('adm-zip'),
+ Config = require('../lib/config.json'),
+ fs = require ('fs'),
+ path = require ('path'),
+ https = require ('https'),
+ { URL } = require('url'),
+ util = require('util'),
+ fsUnlink = util.promisify(fs.unlink),
+ options = new URL(Config.fonts.sourceUri);
+
+const indexFile = require.resolve('../.'),
+ pathParts = indexFile.split(path.sep),
+ mainFile = pathParts.pop(),
+ fontPath = pathParts.join(path.sep) + path.sep + 'assets' + path.sep + 'fonts';
+
+var destination = fs.createWriteStream(Config.fonts.localFile);
+destination.on('finish', () => {
+ console.log('Extracting files.');
+ var zip = new AdmZip(Config.fonts.localFile);
+ zip.extractAllTo(fontPath, true);
+ fsUnlink(Config.fonts.localFile)
+ .then(() => {
+ console.log(Config.fonts.localFile + ' deleted.');
+ });
+});
+destination.on('error', (err) => {
+ console.log(err.stack);
+});
+
+const req = https.request(options, (res) => {
+ res.on('data', (d) => {
+ destination.write(d);
+ });
+ res.on('end', () => {
+ destination.end();
+ });
+});
+req.on('error', (e) => {
+ console.error(e);
+});
+req.end();
diff --git a/bin/install.sh b/bin/install.sh
new file mode 100755
index 0000000..d19a2c1
--- /dev/null
+++ b/bin/install.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+chmod 0600 test/.wcryptpass
+bin/get-fonts.js
+bin/browserify.js
+export WCRYPT_PASSFILE=test/.wcryptpass
+MULTIHASH=`bin/ipfsecret.js add -wrn test`
+cd ./examples
+export WCRYPT_PASSFILE=../test/.wcryptpass
+../bin/ipfsecret.js get "$MULTIHASH"
+ipfs get "$MULTIHASH"
+cd ..
diff --git a/bin/ipfsecret.js b/bin/ipfsecret.js
new file mode 100755
index 0000000..fce9564
--- /dev/null
+++ b/bin/ipfsecret.js
@@ -0,0 +1,389 @@
+#!/bin/sh
+':' //; exec "$(command -v nodejs || command -v node)" "$0" "$@"
+'use strict';
+const Config = require('../lib/config.json'),
+ bs58 = require('bs58'),
+ chop = require('chop'),
+ fs = require('fs'),
+ os = require('os'),
+ homePassFile = os.homedir() + '/.wcryptpass',
+ indexRegex = new RegExp(Config.ipfs.mainIndex + '$'),
+ mkdirp = require('mkdirp'),
+ passPath = process.env.WCRYPT_PASSFILE || homePassFile,
+ path = require('path'),
+ util = require('util'),
+ fsStat = util.promisify(fs.stat),
+ mkdir = util.promisify(mkdirp),
+ readline = require('readline-sync'),
+ readFile = util.promisify(fs.readFile),
+ subdirRegex = new RegExp('Qm.{44}' + Config.ipfs.mainSubdir),
+ yargs = require('yargs'),
+ IPFSecret = require('../.'),
+ prog = (path.basename(__filename).replace(/\.js$/,''));
+
+if (!process.stdin.isTTY) {
+ console.error(`Piping data to ${ prog } not currently supported.`);
+ process.exit(0);
+}
+else {
+ var includeHidden = false;
+ const ipfsecret = getIPFSecretObject();
+ const argv = getArgv(ipfsecret);
+ handleMainYargs();
+ if (yargs.argv.version) {
+ process.stdout.write(IPFSecret.version + "\n");
+ process.exit(0);
+ }
+ if (argv._.length < 1) {
+ yargs.showHelp();
+ }
+ else {
+ const arg = argv._[1], command = argv._[0];
+ validateCommand(arg, command);
+ exec(ipfsecret, arg, command);
+ }
+}
+
+function add(ipfsecret, command, arg) {
+ fsStat(arg)
+ .then(stats => {
+ checkAddSanity(command, arg, stats);
+ encrypt(ipfsecret, arg);
+ })
+ .catch(err => {
+ console.error(Config.err.noStat + err);
+ });
+}
+
+function addToIPFS(ipfsecret, arg, passphrase) {
+ return ipfsecret.add(arg, {
+ hidden: includeHidden,
+ indexed: yargs.argv.web,
+ passphrase: passphrase,
+ root: true
+ })
+}
+
+function checkAddSanity(command, arg, stats) {
+ if (stats.isDirectory() && !(yargs.argv.recursive)) {
+ console.error(`Error: '${ arg }' is a directory, use` +
+ ` '--recursive' to specify directories`);
+ process.exit(0);
+ }
+ else if (stats.isFile() && yargs.argv.recursive) {
+ console.error(`Error: '${ arg }' is a file, ` +
+ `'--recursive' only valid for directories`);
+ process.exit(0);
+ }
+ else if (yargs.argv.naked && yargs.argv.gateway) {
+ console.error(`Error: '--naked' is ` +
+ `invalid when used with '--gateway'`);
+ process.exit(0);
+ }
+}
+
+function checkPassFile(stats) {
+ if (parseInt(stats.mode) === 33152) {
+ return readFile(passPath)
+ .then(data => {
+ const passphrase = chop.chomp(data.toString('utf8'));
+ if (!passphrase) throw new Error(Config.err.passReqd);
+ return passphrase;
+ })
+ .catch(err => {handlePassErr(err);});
+ }
+ else {throw new Error(Config.err.filePerms);}
+}
+
+function debug(msg) {
+ var msg = Array.prototype.slice.call(arguments);
+ msg.unshift('[debug] ');
+ msg = msg.join(' ');
+ if (IPFSecret.DEBUG) console.error(msg);
+}
+
+function decrypt(ipfsecret, arg, passphrase) {
+ ipfsecret.get(arg, passphrase)
+ .then(stream => {getObjects(stream);})
+ .catch(err => {console.error(err);});
+}
+
+function encrypt(ipfsecret, arg) {
+ getPassphrase('encrypt')
+ .then(passphrase => {
+ addToIPFS(ipfsecret, arg, passphrase)
+ .then(hash => {outputResult(ipfsecret, hash);})
+ .catch(err => {console.error(err);});
+ })
+ .catch(err => {console.error(err);});
+}
+
+function exec(ipfsecret, arg, command) {
+ if (command === 'add') {
+ if (yargs.argv.web) debug('Adding ' + arg + ' with web interface');
+ else debug('Adding ' + arg);
+ add(ipfsecret, command, arg);
+ }
+ else if (command === 'get') {
+ debug('Retrieving ' + arg);
+ get(ipfsecret, arg);
+ }
+}
+
+function get(ipfsecret, arg) {
+ getPassphrase('decrypt')
+ .then(passphrase => {decrypt(ipfsecret, arg, passphrase);})
+ .catch(err => {console.error(err);});
+}
+
+function getAddOptions() {
+ return {
+ web: ['w', 'false', 'Add web interface'],
+ naked: ['n', 'false', 'With --web, return naked hash vs URL'],
+ recursive: ['r', 'false', 'Add as directory, recursively'],
+ hidden: ['H', 'false', 'When adding directory, include hidden files'],
+ };
+}
+
+function getAddArgs(yargs) {
+ let options = getAddOptions(), keys = Object.keys(options);
+ const argv = yargs.usage('Usage: $0 add [options] [file|dir]');
+ keys.forEach(k => {
+ let o = options[k];
+ argv.option(k, {alias: o[0], default:o[1], describe:o[2],
+ boolean: true});
+ });
+ argv.wrap(null).argv;
+}
+
+function getArgDescs() {
+ return {
+ add: 'Encrypt & add files to IPFS',
+ api: 'Specify IPFS API configuration',
+ debug: 'Print debugging info to stderr',
+ gateway: 'Use this HTTP(S) gateway when returning gateway address',
+ get: 'Retrieve & decrypt encrypted files from IPFS',
+ list: 'List known HTTPS gateways',
+ out: 'Path where output should be stored',
+ route: '/ip4/' + Config.ipfs.host + '/tcp/' + Config.ipfs.port,
+ ver: 'Display version and exit'
+ };
+}
+
+function getArgv(ipfsecret) {
+ const desc = getArgDescs();
+ const argv = yargs.usage('Usage: $0 [options]')
+ .strict()
+ .command('get', desc.get, function (yargs) {
+ const argv = yargs.usage('Usage: $0 get [multihash]')
+ .option('output', {alias: 'o', describe: desc.out,
+ type: 'string'})
+ .help('help').wrap(null).argv
+ })
+ .command('add', desc.add, function (yargs) {getAddArgs(yargs);})
+ .command('list', desc.list, function (yargs) {
+ let count = 0;
+ function list(item) {console.log((count++) + ' - ' + item);}
+ (ipfsecret.getGatewayList()).forEach(list);
+ process.exit(0);
+ })
+ .option('debug',
+ {alias: 'd', default: false, describe: desc.debug,
+ boolean: true})
+ .option('api',
+ {alias: 'a', default: desc.route, describe: desc.api,
+ type: 'string'})
+ .option('gateway',
+ {alias: 'g', default: false, describe: desc.gateway,
+ type: 'string'})
+ .option('version',
+ {alias: 'v', default: false, describe: desc.ver,
+ boolean: true})
+ .implies('gateway', 'web')
+ .implies('naked', 'web')
+ .help('help')
+ .wrap(null)
+ .argv;
+ return argv;
+}
+
+function getDefaultGateway(h) {
+ const gPort = Config.ipfs.gatewayPort,
+ host = Config.ipfs.host,
+ idx = Config.ipfs.mainIndex,
+ proto = Config.ipfs.proto,
+ def = `${ proto }://${ host }:${gPort}/ipfs/${ h }/${ idx }`;
+ return def;
+}
+
+function getIPFSecretObject() {
+ let ipfsecret;
+ if (yargs.argv.api) {
+ const parts = (yargs.argv.api).split('/'),
+ host = parts[2], port = parts[4];
+ ipfsecret = new IPFSecret({host: host, port: port});
+ }
+ else ipfsecret = new IPFSecret();
+ return ipfsecret;
+}
+
+function getMainOptions() {
+ const t = `/ip4/${ Config.ipfs.host }/tcp/${ Config.ipfs.port }`;
+ return {
+ debug: ['d', 'false', 'Print debugging info to stderr', true],
+ api: ['a', t, 'Specify IPFS API config', false, 'string'],
+ version: ['v', 'false', 'Display version and exit', true]
+ };
+}
+
+function getObjects(stream) {
+ stream.on('data', (obj) => {
+ if (
+ (obj.content) &&
+ !(obj.path.match(subdirRegex)) &&
+ !(obj.path.match(indexRegex)) &&
+ !(obj.path.match('mitm.html'))
+ ) {
+ obj.content.on('error', (err) => {
+ console.error('Error: Could not decrypt ' + obj.path);
+ });
+ const hasPath = new RegExp(path.sep);
+ if ((obj.path).match(hasPath)) handleDir(obj);
+ else handleFile(obj);
+ }
+ });
+}
+
+function getPassphrase(mode) {
+ return fsStat(passPath)
+ .then(stats => {return checkPassFile(stats);})
+ .catch(err => {
+ if (err.code === 'ENOENT') {
+ debug('No wcryptpass file found.');
+ return getPassphraseFromPrompt(mode);
+ }
+ else if (err.message === Config.err.filePerms) {
+ debug('wcryptpass file has insecure permissions.');
+ return getPassphraseFromPrompt(mode);
+ }
+ else throw err;
+ });
+}
+
+function getPassphraseFromPrompt(mode) {
+ return new Promise ((resolve, reject) => {
+ var passphrase = readline.question(Config.cmdline.passPrompt, {
+ hideEchoBack: true, mask: ''
+ });
+ if (!passphrase)
+ reject(new Error(Config.err.passReqd));
+ else if (mode === 'encrypt') {
+ var confirmPassphrase = readline.question(
+ Config.cmdline.passConf, {hideEchoBack: true, mask: ''});
+ if (confirmPassphrase !== passphrase)
+ resolve(getPassphraseFromPrompt(mode));
+ else resolve(passphrase.toString());
+ }
+ else resolve(passphrase.toString());
+ });
+}
+
+function handleDir(obj) {
+ const parsed = path.parse(obj.path),
+ dirParts = (parsed.dir).split(path.sep),
+ multihash = dirParts.shift(),
+ lDir = yargs.argv.output || (prog + '-' +
+ Config.ipfs.decryptedPrefix + '-' + multihash),
+ relDir = lDir + path.sep + dirParts.join(path.sep),
+ relative = relDir + path.sep + parsed.name + parsed.ext;
+
+ mkdir(relDir)
+ .then(() => {handleObj(relative, obj);})
+ .catch(err => {throw err;});
+}
+
+function handleFile(obj) {
+ const multihash = obj.path,
+ lFile = yargs.argv.output || (prog + '-' +
+ Config.ipfs.decryptedPrefix + '-' +
+ multihash),
+ writeable = fs.createWriteStream(lFile);
+ obj.content.pipe(writeable);
+ obj.content.on('finish', () => {
+ debug('Retrieved ' + multihash);
+ });
+}
+
+function handleObj(relative, obj) {
+ if (obj.content) {
+ const writeable = fs.createWriteStream(relative);
+ obj.content.pipe(writeable);
+ obj.content.on('finish', () => {
+ debug('Retrieved ' + relative);
+ });
+ }
+}
+
+function handleMainYargs() {
+ if (yargs.argv.hidden && !yargs.argv.recursive) {
+ console.error('--hidden only valid when used with --recursive');
+ process.exit(0);
+ }
+ if (yargs.argv.api) {
+ var regex = new RegExp('^/ip4/.+/tcp/\\d+$');
+ if (!(yargs.argv.api).match(regex)) {
+ console.error('--api syntax is invalid');
+ process.exit(0);
+ }
+ }
+ if (yargs.argv.debug) IPFSecret.DEBUG = true;
+ if (yargs.argv.hidden) includeHidden = true;
+}
+
+function handlePassErr(err) {
+ if (err.code === 'ENOENT') {
+ debug('No wcryptpass file found.');
+ return getPassphraseFromPrompt(mode);
+ }
+ else if (err.message === Config.err.filePerms) {
+ debug('wcryptpass file has insecure permissions.');
+ return getPassphraseFromPrompt(mode);
+ }
+ else if (err.message === Config.err.passReqd) throw err;
+ else throw err;
+}
+
+function outputResult(ipfsecret, hash) {
+ if (yargs.argv.web) {
+ if (yargs.argv.gateway) process.stdout.write(
+ useGateway(ipfsecret, bs58.encode(hash)));
+ else if (yargs.argv.web && yargs.argv.naked)
+ process.stdout.write(bs58.encode(hash));
+ else process.stdout.write(getDefaultGateway(bs58.encode(hash)));
+ }
+ else process.stdout.write(bs58.encode(hash));
+}
+
+function useGateway(ipfsecret, h) {
+ const idx = Config.ipfs.mainIndex,
+ list = ipfsecret.getGatewayList();
+ if (list[parseInt(yargs.argv.gateway)])
+ return `${ list[parseInt(yargs.argv.gateway)] }/ipfs/${ h }/${ idx }`;
+ else
+ return `${ yargs.argv.gateway }/ipfs/${ h }/${ idx }`;
+}
+
+function validateCommand(arg, command) {
+ const known = {add: 1, get: 1, list: 1};
+ if (!known[command]) {
+ console.error("\nUnknown command.\n");
+ yargs.showHelp();
+ process.exit(0);
+ }
+ else if (!arg) {
+ if (command === 'get') console.error('Multihash required');
+ if (command === 'add') console.error('File or directory required');
+ yargs.showHelp();
+ process.exit(0);
+ }
+}
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..17dab7c
--- /dev/null
+++ b/index.js
@@ -0,0 +1,52 @@
+const through = require('through2'),
+ Crypto = require('./lib/crypto'),
+ Indexer = require('./lib/indexer'),
+ Pkg = require('./package.json'),
+ Util = require('./lib/util');
+
+module.exports = exports = function (ipfsOpts) {
+
+ ipfsOpts = ipfsOpts || {};
+
+ const util = new Util(exports.DEBUG, ipfsOpts);
+ crypto = new Crypto(util),
+ idx = new Indexer(util),
+ ipfss = this;
+
+ util.debug('Debugging enabled.');
+
+ ipfss.add = (path, opt) => {
+ if (opt.indexed) return idx.create(opt, path, crypto.init);
+ else return crypto.encrypt(opt, path);
+ };
+
+ ipfss.addIndexed = (path, opt) => {
+ if (typeof opt === 'string') opt = {passphrase: opt};
+ opt.indexed = true;
+ return idx.create(opt, path, crypto.init);
+ };
+
+ ipfss.get = (hash, opt) => {
+ const stream = through.obj(
+ function decrypt(entry, enc, callback) {
+ if (entry.content)
+ entry = crypto.decrypt(opt, entry);
+ this.push(entry);
+ callback();
+ }
+ );
+ return crypto.decryptStream(hash, stream)
+ };
+
+ ipfss.getGatewayList = () => {
+ return util.cfg.ipfs.gateways
+ };
+
+ ipfss.version = exports.version = Pkg.version;
+
+ if (process.env.IPFSECRET_ENV === 'test') {
+ ipfss.fonts = idx.fonts;
+ ipfss.css = idx.css;
+ ipfss.js = idx.js;
+ }
+};
diff --git a/lib/config.json b/lib/config.json
new file mode 100644
index 0000000..6f8734f
--- /dev/null
+++ b/lib/config.json
@@ -0,0 +1,98 @@
+{
+ "cmdline": {
+ "passConf": "Confirm passphrase: ",
+ "passPrompt": "Passphrase? "
+ },
+ "err" : {
+ "encryptFail": "Encryption failed- ",
+ "filePerms": "File permissions are insecure.",
+ "multiString": "Please pass in the path as a String.",
+ "notAHash": "Result is not a recognized IPFS hash format.",
+ "noEncrypt": "Data is not webcrypto-crypt encrypted.",
+ "noErr": "Expected error but got none.",
+ "noHidden": "Could not detect any hidden encrypted files.",
+ "noRead": "Could not read dir- ",
+ "noStat": "Could not stat file- ",
+ "noSupp": " is not supported.",
+ "noUnlink": "Could not unlink file- ",
+ "objEmpty": "Object is empty.",
+ "passOrOpt": "Please pass in a passphrase String or options Object.",
+ "passReqd": "Passphrase is required.",
+ "pathString": "Please pass in the path as a String.",
+ "redacted": "(REDACTED)",
+ "relLinks": "Expected relative links were not found.",
+ "tooLarge": "This file may be too large for your browser to download.",
+ "zeroByte": "Skipping zero byte file- "
+ },
+ "fonts": {
+ "localDir": "assets/fonts",
+ "localFile": "/tmp/roboto.zip",
+ "sourceUri": "https://google-webfonts-helper.herokuapp.com/api/fonts/roboto?download=zip&subsets=latin&variants=300,700,300italic,regular,700italic&formats=woff,woff2"
+ },
+ "ipfs": {
+ "cryptoLib": {
+ "integrityHash": "assets/js/sha512.txt",
+ "path": "js/ipfsecret.js",
+ "source": {
+ "name": "dist/ipfsecret.js"
+ }
+ },
+ "css": {
+ "base": {
+ "path": "styles/normalize.css",
+ "source": "normalize.css/normalize.css"
+ },
+ "dialog": {
+ "path": "styles/dialog.css",
+ "source": "assets/css/dialog.css"
+ },
+ "fonts": {
+ "path": "styles/fonts.css",
+ "source": "assets/css/fonts.css"
+ },
+ "main": {
+ "path": "styles/milligram.min.css",
+ "source": "milligram/dist/milligram.min.css"
+ },
+ "map": {
+ "path": "styles/milligram.min.css.map",
+ "source": "milligram/dist/milligram.min.css.map"
+ }
+ },
+ "decryptedPrefix": "decrypted",
+ "encryptedSuffix": "wcrypt",
+ "fonts": {
+ "path": "fonts/"
+ },
+ "footerLink": "https://ipfs.io",
+ "footerText": "IPFS",
+ "gatewayPort": 8080,
+ "gateways": [
+ "https://gateway.ipfs.io",
+ "https://earth.i.ipfs.io",
+ "https://mercury.i.ipfs.io",
+ "https://gateway.ipfsstore.it:8443",
+ "https://scrappy.i.ipfs.io",
+ "https://chappy.i.ipfs.io"
+ ],
+ "host": "127.0.0.1",
+ "mainIndex" : "ipfsecret.html",
+ "mainSubdir": "/ipfsecret",
+ "port": 5001,
+ "proto": "http",
+ "ui": {
+ "browserDecryptNotSupported": "Cannot decrypt this file in the browser. Try downloading the encrypted file and decrypting it with node.js.",
+ "decrypt": "Decrypt",
+ "decrypting": "Decrypting",
+ "decryptUpdateDelay": 5,
+ "download": "Download",
+ "downloading": "Downloading, please wait",
+ "modified": "Date modified",
+ "name": "Name",
+ "offlineMode": "Offline mode detected. Attempting redirect to local IPFS gateway.",
+ "offlineMonitorInterval": 10000,
+ "passphrase": "Passphrase",
+ "size": "Size"
+ }
+ }
+}
diff --git a/lib/crypto/encrypt.js b/lib/crypto/encrypt.js
new file mode 100644
index 0000000..e46ba4f
--- /dev/null
+++ b/lib/crypto/encrypt.js
@@ -0,0 +1,126 @@
+const dir = require('node-dir'),
+ fs = require('fs'),
+ fsStat = require('util').promisify(fs.stat),
+ path = require('path'),
+ wcrypt = require('webcrypto-crypt'),
+ wStream = require('webcrypto-crypt/lib/node-streams.js');
+
+module.exports = exports = function (util) {
+
+ const encrypt = this;
+
+ encrypt.init = (opt, paths) => {
+ const wcryptOptions = util.setWcrypt(opt);
+ encrypt.wcrypt = new wcrypt.cipher(wcryptOptions),
+ clone = JSON.parse(JSON.stringify(wcryptOptions));
+ clone.material.passphrase = util.cfg.err.redacted;
+ util.debug('Wcrypt opt: ' + JSON.stringify(clone));
+ if (paths.type === 'dir') return encryptDir(opt, paths);
+ else if (paths.type === 'file') return encryptFile(opt, paths);
+ else {throw new Error(paths.source.abs + util.cfg.err.noSupp);}
+ };
+
+ function add(opt, stream, paths, filename) {
+ return fsStat(filename).then(stats => {
+ if (stats.size > 0)
+ return createEntry(opt, paths, stream, filename);
+ else
+ util.debug(util.cfg.err.zeroByte + filename);return false;
+ })
+ .catch(err => {throw err;});
+ }
+
+ function addFile(opt, paths, entries, err, stream, filename, next) {
+ if (err) reject(err);
+ if (qualifies(opt, filename)) {
+ return add(opt, stream, paths, filename)
+ .then(entry => {
+ if (entry) entries.push(entry);
+ next();
+ })
+ .catch(err => {throw err;});
+ }
+ else {next();}
+ }
+
+
+ function createEntry(opt, paths, stream, filename) {
+ util.debug('Encrypting file: ' + filename);
+ paths = util.setFile(paths, filename);
+ return {
+ path: paths.ipfs.file + '.' + opt.suffix,
+ content: wStream.encrypt(encrypt.wcrypt, stream)
+ };
+ }
+
+ function encryptDir(opt, paths) {
+ util.debug('Adding directory ' + paths.source.abs);
+ return encrypt.wcrypt.encrypt(Buffer.from(''))
+ .then(() => {
+ return encrypt.wcrypt.encrypt(Buffer.from(''))
+ .then(() => {return readDir(opt, paths);})
+ .catch((err) => {throw err;});
+ })
+ .catch((err) => {throw err;});
+ }
+
+ function encryptFile(opt, paths) {
+ util.debug('Adding file ' + paths.source.abs);
+ return encrypt.wcrypt.encrypt(Buffer.from(''))
+ .then(() => {
+ const cleartextIn = fs.createReadStream(paths.source.abs),
+ encryptedStream = wStream.encrypt(encrypt.wcrypt,
+ cleartextIn);
+ return oneFile(opt, paths, encryptedStream);
+ })
+ .catch((err) => {throw err;});
+ }
+
+ function getAllEntries(opt, paths, entries, resolve, reject) {
+ let dirOpt;
+ if (opt.hidden) dirOpt = {};
+ else dirOpt = {exclude: /^\./};
+ dirOpt.encoding = null; // force binary
+ dir.readFilesStream(paths.source.abs, dirOpt,
+ (err, stream, filename, next) => {
+ addFile(opt, paths, entries, err, stream, filename, next);
+ },
+ err => {
+ if (err) reject(err);
+ else resolve(entries);
+ }
+ );
+ }
+
+ function oneFile(opt, paths, encryptedStream) {
+ if (opt.indexed) {
+ return new Promise((resolve, reject) => {
+ resolve([{
+ path: paths.ipfs.item + '.' + opt.suffix,
+ content: encryptedStream
+ }]);
+ });
+ }
+ else {
+ return new Promise((resolve, reject) => {
+ resolve([{
+ path: path.basename(paths.ipfs.item + '.' + opt.suffix),
+ content: encryptedStream
+ }]);
+ });
+ }
+ }
+
+ function qualifies(opt, filename) {
+ if (opt.hidden || !filename.match(/\/\./)) return true;
+ return false;
+ }
+
+ function readDir(opt, paths, entries) {
+ entries = entries || [];
+ return new Promise((resolve, reject) => {
+ getAllEntries(opt, paths, entries, resolve, reject);
+ });
+ }
+
+};
diff --git a/lib/crypto/index.js b/lib/crypto/index.js
new file mode 100644
index 0000000..c2e7de7
--- /dev/null
+++ b/lib/crypto/index.js
@@ -0,0 +1,39 @@
+const wStream = require('webcrypto-crypt/lib/node-streams.js'),
+ Encrypt = require('./encrypt');
+
+module.exports = exports = function (util) {
+ const crypto = this,
+ encrypt = new Encrypt(util);
+
+ crypto.decrypt = (opt, entry) => {
+ opt = util.validateOpt(opt);
+ entry.content.on('error', err => {
+ util.debug(`${ err } ${ entry.path}`);
+ delete entry.content;
+ });
+ entry.content = wStream.decrypt(opt.passphrase, entry.content);
+ entry.path = entry.path.replace('.' + opt.suffix, '');
+ return entry;
+ };
+
+ crypto.decryptStream = (hash, stream) => {
+ hash = util.validateHash(hash);
+ return util.getFromIPFS(hash, stream);
+ };
+
+ crypto.encrypt = (opt, path) => {
+ opt = util.validateOpt(opt);
+ return util.setPaths(opt, path)
+ .then(paths => {
+ return encrypt.init(opt, paths)
+ .then(files => {return util.addToIPFS(opt, files);})
+ .catch(err => {throw err;});
+ })
+ .catch(err => {throw err;});
+ };
+
+ crypto.init = encrypt.init;
+
+ crypto.util = util;
+
+};
diff --git a/lib/indexer/html.js b/lib/indexer/html.js
new file mode 100644
index 0000000..935db83
--- /dev/null
+++ b/lib/indexer/html.js
@@ -0,0 +1,236 @@
+const fs = require('fs'),
+ he = require('he'),
+ path = require('path'),
+ readFile = require('util').promisify(fs.readFile);
+
+module.exports = exports = function (util) {
+
+ let html = this;
+ mainDirRegex = new RegExp((util.cfg.ipfs.mainSubdir).replace(/^\//,''));
+
+ let integrityHash = '';
+ readFile(__dirname + path.sep + '..' + path.sep + '..' +
+ path.sep + util.cfg.ipfs.cryptoLib.integrityHash)
+ .then((hash) => {integrityHash = hash;})
+ .catch(err => {throw err;});
+
+ html.addRow = (item, size, date, button) => {
+ const formatted = util.format(size),
+ s = ' ',
+ ss = s.repeat(5);
+ return ` ${ ss }
+ ${ ss }${ s }${ item } |
+ ${ ss }${ s }${ formatted } |
+ ${ ss }${ s }${ date } |
+ ${ ss }${ s }${ button } |
+ ${ ss }
`;
+ };
+
+ html.createPointers = (opt, paths) => {
+ return new Promise((resolve, reject) => {
+ resolve(createPointers(opt, paths));
+ });
+ };
+
+ html.createLink = (href, text, isLinked, isDir) => {
+ href = encodeURI(href);
+ text = he.encode(text);
+ let c = '', e = '', i = '', lo = '', lc = '', o = '';
+ if (href && isLinked) { e = ''; lo = '';}
+ if (isDir) { o = '['; c = ']';i = '/' + util.cfg.ipfs.mainIndex}
+ return `${ lo }${ href }${ i }${ lc }${ o }${ text }${ c }${ e }`;
+ };
+
+ html.decryptButton = () => {
+ return '';
+ };
+
+ html.finish = (paths, items) => {
+ items.push(html.getFooter());
+ let p = paths.ipfs.item + '/' + util.cfg.ipfs.mainIndex;
+ if (paths.type === 'file') p = path.parse(paths.ipfs.item).dir + '/' + util.cfg.ipfs.mainIndex;
+
+ return {
+ path: p,
+ content: Buffer.from(items.join("\n"), 'utf8')
+ };
+ };
+
+ html.getFooter = (footerLink, footerText) => {
+ if (!footerLink) footerLink = util.cfg.ipfs.footerLink;
+ if (!footerText) footerText = util.cfg.ipfs.footerText;
+ let footer = `
+
+
+
+
+