Skip to content

Commit ae7b635

Browse files
committed
Basic implementation (#1)
* Basic vercel * Better vercel * refactor * Improved vercel rewrite * Added fieldlength check * Lightweight CORS. Permanent redirects * Fixed READMe and vercel.json
1 parent 746491b commit ae7b635

File tree

8 files changed

+391
-2
lines changed

8 files changed

+391
-2
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package-lock.json
2+
node_modules
3+
**/.env
4+
.vercel

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,26 @@
1-
# serverless-vercel-redis
2-
Securelay API Implementation as Serverless Function for hosting on Vercel. Uses Redis as DB. Work In Progress.
1+
# About
2+
This is a serverless, NodeJS implementation of the [Securelay API](https://github.com/securelay/specs).
3+
It uses [Redis](https://redis.io/docs/latest/commands/) as database.
4+
This particular implementation is configured to be hosted on [Vercel](https://vercel.com/pricing)
5+
out of the box. However, with a few [variations](https://fastify.dev/docs/latest/Guides/Serverless/),
6+
this implmentation may be run on any serverless platform such as [AWS Lambda](https://fastify.dev/docs/latest/Guides/Serverless/#aws),
7+
provided a Redis DB, such as [Upstash](https://upstash.com/pricing/redis), can be used.
8+
9+
# How to host on Vercel
10+
11+
### Using GUI
12+
- [Create](https://vercel.com/signup) a Vercel account.
13+
- Get Redis in the form of [Vercel's KV store](https://vercel.com/docs/storage/vercel-kv/quickstart).
14+
- Import [this](https://github.com/securelay/api-serverless-redis-vercel) project and deploy it by following [this tutorial](https://vercel.com/docs/getting-started-with-vercel/import).
15+
- Note: Before deploying, set the environment variables through Vercel's Project Settings page by following the template provided in [example.env](./example.env).
16+
17+
### Using CLI
18+
- [Create](https://vercel.com/signup) a Vercel account.
19+
- Get Redis in the form of [Vercel's KV store](https://vercel.com/docs/storage/vercel-kv/quickstart).
20+
- Set the environment variables through Vercel's Project Settings page by following the template provided in [example.env](./example.env).
21+
- Get vercel CLI: `npm i -g vercel`.
22+
- Clone this repo: `git clone https://github.com/securelay/api-serverless-redis-vercel`.
23+
- `cd api-serverless-redis-vercel`.
24+
- `vercel login`.
25+
- Deploy locally and test: `vercel dev`.
26+
- If everything is working fine deploy publicly on Vercel: `vercel`.

api/helper.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import Crypto from 'node:crypto';
2+
import { kv } from '@vercel/kv';
3+
4+
const secret = process.env.SECRET;
5+
const sigLen = parseInt(process.env.SIG_LEN);
6+
const hashLen = parseInt(process.env.HASH_LEN);
7+
const ttl = parseInt(process.env.TTL);
8+
const dbKeyPrefix = {
9+
manyToOne: "m2o:",
10+
oneToMany: "o2m:",
11+
oneToOne: "o2o:",
12+
}
13+
14+
function hash(str){
15+
return Crypto.hash('md5', str, 'base64url').substr(0,hashLen);
16+
// For small size str Crypto.hash() is faster than Crypto.createHash()
17+
}
18+
19+
function sign(str){
20+
// Note: https://nodejs.org/api/crypto.html#using-strings-as-inputs-to-cryptographic-apis
21+
return Crypto.createHmac('md5', secret).update(str).digest('base64url').substr(0,sigLen);
22+
}
23+
24+
export function validate(key){
25+
const sig = key.substr(0, sigLen);
26+
const hash = key.substr(sigLen,);
27+
if (sig === sign(hash + 'public')){
28+
return 'public';
29+
} else if (sig === sign(hash + 'private')){
30+
return 'private';
31+
} else {
32+
return false;
33+
}
34+
}
35+
36+
export function genPublicKey(privateKey){
37+
const privateHash = privateKey.substr(sigLen,);
38+
const publicHash = hash(privateHash);
39+
const publicKey = sign(publicHash + 'public') + publicHash;
40+
return publicKey
41+
}
42+
43+
export function genKeyPair(seed = Crypto.randomUUID()){
44+
const privateHash = hash(seed);
45+
const privateKey = sign(privateHash + 'private') + privateHash;
46+
const publicKey = genPublicKey(privateKey);
47+
return {private: privateKey, public: publicKey};
48+
}
49+
50+
export async function publicProduce(publicKey, data){
51+
const dbKey = dbKeyPrefix.manyToOne + publicKey;
52+
return kv.rpush(dbKey, data).then(kv.expire(dbKey, ttl));
53+
}
54+
55+
export async function privateConsume(privateKey){
56+
const publicKey = genPublicKey(privateKey);
57+
const dbKey = dbKeyPrefix.manyToOne + publicKey;
58+
const llen = await kv.llen(dbKey);
59+
if (!llen) return [];
60+
return kv.lpop(dbKey, llen);
61+
}
62+
63+
export async function privateProduce(privateKey, data){
64+
const publicKey = genPublicKey(privateKey);
65+
const dbKey = dbKeyPrefix.oneToMany + publicKey;
66+
return kv.set(dbKey, data, { ex: ttl });
67+
}
68+
69+
export async function publicConsume(publicKey){
70+
const dbKey = dbKeyPrefix.oneToMany + publicKey;
71+
return kv.get(dbKey);
72+
}
73+
74+
export async function oneToOneProduce(privateKey, key, data){
75+
const publicKey = genPublicKey(privateKey);
76+
const dbKey = dbKeyPrefix.oneToOne + publicKey;
77+
let field = {};
78+
field[key] = data;
79+
return kv.hset(dbKey, field).then(kv.expire(dbKey, ttl));
80+
}
81+
82+
export async function oneToOneConsume(publicKey, key){
83+
const dbKey = dbKeyPrefix.oneToOne + publicKey;
84+
const field = key;
85+
return kv.hget(dbKey, field).then(kv.hdel(dbKey, field));
86+
}
87+
88+
export async function oneToOneIsConsumed(privateKey, key){
89+
const publicKey = genPublicKey(privateKey);
90+
const dbKey = dbKeyPrefix.oneToOne + publicKey;
91+
const field = key;
92+
const bool = await kv.hexists(dbKey, field);
93+
if (bool) {
94+
return "Not consumed yet.";
95+
} else {
96+
return "Consumed.";
97+
}
98+
}

api/index.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import * as helper from './helper.js';
2+
import Fastify from 'fastify';
3+
4+
const bodyLimit = parseInt(process.env.BODYLIMIT);
5+
const fieldLimit = parseInt(process.env.FIELDLIMIT);
6+
7+
// Impose content-length limit
8+
const fastify = Fastify({
9+
ignoreTrailingSlash: true,
10+
bodyLimit: bodyLimit
11+
})
12+
13+
// Enable CORS. This implementation is lightweight than importing '@fastify/cors'
14+
fastify.addHook('onRequest', (request, reply, done) => {
15+
reply.headers({
16+
'Access-Control-Allow-Origin': '*',
17+
'Access-Control-Allow-Methods': 'GET,POST'
18+
})
19+
done();
20+
})
21+
22+
// Enable parser for application/x-www-form-urlencoded
23+
fastify.register(import('@fastify/formbody'))
24+
25+
const callUnauthorized = function(reply, msg){
26+
reply.code(401).send({message: msg, error: "Unauthorized", statusCode: reply.statusCode});
27+
}
28+
29+
const callBadRequest = function(reply, msg){
30+
reply.code(400).send({message: msg, error: "Bad Request", statusCode: reply.statusCode});
31+
}
32+
33+
const callInternalServerError = function(reply, msg){
34+
reply.code(500).send({message: msg, error: "Internal Server Error", statusCode: reply.statusCode});
35+
}
36+
37+
fastify.get('/', (request, reply) => {
38+
reply.redirect('https://securelay.github.io', 301);
39+
})
40+
41+
fastify.post('/', (request, reply) => {
42+
reply.redirect('https://securelay.github.io', 301);
43+
})
44+
45+
fastify.get('/keys', (request, reply) => {
46+
reply.send(helper.genKeyPair());
47+
})
48+
49+
fastify.get('/keys/:key', (request, reply) => {
50+
const { key } = request.params;
51+
const keyType = helper.validate(key);
52+
if (keyType === 'public') {
53+
reply.send({type: "public"});
54+
} else if (keyType === 'private') {
55+
reply.send({type: "private", public: helper.genPublicKey(key)});
56+
} else {
57+
reply.callNotFound();
58+
}
59+
})
60+
61+
fastify.post('/public/:publicKey', async (request, reply) => {
62+
const { publicKey } = request.params;
63+
try {
64+
if (helper.validate(publicKey) !== 'public') throw 401;
65+
await helper.publicProduce(publicKey, JSON.stringify(request.body));
66+
reply.send({message: "Done", error: "Ok", statusCode: reply.statusCode});
67+
} catch (err) {
68+
if (err == 401) {
69+
callUnauthorized(reply, 'Provided key is not Public');
70+
} else {
71+
callInternalServerError(reply, err);
72+
}
73+
}
74+
})
75+
76+
fastify.get('/private/:privateKey', async (request, reply) => {
77+
const { privateKey } = request.params;
78+
try {
79+
if (helper.validate(privateKey) !== 'private') throw 401;
80+
const dataArray = await helper.privateConsume(privateKey);
81+
if (!dataArray.length) throw 404;
82+
reply.send(dataArray);
83+
} catch (err) {
84+
if (err == 401) {
85+
callUnauthorized(reply, 'Provided key is not Private');
86+
} else if (err == 404) {
87+
reply.callNotFound();
88+
} else {
89+
callInternalServerError(reply, err);
90+
}
91+
}
92+
})
93+
94+
fastify.post('/private/:privateKey', async (request, reply) => {
95+
const { privateKey } = request.params;
96+
try {
97+
if (helper.validate(privateKey) !== 'private') throw 401;
98+
await helper.privateProduce(privateKey, JSON.stringify(request.body));
99+
reply.send({message: "Done", error: "Ok", statusCode: reply.statusCode});
100+
} catch (err) {
101+
if (err == 401) {
102+
callUnauthorized(reply, 'Provided key is not Private');
103+
} else {
104+
callInternalServerError(reply, err);
105+
}
106+
}
107+
})
108+
109+
fastify.get('/public/:publicKey', async (request, reply) => {
110+
const { publicKey } = request.params;
111+
try {
112+
if (helper.validate(publicKey) !== 'public') throw 401;
113+
const data = await helper.publicConsume(publicKey);
114+
if (!data) throw 404;
115+
reply.send(data);
116+
} catch (err) {
117+
if (err == 401) {
118+
callUnauthorized(reply, 'Provided key is not Public');
119+
} else if (err == 404) {
120+
reply.callNotFound();
121+
} else {
122+
callInternalServerError(reply, err);
123+
}
124+
}
125+
})
126+
127+
fastify.post('/private/:privateKey/:key', async (request, reply) => {
128+
const { privateKey, key } = request.params;
129+
if (key.substr(0,fieldLimit) !== key) {callBadRequest(reply, 'Provided field is too long'); return;}
130+
try {
131+
if (helper.validate(privateKey) !== 'private') throw 401;
132+
await helper.oneToOneProduce(privateKey, key, JSON.stringify(request.body));
133+
reply.send({message: "Done", error: "Ok", statusCode: reply.statusCode});
134+
} catch (err) {
135+
if (err == 401) {
136+
callUnauthorized(reply, 'Provided key is not Private');
137+
} else {
138+
callInternalServerError(reply, err);
139+
}
140+
}
141+
})
142+
143+
fastify.get('/public/:publicKey/:key', async (request, reply) => {
144+
const { publicKey, key } = request.params;
145+
if (key.substr(0,fieldLimit) !== key) {callBadRequest(reply, 'Provided field is too long'); return;}
146+
try {
147+
if (helper.validate(publicKey) !== 'public') throw 401;
148+
const data = await helper.oneToOneConsume(publicKey, key);
149+
if (!data) throw 404;
150+
reply.send(data);
151+
} catch (err) {
152+
if (err == 401) {
153+
callUnauthorized(reply, 'Provided key is not Public');
154+
} else if (err == 404) {
155+
reply.callNotFound();
156+
} else {
157+
callInternalServerError(reply, err);
158+
}
159+
}
160+
})
161+
162+
fastify.get('/private/:privateKey/:key', async (request, reply) => {
163+
const { privateKey, key } = request.params;
164+
if (key.substr(0,fieldLimit) !== key) {callBadRequest(reply, 'Provided field is too long'); return;}
165+
try {
166+
if (helper.validate(privateKey) !== 'private') throw 401;
167+
reply.send(await helper.oneToOneIsConsumed(privateKey, key));
168+
} catch (err) {
169+
if (err == 401) {
170+
callUnauthorized(reply, 'Provided key is not Private');
171+
} else {
172+
callInternalServerError(reply, err);
173+
}
174+
}
175+
})
176+
177+
export default async function handler(req, res) {
178+
await fastify.ready();
179+
fastify.server.emit('request', req, res);
180+
}

api/test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
Brief: Testing
3+
Run: node --env-file=.env test.js
4+
*/
5+
6+
import * as helper from './helper.js';
7+
8+
9+
const key = helper.genKeyPair();
10+
11+
console.log(JSON.stringify(key));
12+
13+
console.log('This should show public: ' + helper.validate(key.public));
14+
console.log('This should show private: ' + helper.validate(key.private));
15+
console.log('This should show false: ' + helper.validate('random'));
16+
17+
await helper.publicProduce(key.public, 'dataA hi"');
18+
await helper.publicProduce(key.public, 'dataB hi"');
19+
await helper.publicProduce(key.public, 'dataC hi"');
20+
21+
console.log(await helper.privateConsume(key.private));
22+
23+
await helper.privateProduce(key.private, 'data hi"');
24+
console.log(await helper.publicConsume(key.public));
25+
26+
await helper.oneToOneProduce(key.private, 'some Key', 'data "for one to one at some key');
27+
console.log(await helper.oneToOneConsume(key.public, 'some Key'));
28+
console.log(await helper.oneToOneIsConsumed(key.private, 'some Key'));
29+
30+
console.log('End of synchronous execution. Anything logged after this is from async only!')

example.env

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# This is a template for the .env file.
2+
# Edit the following values and rename this file to .env
3+
4+
# Crypto related
5+
SECRET='SomeRandomString' # Server's secret used for signing
6+
SIG_LEN=5 # Signature character count
7+
HASH_LEN=5 # Hash character count
8+
9+
# Limits related
10+
TTL=86400 # Time to live for data in seconds
11+
BODYLIMIT=10000 # Max content-length in bytes. Note: 1 byte = 1 character.
12+
FIELDLIMIT = 5 # Max length of the one-to-one token
13+
14+
# Redis credentials
15+
KV_URL=
16+
KV_REST_API_URL=
17+
KV_REST_API_TOKEN=
18+
KV_REST_API_READ_ONLY_TOKEN=

package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"dependencies": {
3+
"@fastify/cors": "^10.0.1",
4+
"@fastify/formbody": "^8.0.1",
5+
"@vercel/kv": "^2.0.0",
6+
"fastify": "^5.0.0"
7+
},
8+
"type": "module",
9+
"scripts": {
10+
"test": "node --env-file=.env api/test.js"
11+
}
12+
}

0 commit comments

Comments
 (0)