When using Storj from FileZilla, we need to enter 12 words as an encryptiopn key. This post introduces how Storj encrypts our files with that encryption key. It is a summry of the following source files:
- https://github.com/Storj/libstorj/blob/master/src/downloader.c
- https://github.com/Storj/libstorj/blob/master/src/crypto.c
- https://github.com/Storj/libstorj/blob/master/src/bip39.c
Although they are deprecated, there are other source files in JapaScript:
- https://github.com/Storj/storj.js/blob/master/lib/api/download.js
- https://github.com/Storj/core/blob/ae563e7362b22ebe94616edeff723c1889763c27/lib/crypto-tools/deterministic-key-iv.js
- https://github.com/Storj/core/blob/ae563e7362b22ebe94616edeff723c1889763c27/lib/crypto-tools/decrypt-stream.js
In Storj, each file is stored in a bucket and identifired with a pair of bucket ID and file ID. These information is also used as a part of encryption key as well as 12 words encryption key.
Such 12 words encryption key is popular in some cryptocurrencies and called mnemonic code defined in BIP39. Storj actually uses a library for Bitcoin and generate a master seed from the mnemonic code:
import Mnemonic from 'bitcore-mnemonic’;
const encryptionKey = Mnemonic('12 words');
const seed = encryptionKey.toSeed();
The master seed is concatinated with the bucket ID where the file stored, and used as the input of SHA-512 hash. The first 256 bits of the hash is called bucket key.
import crypto from 'crypto';
const bucketID = Buffer('bucket ID', 'hex')
const bucketKey = crypto.createHash('sha512').update(Buffer.concat([seed, bucketID])).digest().slice(0, 32);
The bucket key is also concatinated with the file ID, and then used as the input of SHA-512 hash. Again, the first 256 bits of the hash value is called file key.
const fileID = Buffer('file ID', 'hex')
const fileKey = crypto.createHash('sha512').update(Buffer.concat([bucketKey, fileID])).digest().slice(0, 32);
The file is encrypted by AES-256-CTR algorithm where the encryption key is the SHA-256 hash of the file key, and the initialization vector is the fist 128 bits of the RIPEMD-160 hash of the file ID.
import fs from 'fs';
const key = crypto.createHash('sha256').update(fileKey).digest();
const iv = crypto.createHash('rmd160').update(fileID).digest().slice(0, 16)
const cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
const input = fs.createReadStream(‘plain_file');
const output = fs.createWriteStream(‘encrypted_file');
input.pipe(cipher).pipe(output);
Note that, in AES-256-CTR, decryption algorithm is same as the encryption algorithm, and we can decrypt the encrypted file with the same encryption key and initialization vactor.
const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
const input = fs.createReadStream(‘encrypted_file');
const output = fs.createWriteStream(‘plain_file');
input.pipe(decipher).pipe(output);