secure-store-redis
Advanced tools
Comparing version 3.0.0-rc.3 to 3.0.0-rc.4
import { RedisClientOptions, RedisClientType, RedisFunctions, RedisModules, RedisScripts } from "redis"; | ||
/** | ||
* Possible Config parameters for SecureStore constructor | ||
*/ | ||
interface SecureStoreConfig { | ||
/** | ||
* A unique ID which can be used to prefix data stored in Redis | ||
*/ | ||
uid?: string; | ||
/** | ||
* A 32 character encryption secret, it will be automatically generated if not provided | ||
*/ | ||
secret?: string; | ||
/** | ||
* Redis connect config object | ||
*/ | ||
redis: RedisClientOptions; | ||
} | ||
/** | ||
* SecureStore class | ||
* | ||
* Automatically encrypt any data saved to redis | ||
* | ||
* @export | ||
* @class SecureStore | ||
*/ | ||
export default class SecureStore { | ||
uid: string; | ||
secret: string; | ||
/** | ||
* Redis client | ||
*/ | ||
client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>; | ||
private config; | ||
constructor(uid: string, secret: string, cfg: SecureStoreConfig); | ||
quit(): Promise<void>; | ||
/** | ||
* Creates an instance of SecureStore. | ||
* | ||
* @constructor | ||
*/ | ||
constructor(cfg: SecureStoreConfig); | ||
/** | ||
* Disconnects the Redis client | ||
*/ | ||
disconnect(): Promise<void>; | ||
/** | ||
* Initializes (connects) the Redis client to the Redis server | ||
*/ | ||
init(): Promise<void>; | ||
save(key: string, data: any, postfix?: string): Promise<number>; | ||
/** | ||
* Save and encrypt arbitrary data to Redis | ||
*/ | ||
save(key: string, data: unknown, postfix?: string): Promise<number>; | ||
/** | ||
* Get and decrypt arbitrary data from Redis | ||
*/ | ||
get(key: string, postfix?: string): Promise<any>; | ||
/** | ||
* Delete arbitrary data from Redis | ||
*/ | ||
delete(key: string, postfix?: string): Promise<number>; | ||
/** | ||
* Encrypts arbitrary data, returning an encrypted string | ||
*/ | ||
private encrypt; | ||
/** | ||
* Decrypts given encrypted string, returning its arbitrary data | ||
*/ | ||
private decrypt; | ||
/** | ||
* Generate sha256 sum from given text | ||
*/ | ||
private static shasum; | ||
} | ||
export {}; |
@@ -18,47 +18,72 @@ "use strict"; | ||
const debug_1 = __importDefault(require("debug")); | ||
const ALGORITHM = "aes-256-cbc", IV_LENGTH = 16; | ||
const log = (0, debug_1.default)("secure-store-redis"); | ||
const ALGORITHM = "aes-256-cbc", IV_LENGTH = 16; | ||
/** | ||
* SecureStore class | ||
* | ||
* Automatically encrypt any data saved to redis | ||
* | ||
* @export | ||
* @class SecureStore | ||
*/ | ||
class SecureStore { | ||
constructor(uid, secret, cfg) { | ||
if (typeof uid !== "string") { | ||
throw new Error("A uid must be specified"); | ||
/** | ||
* Creates an instance of SecureStore. | ||
* | ||
* @constructor | ||
*/ | ||
constructor(cfg) { | ||
if (typeof cfg.redis !== "object") { | ||
throw new Error("Redis config must be specified"); | ||
} | ||
else if (typeof secret !== "string") { | ||
throw new Error("No secret specified"); | ||
if (typeof cfg.uid !== "undefined") { | ||
if (typeof cfg.uid !== "string") { | ||
throw new Error("If specifying a UID, it must be a string"); | ||
} | ||
} | ||
else if (secret.length !== 32) { | ||
throw new Error("Secret must be 32 char string"); | ||
else { | ||
cfg.uid = (0, crypto_1.randomBytes)(4).toString("hex"); | ||
} | ||
this.uid = uid; | ||
this.secret = secret; | ||
if (typeof cfg.secret !== "undefined") { | ||
if (typeof cfg.secret !== "string" || cfg.secret.length !== 32) { | ||
throw new Error(`If specifying a secret, it must be a 32 char string (length: ${cfg.secret.length})`); | ||
} | ||
} | ||
else { | ||
cfg.secret = (0, crypto_1.randomBytes)(16).toString("hex"); | ||
} | ||
this.config = cfg; | ||
} | ||
quit() { | ||
/** | ||
* Disconnects the Redis client | ||
*/ | ||
disconnect() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (this.client) { | ||
log("Redis client quit called"); | ||
yield this.client.quit(); | ||
try { | ||
log("Redis client disconnect called"); | ||
yield this.client.disconnect(); | ||
} | ||
catch (e) { | ||
log("Redis disconnect failed, ignoring"); | ||
} | ||
} | ||
}); | ||
} | ||
disconnect() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (this.client) { | ||
yield this.client.disconnect(); | ||
} | ||
}); | ||
} | ||
/** | ||
* Initializes (connects) the Redis client to the Redis server | ||
*/ | ||
init() { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (this.client) { | ||
return Promise.resolve(); | ||
} | ||
else { | ||
if (!this.client) { | ||
return new Promise((resolve, reject) => { | ||
const client = (0, redis_1.createClient)(this.config.redis); | ||
client.on("error", (err) => { | ||
log("error connecting", err); | ||
log("Redis connection error", err); | ||
return reject(err); | ||
}); | ||
client.connect().then(() => { | ||
log("connected"); | ||
log("Connected to Redis"); | ||
this.client = client; | ||
@@ -71,3 +96,5 @@ resolve(); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
/** | ||
* Save and encrypt arbitrary data to Redis | ||
*/ | ||
save(key, data, postfix = "") { | ||
@@ -93,5 +120,8 @@ return __awaiter(this, void 0, void 0, function* () { | ||
const hash = SecureStore.shasum(key); | ||
return this.client.HSET(this.uid + postfix, hash, data); | ||
return this.client.HSET(this.config.uid + postfix, hash, data); | ||
}); | ||
} | ||
/** | ||
* Get and decrypt arbitrary data from Redis | ||
*/ | ||
get(key, postfix = "") { | ||
@@ -105,3 +135,3 @@ return __awaiter(this, void 0, void 0, function* () { | ||
const hash = SecureStore.shasum(key); | ||
const res = yield this.client.HGET(this.uid + postfix, hash); | ||
const res = yield this.client.HGET(this.config.uid + postfix, hash); | ||
let data; | ||
@@ -113,9 +143,11 @@ if (typeof res === "string") { | ||
catch (e) { | ||
throw new Error(e); | ||
log("Failed to decrypt data"); | ||
return null; | ||
} | ||
try { | ||
data = JSON.parse(data); | ||
// eslint-disable-next-line no-empty | ||
} | ||
catch (e) { } | ||
catch (e) { | ||
log("Failed to parse dataset as JSON"); | ||
} | ||
} | ||
@@ -128,2 +160,5 @@ else { | ||
} | ||
/** | ||
* Delete arbitrary data from Redis | ||
*/ | ||
delete(key, postfix = "") { | ||
@@ -137,9 +172,11 @@ return __awaiter(this, void 0, void 0, function* () { | ||
const hash = SecureStore.shasum(key); | ||
return this.client.HDEL(this.uid + postfix, hash); | ||
return this.client.HDEL(this.config.uid + postfix, hash); | ||
}); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
/** | ||
* Encrypts arbitrary data, returning an encrypted string | ||
*/ | ||
encrypt(data) { | ||
const iv = (0, crypto_1.randomBytes)(IV_LENGTH); | ||
const cipher = (0, crypto_1.createCipheriv)(ALGORITHM, Buffer.from(this.secret), iv); | ||
const cipher = (0, crypto_1.createCipheriv)(ALGORITHM, Buffer.from(this.config.secret), iv); | ||
let encrypted = cipher.update(data); | ||
@@ -149,2 +186,5 @@ encrypted = Buffer.concat([encrypted, cipher.final()]); | ||
} | ||
/** | ||
* Decrypts given encrypted string, returning its arbitrary data | ||
*/ | ||
decrypt(encrypted) { | ||
@@ -154,3 +194,3 @@ const parts = encrypted.split(":"); | ||
const encryptedText = Buffer.from(parts.join(":"), "hex"); | ||
const decipher = (0, crypto_1.createDecipheriv)(ALGORITHM, Buffer.from(this.secret), iv); | ||
const decipher = (0, crypto_1.createDecipheriv)(ALGORITHM, Buffer.from(this.config.secret), iv); | ||
let decrypted = decipher.update(encryptedText); | ||
@@ -160,2 +200,5 @@ decrypted = Buffer.concat([decrypted, decipher.final()]); | ||
} | ||
/** | ||
* Generate sha256 sum from given text | ||
*/ | ||
static shasum(text) { | ||
@@ -162,0 +205,0 @@ const s = (0, crypto_1.createHash)("sha256"); |
@@ -17,111 +17,238 @@ "use strict"; | ||
const index_1 = __importDefault(require("./index")); | ||
describe("SecureStore", () => { | ||
let ss; | ||
const tests = [ | ||
{ | ||
desc: 'get something that does not exist', | ||
run: () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield ss.get('blahblah'); | ||
const complexObj = { | ||
foo: "bar", | ||
bad: "obj", | ||
this: true, | ||
me: { | ||
o: { | ||
me: { | ||
o: [true, false, true, null, "this", "that", true, 9], | ||
}, | ||
}, | ||
}, | ||
}; | ||
const tests = [ | ||
{ | ||
desc: "get something that does not exist", | ||
test: (store) => { | ||
return () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield store.get("blahblah"); | ||
(0, chai_1.expect)(res).to.eql(null); | ||
}) | ||
}); | ||
}, | ||
{ | ||
desc: 'save string', | ||
run: () => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss.save('foo', 'hallo'); | ||
}) | ||
}, | ||
{ | ||
desc: "save string", | ||
test: (store) => { | ||
return () => __awaiter(void 0, void 0, void 0, function* () { | ||
yield store.save("foo", "hallo"); | ||
}); | ||
}, | ||
{ | ||
desc: 'get string', | ||
run: () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield ss.get('foo'); | ||
(0, chai_1.expect)(typeof res).to.eql('string'); | ||
(0, chai_1.expect)(res).to.eql('hallo'); | ||
}) | ||
}, | ||
{ | ||
desc: "get string", | ||
test: (store) => { | ||
return () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield store.get("foo"); | ||
(0, chai_1.expect)(typeof res).to.eql("string"); | ||
(0, chai_1.expect)(res).to.eql("hallo"); | ||
}); | ||
}, | ||
{ | ||
desc: 'save object', | ||
run: () => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss.save('foo', { bar: 'baz', wang: 'bang' }); | ||
}) | ||
}, | ||
{ | ||
desc: "save object", | ||
test: (store) => { | ||
return () => __awaiter(void 0, void 0, void 0, function* () { | ||
yield store.save("foo", { bar: "baz", wang: "bang" }); | ||
}); | ||
}, | ||
{ | ||
desc: 'get object', | ||
run: () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield ss.get('foo'); | ||
(0, chai_1.expect)(typeof res).to.eql('object'); | ||
(0, chai_1.expect)(res).to.eql({ bar: 'baz', wang: 'bang' }); | ||
}) | ||
} | ||
]; | ||
describe("connect with URL", () => { | ||
beforeEach(() => __awaiter(void 0, void 0, void 0, function* () { | ||
ss = new index_1.default('secure-store-redis-tests', '823HD8DG26JA0LK1239Hgb651TWfs0j1', { | ||
}, | ||
{ | ||
desc: "get object", | ||
test: (store) => { | ||
return () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield store.get("foo"); | ||
(0, chai_1.expect)(typeof res).to.eql("object"); | ||
(0, chai_1.expect)(res).to.eql({ bar: "baz", wang: "bang" }); | ||
}); | ||
}, | ||
}, | ||
{ | ||
desc: "save complex object", | ||
test: (store) => { | ||
return () => __awaiter(void 0, void 0, void 0, function* () { | ||
yield store.save("complex", complexObj); | ||
}); | ||
}, | ||
}, | ||
{ | ||
desc: "get complex object", | ||
test: (store) => { | ||
return () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield store.get("complex"); | ||
(0, chai_1.expect)(typeof res).to.eql("object"); | ||
(0, chai_1.expect)(res).to.eql(complexObj); | ||
}); | ||
}, | ||
}, | ||
]; | ||
describe("SecureStore", () => { | ||
describe("Client get and save", () => { | ||
const ss = new index_1.default({ | ||
uid: "ssr-test", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
describe("First client", () => { | ||
for (const test of tests) { | ||
it(test.desc, test.test(ss)); | ||
} | ||
}); | ||
describe("Second client", () => { | ||
const ss2 = new index_1.default({ | ||
uid: "ssr-test2", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j2", | ||
redis: { | ||
url: 'redis://127.0.0.1:6379' | ||
} | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
it("get (wrong store)", () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield ss2.get("foo"); | ||
(0, chai_1.expect)(res).to.eql(null); | ||
})); | ||
after(() => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss2.disconnect(); | ||
})); | ||
}); | ||
after(() => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss.disconnect(); | ||
})); | ||
for (const test of tests) { | ||
it(test.desc, test.run); | ||
} | ||
it('quit', () => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss.quit(); | ||
}); | ||
describe("Invocations", () => { | ||
it("without uid", () => __awaiter(void 0, void 0, void 0, function* () { | ||
const store = new index_1.default({ | ||
secret: "dh348djgk548fks83kds8kdsfgssgjfg", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
yield store.save("foo", "hello"); | ||
(0, chai_1.expect)(yield store.get("foo")).to.eql("hello"); | ||
yield store.disconnect(); | ||
})); | ||
it('disconnect', () => __awaiter(void 0, void 0, void 0, function* () { | ||
it("no clashing", () => __awaiter(void 0, void 0, void 0, function* () { | ||
const store = new index_1.default({ | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
yield store.save("foo", "hello1"); | ||
(0, chai_1.expect)(yield store.get("foo")).to.eql("hello1"); | ||
const store2 = new index_1.default({ | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
yield store2.save("foo", "hello2"); | ||
(0, chai_1.expect)(yield store2.get("foo")).to.eql("hello2"); | ||
yield store.disconnect(); | ||
yield store2.disconnect(); | ||
})); | ||
}); | ||
describe("Long UID", () => { | ||
const ss = new index_1.default({ | ||
uid: "secure-store-redis-test1", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
describe("First client", () => { | ||
for (const test of tests) { | ||
it(test.desc, test.test(ss)); | ||
} | ||
}); | ||
describe("Second client", () => { | ||
const ss2 = new index_1.default({ | ||
uid: "secure-store-redis-test2", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j2", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
it("get (wrong store)", () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield ss2.get("foo"); | ||
(0, chai_1.expect)(res).to.eql(null); | ||
})); | ||
after(() => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss2.disconnect(); | ||
})); | ||
}); | ||
after(() => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss.disconnect(); | ||
})); | ||
}); | ||
// { | ||
// desc: 'new scope', | ||
// run: async () => { | ||
// env.mod2 = new env.Mod('secure-store-redis-tests', 'idontknowthekeyidontknowthekey12', { | ||
// host: '127.0.0.1', | ||
// port: 6379 | ||
// }); | ||
// await env.mod2.init(); | ||
// test.assertTypeAnd(env.mod2, 'object'); | ||
// test.assertTypeAnd(env.mod2.get, 'function'); | ||
// test.assertType(env.mod2.save, 'function'); | ||
// } | ||
// }, | ||
// | ||
// | ||
// { | ||
// desc: 'get (wrong secret)', | ||
// run: () => { | ||
// const res = env.mod2.get('foo'); | ||
// expect(res).to.eql(null); | ||
// } | ||
// }, | ||
// | ||
// { | ||
// desc: 'save complex object', | ||
// run: async () => { | ||
// env.complexObj = { | ||
// foo: 'bar', | ||
// bad: 'obj', | ||
// this: true, | ||
// me: { | ||
// o: { | ||
// me: { | ||
// o: [true, false, true, null, 'this', 'that', true, 9] | ||
// } | ||
// } | ||
// } | ||
// }; | ||
// await env.mod2.save('complex', env.complexObj); | ||
// test.done(); | ||
// } | ||
// }, | ||
// | ||
// { | ||
// desc: 'get complex object', | ||
// run: async () => { | ||
// const res = await env.mod2.get('complex'); | ||
// test.assertTypeAnd(res, 'object'); | ||
// test.assert(res, env.complexObj); | ||
// } | ||
// }, | ||
describe("README Example", () => { | ||
const store = new index_1.default({ | ||
uid: "myApp:store", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: "redis://localhost:6379", | ||
}, | ||
}); | ||
describe("First client", () => { | ||
it("save", () => __awaiter(void 0, void 0, void 0, function* () { | ||
yield store.save("quote", "hello world"); | ||
})); | ||
it("get", () => __awaiter(void 0, void 0, void 0, function* () { | ||
(0, chai_1.expect)(yield store.get("quote")).to.eql("hello world"); | ||
})); | ||
it("delete", () => __awaiter(void 0, void 0, void 0, function* () { | ||
(0, chai_1.expect)(yield store.delete("quote")).to.eql(1); | ||
})); | ||
it("get deleted item fails", () => __awaiter(void 0, void 0, void 0, function* () { | ||
(0, chai_1.expect)(yield store.get("quote")).to.eql(null); | ||
})); | ||
it("save", () => __awaiter(void 0, void 0, void 0, function* () { | ||
yield store.save("quote", "hello world again"); | ||
})); | ||
}); | ||
describe("Second client", () => { | ||
const ss2 = new index_1.default({ | ||
uid: "myApp:store", | ||
secret: "this is the wrong secret 32 char", | ||
redis: { | ||
url: "redis://localhost:6379", | ||
}, | ||
}); | ||
it("get (wrong store)", () => __awaiter(void 0, void 0, void 0, function* () { | ||
const res = yield ss2.get("quote"); | ||
(0, chai_1.expect)(res).to.eql(null); | ||
})); | ||
after(() => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss2.disconnect(); | ||
})); | ||
}); | ||
describe("Third client (same secret)", () => { | ||
const ss3 = new index_1.default({ | ||
uid: "myApp:store", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: "redis://localhost:6379", | ||
}, | ||
}); | ||
it("get data from another store", () => __awaiter(void 0, void 0, void 0, function* () { | ||
(0, chai_1.expect)(yield ss3.get("quote")).to.eql("hello world again"); | ||
})); | ||
after(() => __awaiter(void 0, void 0, void 0, function* () { | ||
yield ss3.disconnect(); | ||
})); | ||
}); | ||
after(() => __awaiter(void 0, void 0, void 0, function* () { | ||
yield store.disconnect(); | ||
})); | ||
}); | ||
}); | ||
//# sourceMappingURL=/index.test.js.map |
{ | ||
"name": "secure-store-redis", | ||
"version": "3.0.0-rc.3", | ||
"version": "3.0.0-rc.4", | ||
"description": "A simple wrapper to encrypt and decrypt data stored in Redis", | ||
@@ -64,5 +64,5 @@ "license": "MIT", | ||
"lint": "prettier --check . && eslint --max-warnings 0 .", | ||
"lint:fix": "prettier --write . && eslint --max-warnings 0 --fix .", | ||
"lint:fix": "prettier --write .", | ||
"build": "tsc" | ||
} | ||
} |
@@ -7,35 +7,29 @@ # secure-store-redis | ||
**NOTE** version `2.x` is a rewrite in TypeScript, using async functions, and is | ||
backwards incompatible with `1.x` | ||
```javascript | ||
const SecureStore = require("secure-store-redis").default; | ||
const store = new SecureStore( | ||
"myApp:store", | ||
"823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
{ | ||
redis: { | ||
host: "localhost", | ||
port: 6379, // optional | ||
// optionally use the 'url' property to specify entire redis connect string | ||
// url: 'redis://localhost:6379', | ||
}, // optional | ||
const store = new SecureStore({ | ||
uid: "myApp:store", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: 'redis://localhost:6379', | ||
} | ||
); | ||
)); | ||
await store.init(); | ||
await store.save("quote", "i see dead people"); | ||
await store.save("quote", "hello world"); | ||
let res = await store.get("quote"); | ||
// res: 'i see dead people' | ||
// res: 'hello world' | ||
const num = await store.delete("quote"); | ||
// num: 1 | ||
let res = await store.get("quote"); | ||
// res: null | ||
const num = await store.delete("quote"); | ||
// num: 1 | ||
await store.save("quote", "hello world again"); | ||
await store.save("quote", "i see dead people again"); | ||
const otherStore = new SecureStore("myApp:store", "this is the wrong secret", { | ||
host: "127.0.0.1", | ||
port: 6379, | ||
const otherStore = new SecureStore({ | ||
uid: "myApp:store", | ||
secret: "this is the wrong secret 32 char", | ||
redis: { | ||
url: 'redis://localhost:6379', | ||
} | ||
}); | ||
@@ -45,3 +39,3 @@ await otherStore.init(); | ||
let res = await otherStore.get("quote"); | ||
// res: undefined | ||
// res: null | ||
``` |
@@ -1,126 +0,269 @@ | ||
import {expect} from "chai"; | ||
import { expect } from "chai"; | ||
import SecureStore from "./index"; | ||
const complexObj = { | ||
foo: "bar", | ||
bad: "obj", | ||
this: true, | ||
me: { | ||
o: { | ||
me: { | ||
o: [true, false, true, null, "this", "that", true, 9], | ||
}, | ||
}, | ||
}, | ||
}; | ||
const tests = [ | ||
{ | ||
desc: "get something that does not exist", | ||
test: (store: SecureStore) => { | ||
return async () => { | ||
const res = await store.get("blahblah"); | ||
expect(res).to.eql(null); | ||
}; | ||
}, | ||
}, | ||
describe("SecureStore", () => { | ||
let ss: SecureStore; | ||
{ | ||
desc: "save string", | ||
test: (store: SecureStore) => { | ||
return async () => { | ||
await store.save("foo", "hallo"); | ||
}; | ||
}, | ||
}, | ||
const tests = [ | ||
{ | ||
desc: 'get something that does not exist', | ||
run: async () => { | ||
const res = await ss.get('blahblah'); | ||
expect(res).to.eql(null); | ||
} | ||
desc: "get string", | ||
test: (store: SecureStore) => { | ||
return async () => { | ||
const res = await store.get("foo"); | ||
expect(typeof res).to.eql("string"); | ||
expect(res).to.eql("hallo"); | ||
}; | ||
}, | ||
}, | ||
{ | ||
desc: 'save string', | ||
run: async () => { | ||
await ss.save('foo', 'hallo'); | ||
} | ||
desc: "save object", | ||
test: (store: SecureStore) => { | ||
return async () => { | ||
await store.save("foo", { bar: "baz", wang: "bang" }); | ||
}; | ||
}, | ||
}, | ||
{ | ||
desc: 'get string', | ||
run: async () => { | ||
const res = await ss.get('foo'); | ||
expect(typeof res).to.eql('string'); | ||
expect(res).to.eql('hallo'); | ||
} | ||
desc: "get object", | ||
test: (store: SecureStore) => { | ||
return async () => { | ||
const res = await store.get("foo"); | ||
expect(typeof res).to.eql("object"); | ||
expect(res).to.eql({ bar: "baz", wang: "bang" }); | ||
}; | ||
}, | ||
}, | ||
{ | ||
desc: 'save object', | ||
run: async () => { | ||
await ss.save('foo', { bar: 'baz', wang: 'bang' }); | ||
} | ||
desc: "save complex object", | ||
test: (store: SecureStore) => { | ||
return async () => { | ||
await store.save("complex", complexObj); | ||
}; | ||
}, | ||
}, | ||
{ | ||
desc: 'get object', | ||
run: async () => { | ||
const res = await ss.get('foo'); | ||
expect(typeof res).to.eql('object'); | ||
expect(res).to.eql({ bar: 'baz', wang: 'bang' }); | ||
} | ||
} | ||
]; | ||
desc: "get complex object", | ||
test: (store: SecureStore) => { | ||
return async () => { | ||
const res = await store.get("complex"); | ||
expect(typeof res).to.eql("object"); | ||
expect(res).to.eql(complexObj); | ||
}; | ||
}, | ||
}, | ||
]; | ||
describe("connect with URL", () => { | ||
beforeEach(async () => { | ||
ss = new SecureStore('secure-store-redis-tests', '823HD8DG26JA0LK1239Hgb651TWfs0j1', { | ||
redis: { | ||
url: 'redis://127.0.0.1:6379' | ||
} | ||
describe("SecureStore", () => { | ||
describe("Client get and save", () => { | ||
const ss = new SecureStore({ | ||
uid: "ssr-test", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
}); | ||
for (const test of tests) { | ||
it(test.desc, test.run); | ||
} | ||
describe("First client", () => { | ||
for (const test of tests) { | ||
it(test.desc, test.test(ss)); | ||
} | ||
}); | ||
it('quit', async () => { | ||
await ss.quit(); | ||
describe("Second client", () => { | ||
const ss2 = new SecureStore({ | ||
uid: "ssr-test2", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j2", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
it("get (wrong store)", async () => { | ||
const res = await ss2.get("foo"); | ||
expect(res).to.eql(null); | ||
}); | ||
after(async () => { | ||
await ss2.disconnect(); | ||
}); | ||
}); | ||
after(async () => { | ||
await ss.disconnect(); | ||
}); | ||
}); | ||
it('disconnect', async () => { | ||
await ss.disconnect(); | ||
describe("Invocations", () => { | ||
it("without uid", async () => { | ||
const store = new SecureStore({ | ||
secret: "dh348djgk548fks83kds8kdsfgssgjfg", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
await store.save("foo", "hello"); | ||
expect(await store.get("foo")).to.eql("hello"); | ||
await store.disconnect(); | ||
}); | ||
it("no clashing", async () => { | ||
const store = new SecureStore({ | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
await store.save("foo", "hello1"); | ||
expect(await store.get("foo")).to.eql("hello1"); | ||
const store2 = new SecureStore({ | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
await store2.save("foo", "hello2"); | ||
expect(await store2.get("foo")).to.eql("hello2"); | ||
await store.disconnect(); | ||
await store2.disconnect(); | ||
}); | ||
}); | ||
}); | ||
describe("Long UID", () => { | ||
const ss = new SecureStore({ | ||
uid: "secure-store-redis-test1", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
// { | ||
// desc: 'new scope', | ||
// run: async () => { | ||
// env.mod2 = new env.Mod('secure-store-redis-tests', 'idontknowthekeyidontknowthekey12', { | ||
// host: '127.0.0.1', | ||
// port: 6379 | ||
// }); | ||
// await env.mod2.init(); | ||
// test.assertTypeAnd(env.mod2, 'object'); | ||
// test.assertTypeAnd(env.mod2.get, 'function'); | ||
// test.assertType(env.mod2.save, 'function'); | ||
// } | ||
// }, | ||
// | ||
// | ||
// { | ||
// desc: 'get (wrong secret)', | ||
// run: () => { | ||
// const res = env.mod2.get('foo'); | ||
// expect(res).to.eql(null); | ||
// } | ||
// }, | ||
// | ||
// { | ||
// desc: 'save complex object', | ||
// run: async () => { | ||
// env.complexObj = { | ||
// foo: 'bar', | ||
// bad: 'obj', | ||
// this: true, | ||
// me: { | ||
// o: { | ||
// me: { | ||
// o: [true, false, true, null, 'this', 'that', true, 9] | ||
// } | ||
// } | ||
// } | ||
// }; | ||
// await env.mod2.save('complex', env.complexObj); | ||
// test.done(); | ||
// } | ||
// }, | ||
// | ||
// { | ||
// desc: 'get complex object', | ||
// run: async () => { | ||
// const res = await env.mod2.get('complex'); | ||
// test.assertTypeAnd(res, 'object'); | ||
// test.assert(res, env.complexObj); | ||
// } | ||
// }, | ||
describe("First client", () => { | ||
for (const test of tests) { | ||
it(test.desc, test.test(ss)); | ||
} | ||
}); | ||
describe("Second client", () => { | ||
const ss2 = new SecureStore({ | ||
uid: "secure-store-redis-test2", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j2", | ||
redis: { | ||
url: "redis://127.0.0.1:6379", | ||
}, | ||
}); | ||
it("get (wrong store)", async () => { | ||
const res = await ss2.get("foo"); | ||
expect(res).to.eql(null); | ||
}); | ||
after(async () => { | ||
await ss2.disconnect(); | ||
}); | ||
}); | ||
after(async () => { | ||
await ss.disconnect(); | ||
}); | ||
}); | ||
describe("README Example", () => { | ||
const store = new SecureStore({ | ||
uid: "myApp:store", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: "redis://localhost:6379", | ||
}, | ||
}); | ||
describe("First client", () => { | ||
it("save", async () => { | ||
await store.save("quote", "hello world"); | ||
}); | ||
it("get", async () => { | ||
expect(await store.get("quote")).to.eql("hello world"); | ||
}); | ||
it("delete", async () => { | ||
expect(await store.delete("quote")).to.eql(1); | ||
}); | ||
it("get deleted item fails", async () => { | ||
expect(await store.get("quote")).to.eql(null); | ||
}); | ||
it("save", async () => { | ||
await store.save("quote", "hello world again"); | ||
}); | ||
}); | ||
describe("Second client", () => { | ||
const ss2 = new SecureStore({ | ||
uid: "myApp:store", | ||
secret: "this is the wrong secret 32 char", | ||
redis: { | ||
url: "redis://localhost:6379", | ||
}, | ||
}); | ||
it("get (wrong store)", async () => { | ||
const res = await ss2.get("quote"); | ||
expect(res).to.eql(null); | ||
}); | ||
after(async () => { | ||
await ss2.disconnect(); | ||
}); | ||
}); | ||
describe("Third client (same secret)", () => { | ||
const ss3 = new SecureStore({ | ||
uid: "myApp:store", | ||
secret: "823HD8DG26JA0LK1239Hgb651TWfs0j1", | ||
redis: { | ||
url: "redis://localhost:6379", | ||
}, | ||
}); | ||
it("get data from another store", async () => { | ||
expect(await ss3.get("quote")).to.eql("hello world again"); | ||
}); | ||
after(async () => { | ||
await ss3.disconnect(); | ||
}); | ||
}); | ||
after(async () => { | ||
await store.disconnect(); | ||
}); | ||
}); | ||
}); |
331
src/index.ts
import { | ||
randomBytes, | ||
createCipheriv, | ||
createDecipheriv, | ||
createHash, | ||
randomBytes, | ||
createCipheriv, | ||
createDecipheriv, | ||
createHash, | ||
BinaryLike, | ||
} from "crypto"; | ||
import { | ||
createClient, | ||
RedisClientOptions, | ||
RedisClientType, | ||
RedisFunctions, | ||
RedisModules, | ||
RedisScripts, | ||
createClient, | ||
RedisClientOptions, | ||
RedisClientType, | ||
RedisFunctions, | ||
RedisModules, | ||
RedisScripts, | ||
} from "redis"; | ||
import debug from "debug"; | ||
const ALGORITHM = "aes-256-cbc", | ||
IV_LENGTH = 16; | ||
const log = debug("secure-store-redis"); | ||
const ALGORITHM = "aes-256-cbc", | ||
IV_LENGTH = 16; | ||
/** | ||
* Possible Config parameters for SecureStore constructor | ||
*/ | ||
interface SecureStoreConfig { | ||
redis: RedisClientOptions; | ||
/** | ||
* A unique ID which can be used to prefix data stored in Redis | ||
*/ | ||
uid?: string; | ||
/** | ||
* A 32 character encryption secret, it will be automatically generated if not provided | ||
*/ | ||
secret?: string; | ||
/** | ||
* Redis connect config object | ||
*/ | ||
redis: RedisClientOptions; | ||
} | ||
/** | ||
* SecureStore class | ||
* | ||
* Automatically encrypt any data saved to redis | ||
* | ||
* @export | ||
* @class SecureStore | ||
*/ | ||
export default class SecureStore { | ||
uid: string; | ||
secret: string; | ||
client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>; | ||
private config: SecureStoreConfig; | ||
/** | ||
* Redis client | ||
*/ | ||
client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>; | ||
private config: Required<SecureStoreConfig>; | ||
constructor(uid: string, secret: string, cfg: SecureStoreConfig) { | ||
if (typeof uid !== "string") { | ||
throw new Error("A uid must be specified"); | ||
} else if (typeof secret !== "string") { | ||
throw new Error("No secret specified"); | ||
} else if (secret.length !== 32) { | ||
throw new Error("Secret must be 32 char string"); | ||
/** | ||
* Creates an instance of SecureStore. | ||
* | ||
* @constructor | ||
*/ | ||
constructor(cfg: SecureStoreConfig) { | ||
if (typeof cfg.redis !== "object") { | ||
throw new Error("Redis config must be specified"); | ||
} | ||
if (typeof cfg.uid !== "undefined") { | ||
if (typeof cfg.uid !== "string") { | ||
throw new Error("If specifying a UID, it must be a string"); | ||
} | ||
} else { | ||
cfg.uid = randomBytes(4).toString("hex"); | ||
} | ||
if (typeof cfg.secret !== "undefined") { | ||
if (typeof cfg.secret !== "string" || cfg.secret.length !== 32) { | ||
throw new Error( | ||
`If specifying a secret, it must be a 32 char string (length: ${cfg.secret.length})`, | ||
); | ||
} | ||
} else { | ||
cfg.secret = randomBytes(16).toString("hex"); | ||
} | ||
this.config = cfg as Required<SecureStoreConfig>; | ||
} | ||
this.uid = uid; | ||
this.secret = secret; | ||
this.config = cfg; | ||
} | ||
async quit(): Promise<void> { | ||
if (this.client) { | ||
await this.client.quit(); | ||
/** | ||
* Disconnects the Redis client | ||
*/ | ||
async disconnect(): Promise<void> { | ||
if (this.client) { | ||
log("Redis client quit called"); | ||
await this.client.quit(); | ||
try { | ||
log("Redis client disconnect called"); | ||
await this.client.disconnect(); | ||
} catch (e) { | ||
log("Redis disconnect failed, ignoring"); | ||
} | ||
} | ||
} | ||
} | ||
async disconnect(): Promise<void> { | ||
if (this.client) { | ||
await this.client.disconnect(); | ||
/** | ||
* Initializes (connects) the Redis client to the Redis server | ||
*/ | ||
async init(): Promise<void> { | ||
if (!this.client) { | ||
return new Promise((resolve, reject) => { | ||
const client = createClient(this.config.redis); | ||
client.on("error", (err) => { | ||
log("Redis connection error", err); | ||
return reject(err); | ||
}); | ||
client.connect().then(() => { | ||
log("Connected to Redis"); | ||
this.client = client; | ||
resolve(); | ||
}); | ||
}); | ||
} | ||
} | ||
} | ||
async init(): Promise<void> { | ||
if (this.client) { | ||
return Promise.resolve(); | ||
} else { | ||
return new Promise((resolve, reject) => { | ||
const client = createClient(this.config.redis); | ||
client.on("error", (err) => { | ||
log("error connecting", err); | ||
return reject(err); | ||
}); | ||
client.connect().then(() => { | ||
log("connected"); | ||
this.client = client; | ||
resolve(); | ||
}); | ||
}); | ||
} | ||
} | ||
/** | ||
* Save and encrypt arbitrary data to Redis | ||
*/ | ||
async save(key: string, data: unknown, postfix = "") { | ||
if (typeof key !== "string") { | ||
throw new Error("No hash key specified"); | ||
} else if (!data) { | ||
throw new Error("No data provided, nothing to save"); | ||
} | ||
postfix = postfix ? ":" + postfix : ""; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
async save(key: string, data: any, postfix = "") { | ||
if (typeof key !== "string") { | ||
throw new Error("No hash key specified"); | ||
} else if (!data) { | ||
throw new Error("No data provided, nothing to save"); | ||
} | ||
postfix = postfix ? ":" + postfix : ""; | ||
if (typeof data === "object") { | ||
try { | ||
data = JSON.stringify(data); | ||
} catch (e) { | ||
throw new Error(e); | ||
} | ||
} | ||
if (typeof data === "object") { | ||
try { | ||
data = JSON.stringify(data); | ||
} catch (e) { | ||
throw new Error(e); | ||
} | ||
await this.init(); | ||
data = this.encrypt(data); | ||
const hash = SecureStore.shasum(key); | ||
return this.client.HSET( | ||
this.config.uid + postfix, | ||
hash, | ||
data as Buffer, | ||
); | ||
} | ||
await this.init(); | ||
data = this.encrypt(data); | ||
const hash = SecureStore.shasum(key); | ||
return this.client.HSET(this.uid + postfix, hash, data); | ||
} | ||
/** | ||
* Get and decrypt arbitrary data from Redis | ||
*/ | ||
async get(key: string, postfix = "") { | ||
if (typeof key !== "string") { | ||
throw new Error("No hash key specified"); | ||
} | ||
postfix = postfix ? ":" + postfix : ""; | ||
async get(key: string, postfix = "") { | ||
if (typeof key !== "string") { | ||
throw new Error("No hash key specified"); | ||
} | ||
postfix = postfix ? ":" + postfix : ""; | ||
await this.init(); | ||
const hash = SecureStore.shasum(key); | ||
const res = await this.client.HGET(this.config.uid + postfix, hash); | ||
let data; | ||
if (typeof res === "string") { | ||
try { | ||
data = this.decrypt(res); | ||
} catch (e) { | ||
log("Failed to decrypt data"); | ||
return null; | ||
} | ||
await this.init(); | ||
const hash = SecureStore.shasum(key); | ||
const res = await this.client.HGET(this.uid + postfix, hash); | ||
let data; | ||
if (typeof res === "string") { | ||
try { | ||
data = this.decrypt(res); | ||
} catch (e) { | ||
throw new Error(e); | ||
} | ||
try { | ||
data = JSON.parse(data); | ||
// eslint-disable-next-line no-empty | ||
} catch (e) {} | ||
} else { | ||
data = res; | ||
try { | ||
data = JSON.parse(data); | ||
} catch (e) { | ||
log("Failed to parse dataset as JSON"); | ||
} | ||
} else { | ||
data = res; | ||
} | ||
return data; | ||
} | ||
return data; | ||
} | ||
async delete(key: string, postfix = "") { | ||
if (typeof key !== "string") { | ||
throw new Error("No hash key specified"); | ||
/** | ||
* Delete arbitrary data from Redis | ||
*/ | ||
async delete(key: string, postfix = "") { | ||
if (typeof key !== "string") { | ||
throw new Error("No hash key specified"); | ||
} | ||
postfix = postfix ? ":" + postfix : ""; | ||
await this.init(); | ||
const hash = SecureStore.shasum(key); | ||
return this.client.HDEL(this.config.uid + postfix, hash); | ||
} | ||
postfix = postfix ? ":" + postfix : ""; | ||
await this.init(); | ||
const hash = SecureStore.shasum(key); | ||
return this.client.HDEL(this.uid + postfix, hash); | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
private encrypt(data: any): string { | ||
const iv = randomBytes(IV_LENGTH); | ||
const cipher = createCipheriv(ALGORITHM, Buffer.from(this.secret), iv); | ||
let encrypted = cipher.update(data); | ||
/** | ||
* Encrypts arbitrary data, returning an encrypted string | ||
*/ | ||
private encrypt(data: unknown): string { | ||
const iv = randomBytes(IV_LENGTH); | ||
const cipher = createCipheriv( | ||
ALGORITHM, | ||
Buffer.from(this.config.secret), | ||
iv, | ||
); | ||
let encrypted = cipher.update(data as BinaryLike); | ||
encrypted = Buffer.concat([encrypted, cipher.final()]); | ||
return iv.toString("hex") + ":" + encrypted.toString("hex"); | ||
} | ||
encrypted = Buffer.concat([encrypted, cipher.final()]); | ||
return iv.toString("hex") + ":" + encrypted.toString("hex"); | ||
} | ||
private decrypt(encrypted: string): string { | ||
const parts = encrypted.split(":"); | ||
const iv = Buffer.from(parts.shift(), "hex"); | ||
const encryptedText = Buffer.from(parts.join(":"), "hex"); | ||
const decipher = createDecipheriv(ALGORITHM, Buffer.from(this.secret), iv); | ||
let decrypted = decipher.update(encryptedText); | ||
/** | ||
* Decrypts given encrypted string, returning its arbitrary data | ||
*/ | ||
private decrypt(encrypted: string): string { | ||
const parts = encrypted.split(":"); | ||
const iv = Buffer.from(parts.shift(), "hex"); | ||
const encryptedText = Buffer.from(parts.join(":"), "hex"); | ||
const decipher = createDecipheriv( | ||
ALGORITHM, | ||
Buffer.from(this.config.secret), | ||
iv, | ||
); | ||
let decrypted = decipher.update(encryptedText); | ||
decrypted = Buffer.concat([decrypted, decipher.final()]); | ||
return decrypted.toString(); | ||
} | ||
decrypted = Buffer.concat([decrypted, decipher.final()]); | ||
return decrypted.toString(); | ||
} | ||
private static shasum(text: string): string { | ||
const s = createHash("sha256"); | ||
s.update(text); | ||
return s.digest("hex"); | ||
} | ||
/** | ||
* Generate sha256 sum from given text | ||
*/ | ||
private static shasum(text: string): string { | ||
const s = createHash("sha256"); | ||
s.update(text); | ||
return s.digest("hex"); | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
48655
980
40
1