Comparing version 1.0.4 to 1.0.6
@@ -12,2 +12,12 @@ const { dirname } = require('path') | ||
const isENOENT = (err) => err.code && err.code === 'ENOENT' | ||
const ioError = (err, path, method) => isENOENT(err) | ||
? err instanceof MobilettoNotFoundError | ||
? err | ||
: new MobilettoNotFoundError(path) | ||
: err instanceof MobilettoError || err instanceof MobilettoNotFoundError | ||
? err | ||
: new MobilettoError(`${method}(${path}) error: ${err}`, err) | ||
class StorageClient { | ||
@@ -78,3 +88,3 @@ baseDir | ||
} catch (err) { | ||
throw new MobilettoError(`list(${path}) error: ${err}`, err) | ||
throw ioError(err, path, 'list') | ||
} | ||
@@ -87,7 +97,14 @@ } | ||
if (!lstat) { | ||
throw new MobilettoError(`metadata: lstat error`) | ||
throw new MobilettoError('metadata: lstat error') | ||
} | ||
const type = this.fileType(lstat) | ||
if (type === M_DIR && path !== '') { | ||
const contents = await this.list(path) | ||
if (contents.length === 0) { | ||
throw new MobilettoNotFoundError(path) | ||
} | ||
} | ||
return { | ||
name: file.startsWith(this.baseDir) ? file.substring(this.baseDir.length) : file, | ||
type: this.fileType(lstat), | ||
type, | ||
size: lstat.size, | ||
@@ -97,6 +114,3 @@ mtime: lstat.mtimeMs | ||
} catch (err) { | ||
if (err.code && err.code === 'ENOENT') { | ||
throw new MobilettoNotFoundError(path) | ||
} | ||
throw new MobilettoError(`metadata(${path}) error: ${err}`, err) | ||
throw ioError(err, path, 'metadata') | ||
} | ||
@@ -157,6 +171,3 @@ } | ||
} catch (err) { | ||
if (err.code && err.code === 'ENOENT') { | ||
throw new MobilettoNotFoundError(path) | ||
} | ||
throw new MobilettoError(`read(${path}) error: ${err}`, err) | ||
throw ioError(err, path, 'read') | ||
} | ||
@@ -173,3 +184,3 @@ } | ||
} catch (err) { | ||
if (err.code && err.code === 'ENOENT') { | ||
if (isENOENT(err)) { | ||
if (!quiet) { | ||
@@ -176,0 +187,0 @@ throw new MobilettoNotFoundError(path) |
@@ -83,3 +83,4 @@ const { | ||
const response = await this.client.send(new ListObjectsCommand(bucketParams)) | ||
if (typeof response.Contents !== 'undefined') { | ||
const hasContents = typeof response.Contents !== 'undefined' | ||
if (hasContents) { | ||
response.Contents.forEach((item) => { | ||
@@ -89,3 +90,4 @@ objects.push(item.Key) // todo: transform into standard object | ||
} | ||
if (typeof response.CommonPrefixes !== 'undefined') { | ||
const hasCommonPrefixes = typeof response.CommonPrefixes !== 'undefined' | ||
if (hasCommonPrefixes) { | ||
response.CommonPrefixes.forEach((item) => { | ||
@@ -99,9 +101,18 @@ objects.push(item.Prefix) // todo: transform into standard object | ||
bucketParams.Marker = response.NextMarker | ||
} else if (!hasContents && !hasCommonPrefixes) { | ||
if (path === '') { | ||
break | ||
} | ||
throw new MobilettoNotFoundError(path) | ||
} | ||
// At end of the list, response.truncated is false, and the function exits the while loop. | ||
} catch (err) { | ||
console.log(`${logPrefix} Error: ${err}`) | ||
truncated = false | ||
if (err instanceof MobilettoNotFoundError) { | ||
throw err | ||
} | ||
throw new MobilettoError(`${logPrefix} Error: ${err}`) | ||
} | ||
} | ||
if (recursive && objects.length === 0) { | ||
throw new MobilettoNotFoundError(path) | ||
} | ||
return objects | ||
@@ -219,3 +230,10 @@ } | ||
} | ||
objects = await this._list(path, {MaxKeys: DELETE_OBJECTS_MAX_KEYS}) | ||
try { | ||
objects = await this._list(path, {MaxKeys: DELETE_OBJECTS_MAX_KEYS}) | ||
} catch (e) { | ||
if (!(e instanceof MobilettoNotFoundError)) { | ||
throw e instanceof MobilettoError ? e : new MobilettoError(`remove(${path}): error listing: ${e}`) | ||
} | ||
objects = null | ||
} | ||
} | ||
@@ -222,0 +240,0 @@ } else { |
240
index.js
@@ -0,4 +1,12 @@ | ||
const { basename, dirname } = require('path') | ||
const shasum = require('shasum') | ||
const randomstring = require('randomstring') | ||
const crypt = require('./util/crypt') | ||
const {DEFAULT_CRYPT_ALGO, normalizeKey, normalizeIV} = require("./util/crypt"); | ||
const {DEFAULT_CRYPT_ALGO, normalizeKey, normalizeIV, encrypt, decrypt} = require("./util/crypt"); | ||
const DIR_ENT_DIR_SUFFIX = '__.dirent' | ||
const DIR_ENT_FILE_PREFIX = 'dirent__' | ||
const ENC_PAD_SEP = ' ~ ' | ||
// adapted from https://stackoverflow.com/a/27724419 | ||
@@ -27,2 +35,27 @@ function MobilettoError (message, err) { | ||
const reader = (chunks) => (chunk) => { | ||
if (chunk) { | ||
chunks.push(chunk) | ||
} | ||
} | ||
const UTILITY_FUNCTIONS = { | ||
readFile: (client) => async (path) => { | ||
const chunks = [] | ||
await client.read(path, reader(chunks)) | ||
return Buffer.concat(chunks) | ||
} | ||
} | ||
function addUtilityFunctions (client) { | ||
for (const func of Object.keys(UTILITY_FUNCTIONS)) { | ||
client[func] = UTILITY_FUNCTIONS[func](client) | ||
} | ||
return client | ||
} | ||
async function connect (driverPath, key, secret, opts, encryption = null) { | ||
return await mobiletto(driverPath, key, secret, opts, encryption) | ||
} | ||
async function mobiletto (driverPath, key, secret, opts, encryption = null) { | ||
@@ -35,3 +68,3 @@ const driver = require(driverPath.includes('/') ? driverPath : `./drivers/${driverPath}/index.js`) | ||
if (encryption === null) { | ||
return client | ||
return addUtilityFunctions(client) | ||
} | ||
@@ -46,13 +79,91 @@ const encKey = normalizeKey(encryption.key) | ||
} | ||
const encryptPaths = encryption.encryptPaths || true; | ||
const enc = { | ||
key: encKey, | ||
iv, | ||
algo: encryption.algo || DEFAULT_CRYPT_ALGO | ||
algo: encryption.algo || DEFAULT_CRYPT_ALGO, | ||
encryptPaths, | ||
dirLevels: encryption.dirLevels || 4, | ||
encPathPadding: () => ENC_PAD_SEP + randomstring.generate(1 + Math.floor(2*Math.random())) | ||
} | ||
return { | ||
list: async (path) => client.list(path), | ||
metadata: async (path) => client.metadata(path), | ||
function encryptPath (path) { | ||
const encrypted = shasum(enc.key + ' ' + path) | ||
let newPath = '' | ||
for (let i = 0; i <= enc.dirLevels; i++) { | ||
if (newPath.length > 0) newPath += '/' | ||
newPath += encrypted.substring(i*2, (i*2)+2) | ||
} | ||
return newPath + encrypted | ||
} | ||
const direntDir = dir => encryptPaths ? encryptPath(dir + DIR_ENT_DIR_SUFFIX) : null | ||
const direntFile = (dirent, path) => dirent + '/' + shasum(DIR_ENT_FILE_PREFIX + ' ' + path) | ||
const _metadata = (client) => async (path) => { | ||
if (!encryptPaths) { | ||
return client.metadata(path) | ||
} | ||
let meta | ||
try { | ||
meta = await client.metadata(encryptPath(path)) | ||
} catch (e) { | ||
if (e instanceof MobilettoNotFoundError) { | ||
const dd = direntDir(path); | ||
try { | ||
meta = await client.metadata(dd) | ||
} catch (err) { | ||
if (err instanceof MobilettoNotFoundError) { | ||
const contents = await client.list(dd) | ||
if (Array.isArray(contents) && contents.length > 0) { | ||
return { name: path, type: M_DIR } | ||
} else { | ||
throw err | ||
} | ||
} else { | ||
throw err | ||
} | ||
} | ||
} else { | ||
throw e | ||
} | ||
} | ||
meta.name = path // rewrite name back to plaintext name | ||
return meta | ||
} | ||
const _loadMeta = async (dirent, entries) => { | ||
const files = [] | ||
for (const entry of entries) { | ||
const cipherText = [] | ||
await client.read(dirent + '/' + basename(entry.name), reader(cipherText)) | ||
const plain = decrypt(cipherText.toString(), enc) | ||
const realPath = plain.split(ENC_PAD_SEP)[0] | ||
const meta = await _metadata(client)(realPath) | ||
files.push(meta) | ||
} | ||
return files | ||
} | ||
async function removeDirentFile(path) { | ||
const df = direntFile(direntDir(dirname(path)), path); | ||
await client.remove(df, {recursive: false, quiet: true}) | ||
await client.remove(encryptPath(path), {recursive: false, quiet: true}) | ||
} | ||
const encClient = { | ||
list: async (path) => { | ||
if (!encryptPaths) { | ||
return await client.list(path) | ||
} | ||
const dirent = direntDir(path) | ||
const entries = await client.list(dirent) | ||
if (!entries || entries.length === 0) { | ||
return [] | ||
} | ||
return _loadMeta(dirent, entries) | ||
}, | ||
metadata: _metadata(client), | ||
read: async (path, callback) => { | ||
const realPath = encryptPaths ? encryptPath(path) : path | ||
const cipher = crypt.startDecryptStream(enc) | ||
return client.read(path, | ||
return client.read(realPath, | ||
(chunk) => { | ||
@@ -65,2 +176,21 @@ return callback(crypt.updateCryptStream(cipher, chunk)) | ||
write: async (path, readFunc) => { | ||
// if encrypting paths, write dirent file(s) for all parent directories | ||
if (encryptPaths) { | ||
let p = path | ||
while (true) { | ||
const direntGenerator = function* () { | ||
yield encrypt(p + enc.encPathPadding(), enc) | ||
} | ||
const dir = direntDir(dirname(p)) | ||
const df = direntFile(dir, p); | ||
if (!(await client.write(df, direntGenerator()))) { | ||
throw new MobilettoError('write: error writing dirent file') | ||
} | ||
p = dirname(p) | ||
if (p === '.' || p === '') { | ||
break | ||
} | ||
} | ||
} | ||
function* cryptGenerator(plaintextGenerator) { | ||
@@ -75,3 +205,4 @@ let chunk = plaintextGenerator.next().value | ||
const cipher = crypt.startEncryptStream(enc) | ||
return client.write(path, cryptGenerator(readFunc)) | ||
const realPath = encryptPath(path) | ||
return client.write(realPath, cryptGenerator(readFunc)) | ||
}, | ||
@@ -81,5 +212,61 @@ remove: async (path, options) => { | ||
const quiet = (options && options.quiet) || false | ||
return client.remove(path, {recursive, quiet}) | ||
if (!encryptPaths) { | ||
return client.remove(path, {recursive, quiet}) | ||
} | ||
if (recursive) { | ||
// ugh. we have to iterate over all dirent files, and remove each file/subdir one by one | ||
async function recRm (path) { | ||
const dirent = direntDir(path) | ||
let entries | ||
try { | ||
entries = await client.list(dirent) | ||
} catch (e) { | ||
if (!(e instanceof MobilettoNotFoundError)) { | ||
throw e instanceof MobilettoError | ||
? e | ||
: new MobilettoError('error listing files for recursive deletion', e) | ||
} | ||
} | ||
if (entries && entries.length > 0) { | ||
const files = await _loadMeta(dirent, entries) | ||
for (const f of files) { | ||
await recRm(f.name) | ||
} | ||
} | ||
await removeDirentFile(path); | ||
} | ||
await recRm(path) | ||
} | ||
// remove the single file/dir | ||
await removeDirentFile(path); | ||
// if we were the last dirent file, then also remove dirent directory, and recursively upwards | ||
let parent = path | ||
let dirent = direntDir(parent) | ||
const df = direntFile(dirent, path) | ||
await client.remove(df, {recursive: false, quiet: true}) | ||
while (true) { | ||
try { | ||
const entries = await client.list(dirent) | ||
if (entries.length === 0) { | ||
await removeDirentFile(parent) | ||
} | ||
} catch (e) { | ||
if (!(e instanceof MobilettoNotFoundError)) { | ||
throw e | ||
} | ||
} | ||
if (parent === '.' || parent === '/') { | ||
// do not remove dirent for root dir | ||
break | ||
} | ||
parent = dirname(parent) | ||
dirent = direntDir(parent) | ||
} | ||
return true | ||
} | ||
} | ||
return addUtilityFunctions(encClient) | ||
} | ||
@@ -112,3 +299,3 @@ | ||
if (err) { | ||
console.log(`writeStream: error writing: ${err}`) | ||
console.error(`writeStream: error writing: ${err}`) | ||
throw err | ||
@@ -124,3 +311,3 @@ } | ||
if (err) { | ||
console.log(`closeStream: error closing: ${err}`) | ||
console.error(`closeStream: error closing: ${err}`) | ||
throw err | ||
@@ -131,29 +318,2 @@ } | ||
async function streamReader (stream, callback, endCallback) { | ||
let count = 0 | ||
const streamHandler = async (stream) => { | ||
new Promise((resolve, reject) => { | ||
stream.on('data', (data) => { | ||
count += data ? data.length : 0 | ||
callback(data) | ||
}) | ||
stream.on('error', reject) | ||
stream.on('end', (resolve) => { | ||
if (typeof endCallback === 'function') { | ||
const endData = endCallback() | ||
if (endData) { | ||
count += endData.length | ||
callback(endData) | ||
} | ||
} | ||
resolve() | ||
}) | ||
}) | ||
} | ||
await streamHandler(stream).then(() => { | ||
// console.log('streamHandler ended') | ||
}) | ||
return count | ||
} | ||
const M_FILE = 'file' | ||
@@ -166,5 +326,5 @@ const M_DIR = 'dir' | ||
M_FILE, M_DIR, M_LINK, M_SPECIAL, | ||
mobiletto, | ||
mobiletto, connect, | ||
MobilettoError, MobilettoNotFoundError, | ||
readStream, writeStream, closeStream, streamReader | ||
readStream, writeStream, closeStream | ||
} |
{ | ||
"name": "mobiletto", | ||
"version": "1.0.4", | ||
"version": "1.0.6", | ||
"description": "A storage abstraction layer", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
Mobiletto | ||
========= | ||
Mobiletto is a JavaScript storage abstraction layer. | ||
Mobiletto is a JavaScript storage abstraction layer, with optional transparent client-side encryption. | ||
It supports apps that are agnostic to where files are stored. | ||
All drivers are tested for identical behavior with 30 tests for each driver. | ||
# Basic usage | ||
const { mobiletto } = require('mobiletto') | ||
const { mobiletto, readFile } = require('mobiletto') | ||
@@ -28,3 +30,3 @@ // General usage | ||
// * prefix: optional, all read/writes within the S3 bucket will be under this prefix | ||
// * delimiter: optional, directory delimiter, default is '/' | ||
// * delimiter: optional, directory delimiter, default is '/' (note: always '/' when encryption is enabled) | ||
const s3 = await mobiletto('s3', aws_key, aws_secret, {bucket: 'bk', region: 'us-east-1'}) | ||
@@ -50,2 +52,6 @@ | ||
s3.read(path, callback) | ||
// Read an entire file at once (convenience method) | ||
const localData = await local.readFile(path) // returns a byte Buffer of the file contents | ||
const s3Data = await s3.readFile(path) // returns a byte Buffer of the file contents | ||
@@ -79,5 +85,6 @@ // Write a file | ||
const encryption = { | ||
key: randomstring.generate(128), // required, must be >= 16 chars | ||
iv: randomstring.generate(128), // optional | ||
algo: 'aes-256-cbc' // optional, aes-256-cbc is the default | ||
key: randomstring.generate(128), // required, must be >= 32 chars | ||
iv: randomstring.generate(128), // optional, default is to derive IV from key | ||
algo: 'aes-256-cbc', // optional, aes-256-cbc is the default | ||
encryptPaths: true // optional, default is true | ||
} | ||
@@ -88,4 +95,26 @@ const api = await mobiletto(driverName, key, secret, opts, encryption) | ||
// Subsequent read operations will decrypt data (client side) when reading | ||
// Path names will also be encrypted | ||
// If `encryptPaths` is true, then path names will be hashed using the encrpytion key | ||
Note that when `encryptPaths` is enabled, `list` commands are considerably more inefficient, | ||
especially for directories with a large number of files. | ||
What's happening? A separate "directory entry" directory (encrypted) tracks what files are in that | ||
directory (aka the dirent directory). | ||
* The `list` command reads the directory entry files, decrypts each path listed; then returns metadata for each file | ||
* The `write` command writes dirent files in each parent's dirent directory, recursively; then writes the file | ||
* The `remove` command removes the corresponding dirent file, and its parent if empty, recursively; then removes the file | ||
* Note: Recursive removal on large and deep filesystems can be an expensive operation | ||
Note that even with `encryptPaths` enabled, an adversary with full visibility into your encrypted storage, | ||
even without the key, can still see the total number of directories and how many files are in each, and with some | ||
effort, discover some or all of the overall hierarchical structure. They would not know the names of the | ||
directories/files unless they also knew your encryption key or had otherwise successfully cracked the encryption. | ||
All bets are off then! | ||
# Alternate usage: import fully scoped module and use `connect` | ||
const storage = require('mobiletto') | ||
const s3 = await storage.connect('s3', aws_key, aws_secret, {bucket: 'bk', region: 'us-east-1'}) | ||
const objectData = await s3.readFile('some/path') | ||
# Driver Interface | ||
@@ -117,8 +146,10 @@ A driver is any JS file that exports a 'storageClient' function with this signature: | ||
// Read a file | ||
async read (path, callback) // callback receives a chunk of data; a null chunk signals end of data | ||
// callback receives a chunk of data. endCallback is called at end-of-stream | ||
async read (path, callback, endCallback = null) | ||
// Write a file | ||
async write (path, readFunc) // readFunc returns the data you want to write, or null to finish | ||
// readFunc returns the data you want to write | ||
async write (path, readFunc) | ||
// Remove a file, or recursively delete a directory | ||
async remove (path, {recursive = false, quiet = false}) |
@@ -9,4 +9,4 @@ // To run the tests, you need a .env file one level above this directory | ||
const { mobiletto } = require('../index.js') | ||
const {MobilettoNotFoundError, M_FILE, M_DIR} = require("../index") | ||
const { mobiletto, connect, MobilettoNotFoundError, M_FILE, M_DIR } = require("../index") | ||
const { encrypt, decrypt, normalizeKey, normalizeIV, DEFAULT_CRYPT_ALGO } = require("../util/crypt") | ||
@@ -17,7 +17,11 @@ // chunk size used by generator function, used by driver's 'write' function | ||
const TEMP_SZ_MULTIPLE = 3 // temp file will be ~24k (READ_SZ * 3) | ||
const ENC_SIZE_CLOSE_ENOUGH_PERCENT = 0.05; | ||
// encoded bytes written will differ from actual bytes provided | ||
// likewise, plaintext bytes read will differ from encoded bytes read | ||
// our tests give some leeway when filesize differences | ||
const ENC_SIZE_CLOSE_ENOUGH_PERCENT = 0.10 | ||
DRIVER_CONFIG = { | ||
local: { | ||
key: '/tmp' | ||
key: process.env.LOCAL_STORAGE || '/tmp' | ||
}, | ||
@@ -35,2 +39,7 @@ s3: { | ||
function closeEnough (expected, actual, percent = ENC_SIZE_CLOSE_ENOUGH_PERCENT) { | ||
expect(Math.abs(expected - actual)).to.be.lessThan(Math.floor(expected * percent), | ||
'expected size within 5% of actual (due to encryption)') | ||
} | ||
async function assertMeta (api, name, expectedSize, closeEnoughPercent = null) { | ||
@@ -41,4 +50,3 @@ const meta = await api.metadata(name) | ||
if (closeEnoughPercent) { | ||
expect(Math.abs(expectedSize - meta.size)).to.be.lessThan(Math.floor(meta.size * ENC_SIZE_CLOSE_ENOUGH_PERCENT), | ||
'expected write API to return within 5% of bytes written (due to encryption)') | ||
closeEnough(expectedSize, meta.size, closeEnoughPercent) | ||
} else { | ||
@@ -75,14 +83,17 @@ expect(meta.size).equals(expectedSize, 'expected size of written file to equal size of randomData') | ||
async function readFile(fixture) { | ||
const chunks = [] | ||
function reader(chunk) { | ||
if (chunk) { | ||
chunks.push(chunk) | ||
describe('crypto test', () => { | ||
it("should encrypt and decrypt successfully", async () => { | ||
const enc = { | ||
key: normalizeKey(randomstring.generate(32)), | ||
iv: normalizeIV(randomstring.generate(16)), | ||
algo: DEFAULT_CRYPT_ALGO | ||
} | ||
} | ||
const response = await fixture.api.read(fixture.name, reader) | ||
const data = Buffer.concat(chunks) | ||
return { response, data } | ||
} | ||
const plaintext = randomstring.generate(1024 + Math.floor(1024*Math.random())) | ||
const ciphertext = encrypt(plaintext, enc) | ||
const decrypted = decrypt(ciphertext, enc) | ||
expect(decrypted).to.equal(plaintext, 'decrypted data did not match plaintext') | ||
}) | ||
}) | ||
// To test a single driver: | ||
@@ -128,3 +139,3 @@ // - Uncomment one of the lines below to set driverName to the one you want to test | ||
const name = `test_file_${fileSuffix}` | ||
mobiletto(driverName, config.key, config.secret, config.opts) | ||
connect(driverName, config.key, config.secret, config.opts) | ||
.then(api => { fixture = {api, name, randomData} }) | ||
@@ -138,4 +149,4 @@ .finally(done) | ||
it("should read the file we just wrote", async () => { | ||
const { response, data } = await readFile(fixture); | ||
expect(response).is.equal(data.length, 'expected read API to return correct number of bytes read') | ||
const data = await fixture.api.readFile(fixture.name); | ||
expect(data.length).is.equal(size, 'expected read API to return correct number of bytes read') | ||
expect(data.toString('utf8')).to.equal(fixture.randomData, 'expected to read back the same data we wrote') | ||
@@ -171,9 +182,8 @@ }) | ||
encryptedByteCount = await writeRandomFile(fixture, size); | ||
expect(Math.abs(encryptedByteCount - size)).to.be.lessThan(Math.floor(size * ENC_SIZE_CLOSE_ENOUGH_PERCENT), | ||
'expected write API to return within 5% of bytes written (due to encryption)') | ||
closeEnough(size, encryptedByteCount) | ||
}) | ||
it("should read the encrypted file we just wrote", async () => { | ||
const { response, data } = await readFile(fixture); | ||
expect(response).is.equal(encryptedByteCount, 'expected read API to return correct number of bytes read') | ||
expect(data.toString('utf8')).to.equal(fixture.randomData, 'expected to read back the same data we wrote') | ||
const data = await fixture.api.readFile(fixture.name); | ||
expect(data.length).is.equal(size, 'expected read API to return correct number of bytes read') | ||
expect(data.toString()).to.equal(fixture.randomData, 'expected to read back the same data we wrote') | ||
}) | ||
@@ -192,54 +202,67 @@ it("should load metadata on the encrypted file we just wrote", async () => { | ||
describe(`${driverName} - write files in a new dir, read metadata, recursively delete`, () => { | ||
// a random directory and file within it | ||
const randomParent = `random_dir_${rand(10)}` | ||
const subdirName = `subdir_` + Date.now() | ||
const randomPath = `${randomParent}/${subdirName}/random_file_${Date.now()}` | ||
const fileCount = 3 + Math.floor(Math.random() * 10) | ||
let fixture | ||
beforeEach((done) => { | ||
mobiletto(driverName, config.key, config.secret, config.opts) | ||
.then(api => { fixture = {api, name: randomPath} }) | ||
.finally(done) | ||
for (const encryption of [null, {key: rand(32)}]) { | ||
const encDesc = encryption ? '(with encryption)' : '(without encryption)' | ||
describe(`${driverName} - ${encDesc} write files in a new dir, read metadata, recursively delete`, () => { | ||
// describe(`${driverName} - ENCRYPTION write files in a new dir, read metadata, recursively delete`, () => { | ||
// const encryption = {key: rand(32)} | ||
// const encryption = null | ||
// a random directory and file within it | ||
const randomParent = `testRPD_${rand(2)}/rand_${rand(4)}` | ||
const subdirName = `subdir_` + Date.now() | ||
const randomPath = `${randomParent}/${subdirName}/random_file_${Date.now()}` | ||
//const fileCount = 3 + Math.floor(Math.random() * 10) | ||
const fileCount = 2 | ||
let fixture | ||
beforeEach((done) => { | ||
mobiletto(driverName, config.key, config.secret, config.opts, encryption) | ||
.then(api => { fixture = {api, name: randomPath} }) | ||
.catch((err) => { throw err }) | ||
.finally(done) | ||
}) | ||
it(`should write ${fileCount} files in a new directory`, async () => { | ||
function* dataGenerator() { | ||
// return one chunk of random data | ||
yield rand(READ_SZ) | ||
} | ||
for (let i = 0; i < fileCount; i++) { | ||
const bytesWritten = await fixture.api.write(tempFilename(fixture.name, i), dataGenerator()) | ||
if (encryption) { | ||
closeEnough(READ_SZ, bytesWritten) | ||
} else { | ||
expect(bytesWritten).to.equal(READ_SZ, 'expected write API to return correct number of bytes written') | ||
} | ||
} | ||
}) | ||
it("should load metadata for one of the new files", async () => { | ||
await assertMeta(fixture.api, fixture.name, READ_SZ, encryption ? ENC_SIZE_CLOSE_ENOUGH_PERCENT : null) | ||
}) | ||
it("should see correct types on objects returned from a listing of the new directory", async () => { | ||
const objects = await fixture.api.list(randomParent) | ||
expect(objects).to.have.lengthOf(1) | ||
expect(objects[0]).to.have.property('type', M_DIR, `subdir should have type ${M_DIR}`) | ||
}) | ||
it("should see correct types on objects returned from a listing of the subdirectory", async () => { | ||
const objects = await fixture.api.list(`${randomParent}/${subdirName}`) | ||
expect(objects).to.have.lengthOf(fileCount) | ||
for (let i = 0; i < fileCount; i++) { | ||
// we should find all the files, and they should all have the correct type | ||
expect(objects | ||
.find(o => (o.type === M_FILE && o.name === tempFilename(fixture.name, i))) | ||
).to.not.be.null | ||
} | ||
}) | ||
it("should recursively delete the directory and file we just created", async () => { | ||
const recursive = true | ||
const removed = await fixture.api.remove(randomParent, {recursive}) | ||
expect(removed).to.be.true | ||
}) | ||
it("loading metadata on the file we wrote now fails", async () => { | ||
await assertMetaFail(fixture.api, fixture.name) | ||
}) | ||
it("loading metadata on the parent dir we wrote now fails", async () => { | ||
await assertMetaFail(fixture.api, randomParent) | ||
}) | ||
}) | ||
it(`should write ${fileCount} files in a new directory`, async () => { | ||
function* dataGenerator() { | ||
// return one chunk of random data | ||
yield rand(READ_SZ) | ||
} | ||
for (let i = 0; i < fileCount; i++) { | ||
const response = await fixture.api.write(tempFilename(fixture.name, i), dataGenerator()) | ||
expect(response).to.equal(READ_SZ, 'expected write API to return correct number of bytes written') | ||
} | ||
}) | ||
it("should load metadata for one of the new files", async () => { | ||
await assertMeta (fixture.api, fixture.name, READ_SZ) | ||
}) | ||
it("should see correct types on objects returned from a listing of the new directory", async () => { | ||
const objects = await fixture.api.list(randomParent) | ||
expect(objects).to.have.lengthOf(1) | ||
expect(objects[0]).to.have.property('type', M_DIR, `subdir should have type ${M_DIR}`) | ||
}) | ||
it("should see correct types on objects returned from a listing of the subdirectory", async () => { | ||
const objects = await fixture.api.list(`${randomParent}/${subdirName}`) | ||
expect(objects).to.have.lengthOf(fileCount) | ||
for (let i = 0; i < fileCount; i++) { | ||
// we should find all the files, and they should all have the correct type | ||
expect(objects | ||
.find(o => (o.type === M_FILE && o.name === tempFilename(fixture.name, i))) | ||
).to.not.be.null | ||
} | ||
}) | ||
it("should recursively delete the directory and file we just created", async () => { | ||
const recursive = true | ||
const removed = await fixture.api.remove(randomParent, {recursive}) | ||
expect(removed).to.be.true | ||
}) | ||
it("loading metadata on the file we wrote now fails", async () => { | ||
await assertMetaFail(fixture.api, fixture.name) | ||
}) | ||
it("loading metadata on the parent dir we wrote now fails", async () => { | ||
await assertMetaFail(fixture.api, randomParent) | ||
}) | ||
}) | ||
} | ||
@@ -246,0 +269,0 @@ describe(`${driverName} - expect MobilettoNotFoundError when reading nonexistent file `, () => { |
@@ -39,11 +39,12 @@ // adapted from https://stackoverflow.com/a/64136185 | ||
function encrypt (plainText, key = KEY, iv = CRYPTO_IV, outputEncoding = 'base64') { | ||
if (!key) { | ||
function encrypt (plainText, encryption, outputEncoding = 'base64') { | ||
if (!encryption || !encryption.key) { | ||
if (WARN_PLAINTEXT) { | ||
console.warn(` ****** key is undefined, encryption of user-data is DISABLED`) | ||
console.warn(` ****** encryption.key is undefined, encryption is DISABLED`) | ||
} | ||
return plainText | ||
} | ||
const cipher = getCipher(key, iv) | ||
return Buffer.concat([cipher.update(plainText), cipher.final()]).toString(outputEncoding) | ||
const cipher = getCipher(encryption) | ||
const encoded = Buffer.concat([cipher.update(plainText), cipher.final()]) | ||
return encoded.toString(outputEncoding) | ||
} | ||
@@ -55,8 +56,8 @@ | ||
function decrypt (cipherText, key = KEY, iv = CRYPTO_IV, outputEncoding = 'utf8') { | ||
if (!key) { | ||
function decrypt (cipherText, encryption, outputEncoding = 'utf8', inputEncoding = 'base64') { | ||
if (!encryption || !encryption.key) { | ||
return cipherText | ||
} | ||
const cipher = getDecipher(key, iv) | ||
const data = Buffer.from(cipherText, 'base64') | ||
const cipher = getDecipher(encryption) | ||
const data = Buffer.from(cipherText, inputEncoding) | ||
return Buffer.concat([cipher.update(data), cipher.final()]).toString(outputEncoding) | ||
@@ -63,0 +64,0 @@ } |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
64569
1092
151
10