crypto-storage
Advanced tools
Comparing version
278
index.js
'use strict' | ||
const { EventEmitter } = require('events') | ||
const SALT_KEY = 'salt_key' | ||
const IV_KEY = 'iv_key' | ||
const PASSWORD_LENGTH = 5 | ||
const {EventEmitter} = require('events') | ||
const {crypto, utils} = require('./lib') | ||
const { | ||
SALT_KEY, | ||
IV_KEY, | ||
PASSWORD_LENGTH, | ||
NAME_LENGTH, | ||
TABLE_NAME_KEY, | ||
} = require('./constant') | ||
function CryptoStorage (password) { | ||
if (!(this instanceof CryptoStorage)) return new CryptoStorage(password) | ||
const {getDerivedKey, getIv} = crypto | ||
const { | ||
encode, | ||
decode, | ||
arrayBufferToBase64, | ||
base64ToArrayBuffer, | ||
hashString, | ||
} = utils | ||
function CryptoStorage(ctx) { | ||
if (!(this instanceof CryptoStorage)) return new CryptoStorage(ctx) | ||
EventEmitter.call(this) | ||
@@ -14,6 +28,8 @@ | ||
this._ready = false | ||
this._userPw = null | ||
this._userPassword = null | ||
this._userName = null | ||
this._storage = null | ||
// init | ||
this.open(password) | ||
this.open(ctx) | ||
} | ||
@@ -28,2 +44,3 @@ | ||
} | ||
this._storage = window.localStorage | ||
resolve() | ||
@@ -33,40 +50,97 @@ }) | ||
CryptoStorage.prototype._setPassword = async function (password) { | ||
if (!password || typeof password !== 'string' || password.length < PASSWORD_LENGTH) { | ||
throw new Error('password should be a string of 5 characters') | ||
CryptoStorage.prototype._setTableName = function setTableName(name) { | ||
const rawTable = this._storage.getItem(TABLE_NAME_KEY) | ||
const hashName = hashString(name) | ||
if (!rawTable) { | ||
this._storage.setItem(TABLE_NAME_KEY, JSON.stringify([hashName])) | ||
} else { | ||
const tableName = JSON.parse(rawTable) | ||
if (!Array.isArray(tableName)) { | ||
throw new Error( | ||
'Table name was corrupted you should clear storage and kill instances', | ||
) | ||
} else { | ||
if (tableName.includes(hashName)) { | ||
throw new Error(`Name ${name} is already used`) | ||
} else { | ||
tableName.push(hashName) | ||
} | ||
} | ||
} | ||
const bufferPW = new TextEncoder('utf-8').encode(password) | ||
this._userPw = await window.crypto.subtle.importKey( | ||
} | ||
CryptoStorage.prototype._setContext = async function setContext(ctx) { | ||
const {password, name} = ctx | ||
if (!name || typeof name !== 'string' || name.length < NAME_LENGTH) { | ||
throw new Error(`name should be a string of ${NAME_LENGTH} characters`) | ||
} else { | ||
this._setTableName(name) | ||
} | ||
if ( | ||
!password || | ||
typeof password !== 'string' || | ||
password.length < PASSWORD_LENGTH | ||
) { | ||
throw new Error(`name should be a string of ${PASSWORD_LENGTH} characters`) | ||
} | ||
const bufferName = new TextEncoder('utf-8').encode(name) | ||
this._userName = await window.crypto.subtle.importKey( | ||
'raw', | ||
bufferPW, | ||
bufferName, | ||
'PBKDF2', | ||
false, | ||
['deriveKey'] | ||
['deriveKey'], | ||
) | ||
return this._userPw | ||
const bufferPassword = new TextEncoder('utf-8').encode(password) | ||
this._userPassword = await window.crypto.subtle.importKey( | ||
'raw', | ||
bufferPassword, | ||
'PBKDF2', | ||
false, | ||
['deriveKey'], | ||
) | ||
return {password: this._userPassword, name: this._userName} | ||
} | ||
CryptoStorage.prototype.setItem = async function (key, value) { | ||
if (!this._userPw || !this._ready) throw new Error('CryptoStorage instance is not ready') | ||
if (!key || !value) throw new Error('key (String) and value (String | Array) args are required') | ||
if (typeof key !== 'string') throw new Error('key (String) arg should be a string') | ||
if (!this._userPassword || !this._userName || !this._ready) | ||
throw new Error('CryptoStorage instance is not ready') | ||
if (!key || !value) | ||
throw new Error('key (String) and value (String | Array) args are required') | ||
if (typeof key !== 'string') | ||
throw new Error('key (String) arg should be a string') | ||
const hashedKey = hashString(key) | ||
const hashedKey = hashString(key + this._userName) | ||
const bufferValue = encode(value) | ||
const iv = getIv() | ||
const algorithm = { name: 'AES-GCM', iv } | ||
const derivedKey = await getDerivedKey(this._userPw) | ||
const iv = getIv(this._storage) | ||
const algorithm = {name: 'AES-GCM', iv} | ||
const derivedKey = await getDerivedKey(this._userPassword, this._storage) | ||
const cryptoValue = await window.crypto.subtle.encrypt(algorithm, derivedKey, bufferValue) | ||
const cryptoValue = await window.crypto.subtle.encrypt( | ||
algorithm, | ||
derivedKey, | ||
bufferValue, | ||
) | ||
const formattedCryptoValue = arrayBufferToBase64(cryptoValue) | ||
window.localStorage.setItem(hashedKey, formattedCryptoValue) | ||
this.emit('data', { [key]: value }) | ||
const unEncryptedData = {[key]: value} | ||
this._storage.setItem(hashedKey, formattedCryptoValue) | ||
this.emit('data', unEncryptedData) | ||
return unEncryptedData | ||
} | ||
CryptoStorage.prototype.getItem = async function (key) { | ||
if (!this._userPw || !this._ready) throw new Error('CryptoStorage instance is not ready') | ||
if (!key || typeof key !== 'string') throw new Error('key arg (String) is required') | ||
if (!this._userPassword || !this._ready) | ||
throw new Error('CryptoStorage instance is not ready') | ||
if (!key || typeof key !== 'string') | ||
throw new Error('key arg (String) is required') | ||
const hashedKey = hashString(key) | ||
const item = window.localStorage.getItem(hashedKey) | ||
const hashedKey = hashString(key + this._userName) | ||
const item = this._storage.getItem(hashedKey) | ||
if (!item) { | ||
@@ -77,6 +151,11 @@ console.error(`key '${key}' are not store in the CryptoStorage instance`) | ||
const base64Value = base64ToArrayBuffer(item) | ||
const iv = getIv() | ||
const algorithm = { name: 'AES-GCM', iv } | ||
const derivedKey = await getDerivedKey(this._userPw) | ||
const bufferValue = await window.crypto.subtle.decrypt(algorithm, derivedKey, base64Value) | ||
const iv = getIv(this._storage) | ||
const algorithm = {name: 'AES-GCM', iv} | ||
const derivedKey = await getDerivedKey(this._userPassword, this._storage) | ||
const bufferValue = await window.crypto.subtle.decrypt( | ||
algorithm, | ||
derivedKey, | ||
base64Value, | ||
) | ||
return decode(bufferValue) | ||
@@ -86,8 +165,11 @@ } | ||
CryptoStorage.prototype.removeItem = function (key) { | ||
if (!this._userPw || !this._ready) throw new Error('CryptoStorage instance is not ready') | ||
if (!key || typeof key !== 'string') throw new Error('key arg (String) is required') | ||
if (!this._userPassword || !this._ready) | ||
throw new Error('CryptoStorage instance is not ready') | ||
if (!key || typeof key !== 'string') | ||
throw new Error('key arg (String) is required') | ||
if (key === SALT_KEY || key === IV_KEY) throw new Error('unsafe operation') | ||
const hashedKey = hashString(key) | ||
const item = window.localStorage.getItem(hashedKey) | ||
const hashedKey = hashString(key + this._userName) | ||
const item = this._storage.getItem(hashedKey) | ||
if (!item) { | ||
@@ -97,19 +179,22 @@ console.error(`key '${key}' are not store in the CryptoStorage instance`) | ||
} | ||
window.localStorage.removeItem(hashedKey) | ||
this._storage.removeItem(hashedKey) | ||
return key | ||
} | ||
CryptoStorage.prototype.open = function (password) { | ||
Promise.all([ | ||
this._checkStorage(), | ||
this._setPassword(password) | ||
]).then(() => { | ||
this._ready = true | ||
this.emit('ready', null) | ||
}).catch(error => { | ||
this.emit('ready', error) | ||
}) | ||
CryptoStorage.prototype.open = function (ctx) { | ||
this._checkStorage().catch(error => this.emit('ready', error)) | ||
this._setContext(ctx) | ||
.then(() => { | ||
this._ready = true | ||
this.emit('ready', null) | ||
}) | ||
.catch(error => { | ||
this.emit('ready', error) | ||
}) | ||
} | ||
CryptoStorage.prototype.close = function () { | ||
this._userPw = null | ||
this._userPassword = null | ||
this._ready = false | ||
@@ -120,94 +205,1 @@ this.emit('close') | ||
module.exports = CryptoStorage | ||
// utils | ||
function encode (value) { | ||
if (!value) return '' | ||
if (typeof value !== 'object') { | ||
value = { '-1': value } | ||
} | ||
return new TextEncoder('utf-8').encode(JSON.stringify(value)) | ||
} | ||
function decode (buffer) { | ||
if (!(!buffer || !buffer.constructor || buffer.constructor !== Uint8Array)) { | ||
throw new Error('buffer args (Uint8Array) is required') | ||
} | ||
const stringValue = new TextDecoder('utf-8').decode(buffer) | ||
const objectValue = JSON.parse(stringValue) | ||
if (objectValue['-1']) return objectValue['-1'] | ||
return objectValue | ||
} | ||
function generateSalt () { | ||
const salt = window.crypto.getRandomValues(new Uint8Array(8)) | ||
window.localStorage.setItem(SALT_KEY, JSON.stringify(Array.from(salt))) | ||
return salt | ||
} | ||
function getSalt () { | ||
return window.localStorage.getItem(SALT_KEY) | ||
? new Uint8Array(JSON.parse(window.localStorage.getItem(SALT_KEY))) | ||
: generateSalt() | ||
} | ||
async function getDerivedKey (pw) { | ||
const salt = getSalt() | ||
const params = { | ||
name: 'PBKDF2', | ||
hash: 'SHA-1', | ||
salt: salt, | ||
iterations: 5000 | ||
} | ||
const algorithm = { name: 'AES-GCM', length: 256 } | ||
return window.crypto.subtle.deriveKey( | ||
params, | ||
pw, | ||
algorithm, | ||
false, | ||
['encrypt', 'decrypt'] | ||
) | ||
} | ||
function generateIv () { | ||
const nonce = window.crypto.getRandomValues(new Uint8Array(16)) | ||
window.localStorage.setItem(IV_KEY, JSON.stringify(Array.from(nonce))) | ||
return nonce | ||
} | ||
function getIv () { | ||
return window.localStorage.getItem(IV_KEY) | ||
? new Uint8Array(JSON.parse(window.localStorage.getItem(IV_KEY))) | ||
: generateIv() | ||
} | ||
function arrayBufferToBase64 (buffer) { | ||
let binary = '' | ||
const bytes = new Uint8Array(buffer) | ||
const len = bytes.byteLength | ||
for (let i = 0; i < len; i++) { | ||
binary += String.fromCharCode(bytes[i]) | ||
} | ||
return window.btoa(binary) | ||
} | ||
function base64ToArrayBuffer (stringBase64) { | ||
const binary = window.atob(stringBase64) | ||
const len = binary.length | ||
const bytes = new Uint8Array(len) | ||
for (let i = 0; i < len; i++) { | ||
bytes[i] += binary.charCodeAt(i) | ||
} | ||
return bytes.buffer | ||
} | ||
function hashString (string) { | ||
let hash = 0 | ||
let i | ||
let chr | ||
if (string.length === 0) return hash.toString(16) | ||
for (i = 0; i < string.length; i++) { | ||
chr = string.charCodeAt(i) | ||
hash = ((hash << 5) - hash) + chr | ||
} | ||
return hash.toString(16) | ||
} |
{ | ||
"name": "crypto-storage", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "A light & secure way to store data in browser.", | ||
"main": "index.js", | ||
"scripts": { | ||
"test:lint": "standard", | ||
"test:unit": "browserify test.js --debug | tape-run", | ||
"test": "npm run test:lint && npm run test:unit" | ||
"lint": "eslint", | ||
"format": "prettier --write .", | ||
"test": "browserify test.js --debug | tape-run", | ||
"validate": "npm-run-all -p format lint test" | ||
}, | ||
@@ -16,10 +17,12 @@ "keywords": [ | ||
], | ||
"author": "Tony Gorez (@tonygo_)", | ||
"author": "Tony Gorez (tony-go)", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"browserify": "^16.5.0", | ||
"eslint": "^7.3.1", | ||
"eslint-config-prettier": "^6.11.0", | ||
"npm-run-all": "^4.1.5", | ||
"standard": "^13.1.0", | ||
"prettier": "2.0.5", | ||
"tape-run": "^6.0.0", | ||
"vhs-tape": "^3.2.0", | ||
"browserify": "^16.5.0" | ||
"vhs-tape": "^3.2.0" | ||
}, | ||
@@ -26,0 +29,0 @@ "repository": { |
@@ -1,2 +0,2 @@ | ||
# crypto-storage | ||
# crypto-storage | ||
@@ -9,3 +9,4 @@ ![logo][logo] | ||
**Note** : the current version is a kind of 'beta', development is still in progress ... | ||
**Note** : the current version is a kind of 'beta', development is still in | ||
progress ... | ||
@@ -15,3 +16,3 @@ ## Install | ||
``` | ||
npm install crypto-storage | ||
yarn add crypto-storage | ||
``` | ||
@@ -25,8 +26,8 @@ | ||
const CryptoStorage = require('crypto-storage') | ||
const storage = CryptoStorage('super-pw') | ||
const storage = CryptoStorage({name: 'tester', password: 'super-pw'}) | ||
storage.on('ready', async function(err) { | ||
storage.on('ready', async function (err) { | ||
if (err) throw err | ||
console.log('CryptoStorage is ready !') | ||
// know you can append and get data safely | ||
@@ -51,18 +52,25 @@ await storage.setItem('name', 'tony') | ||
#### const storage = CryptoStorage(password: String) | ||
Create a new storage. Event 'ready' should be emitted when instance will be ready. | ||
#### const storage = CryptoStorage({name: String, password: String}) | ||
#### await storage.setItem(key: String, value: String|Array|Object) | ||
Set an item in the storage. | ||
Create a new storage. Event 'ready' should be emitted when instance will be | ||
ready. | ||
#### await storage.getItem(key: String) | ||
#### await storage.setItem(key: String, value: String|Array|Object): {[key]: value} | ||
Set an item in the storage and emit a `data` and return it. | ||
#### await storage.getItem(key: String): {[key]: value} | ||
Get an item from the storage. | ||
#### storage.removeItem(key: String) | ||
Remove an item from the storage. | ||
#### storage.removeItem(key: String): String | ||
Remove an item from the storage and return the key. | ||
#### storage.close() | ||
Close access to CryptoStorage disabling set/get/removeItem | ||
#### storage.open(password :String) | ||
Open access to CryptoStorage enabling set/get/removeItem | ||
@@ -73,10 +81,14 @@ | ||
#### storage.on('ready') | ||
Emitted when the storage is ready | ||
#### storage.on('data', data) | ||
Emitted when the new data is appended to the storage | ||
#### storage.on('close') | ||
Emitted when CryptoStorage instance is closed | ||
[logo]: https://user-images.githubusercontent.com/22824417/63122825-eb526500-bfa7-11e9-9e6d-d7f8e95b361b.png | ||
[logo]: | ||
https://user-images.githubusercontent.com/22824417/63122825-eb526500-bfa7-11e9-9e6d-d7f8e95b361b.png |
40
test.js
const vhs = require('vhs-tape') | ||
const CryptoStorage = require('.') | ||
vhs('CryptoStorage opening depends on password argument', async t => { | ||
const storage = CryptoStorage('password') | ||
const storage2 = CryptoStorage('pass') | ||
vhs('CryptoStorage opening depends on context argument', async t => { | ||
const storage = CryptoStorage({name: 'g-ray', password: 'password'}) | ||
const storage2 = CryptoStorage({name: 'A', password: 'pass'}) | ||
const storage3 = CryptoStorage() | ||
const storage4 = CryptoStorage({name: 'A'}) | ||
const storage5 = CryptoStorage({password: 'my-pass'}) | ||
const storage6 = CryptoStorage({name: '', password: 'my-pass'}) | ||
storage.on('ready', err => { | ||
t.equal(err, null, 'Good password') | ||
t.equal(err, null, 'Good context') | ||
}) | ||
storage2.on('ready', err => { | ||
t.equal(err.message, 'password should be a string of 5 characters', 'Wrong password length') | ||
t.equal(!!err.message, true, 'Wrong password length') | ||
}) | ||
storage3.on('ready', err => { | ||
t.equal(err.message, 'password should be a string of 5 characters', 'No password') | ||
t.equal(!!err.message, true, 'No context') | ||
}) | ||
storage4.on('ready', err => { | ||
t.equal(!!err.message, true, 'No password in context') | ||
}) | ||
storage5.on('ready', err => { | ||
t.equal(!!err.message, true, 'No password in name') | ||
}) | ||
storage6.on('ready', err => { | ||
t.equal(!!err.message, true, 'Wrong name length') | ||
}) | ||
}) | ||
vhs('Set/get crypt items in localStorage', t => { | ||
const storage = CryptoStorage('appendDataTest') | ||
const storage = CryptoStorage({name: 'tester', password: 'appendDataTest'}) | ||
storage.on('ready', async err => { | ||
if (err) console.log(err) | ||
// string | ||
@@ -32,3 +48,3 @@ await storage.setItem('name', 'crypto-storage') | ||
// array | ||
const arrayKey = ['gray', 'braet', 'vivien'] | ||
const arrayKey = ['g-ray', 'braet', 'vivien'] | ||
await storage.setItem('friends', arrayKey) | ||
@@ -39,7 +55,11 @@ const array = await storage.getItem('friends') | ||
// object | ||
await storage.setItem('details', { age: 30, birthplace: 'neptune' }) | ||
await storage.setItem('details', {age: 30, birthplace: 'neptune'}) | ||
const object = await storage.getItem('details') | ||
t.equal(JSON.stringify(object), JSON.stringify({ age: 30, birthplace: 'neptune' }), 'object') | ||
t.equal( | ||
JSON.stringify(object), | ||
JSON.stringify({age: 30, birthplace: 'neptune'}), | ||
'object', | ||
) | ||
t.end() | ||
}) | ||
}) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
58129
7.61%12
100%353
64.19%90
16.88%7
40%