Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

secure-store-redis

Package Overview
Dependencies
Maintainers
1
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

secure-store-redis - npm Package Compare versions

Comparing version 3.0.0-rc.3 to 3.0.0-rc.4

61

dist/index.d.ts
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 {};

111

dist/index.js

@@ -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();
});
});
});
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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc