secure-api-payload
v1.0.1
Published
Frontend API payload encryption using RSA-OAEP and AES-256-GCM
Maintainers
Readme
Secure API Payload
A lightweight browser-side JavaScript package for encrypting API request payloads before sending them to your backend.
Secure API Payload uses AES-256-GCM to encrypt the payload and RSA-OAEP with SHA-256 to encrypt the AES key.
It supports JSON payloads, FormData, and file uploads.
Note: This package provides an additional application-level encryption layer. Always use HTTPS in production.
Features
- AES-256-GCM payload encryption
- RSA-OAEP key encryption
- SHA-256
- JSON payload support
- FormData support
- File upload support
- Custom RSA public keys
- Browser Web Crypto API
- React support
- Vite support
- Next.js support
- Vue support
- Vanilla JavaScript support
Installation
npm install secure-api-payloadRSA Key Generation
Secure API Payload uses an RSA key pair:
Private Key
|
| Used by Backend
|
v
Decrypt AES Key
Public Key
|
| Used by Frontend
|
v
Encrypt AES KeyThe public key can be used in the frontend.
The private key must only be stored on the backend.
Generate Private Key
Install OpenSSL if it is not already installed.
Generate a 4096-bit RSA private key:
openssl genrsa -out private.pem 4096This creates:
private.pemExample:
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----Generate Public Key
Generate the public key from the private key:
openssl rsa \
-in private.pem \
-pubout \
-out public.pemThis creates:
public.pemExample:
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----You now have:
private.pem
public.pemKey Usage
Use the keys as follows:
Frontend
|
+---- public.pem
|
v
Encrypt AES Key
Backend
|
+---- private.pem
|
v
Decrypt AES KeyImportant
Never:
- Put
private.pemin the frontend - Put
private.pemin thepublicfolder - Put
private.peminsrc - Commit
private.pemto GitHub - Store
private.pemin localStorage - Store
private.pemin sessionStorage - Send
private.pemto the browser
Add the following to .gitignore:
\*.pem
\*.keyUsage
Configure Your Own Public Key
The package allows you to use your own RSA public key.
First generate your keys:
openssl genrsa -out private.pem 4096
openssl rsa \
-in private.pem \
-pubout \
-out public.pemOpen:
public.pemYou will see something similar to:
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
YOUR_PUBLIC_KEY_CONTENT
YOUR_PUBLIC_KEY_CONTENT
...
-----END PUBLIC KEY-----Copy the complete public key, including:
-----BEGIN PUBLIC KEY-----and:
-----END PUBLIC KEY-----Then replace:
const PUBLIC_KEY = `
-----BEGIN PUBLIC KEY-----
YOUR_PUBLIC_KEY
-----END PUBLIC KEY-----
`;with your actual public key:
const PUBLIC_KEY = `
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
YOUR_ACTUAL_PUBLIC_KEY
YOUR_ACTUAL_PUBLIC_KEY
...
-----END PUBLIC KEY-----
`;Then configure the package:
import { configureSecureAPI, APICallPayload } from 'secure-api-payload';
configureSecureAPI({
publicKey: PUBLIC_KEY,
});Using a Public Key from an Environment Variable
For Vite:
VITE_RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
YOUR_PUBLIC_KEY
-----END PUBLIC KEY-----"Then:
import { configureSecureAPI } from 'secure-api-payload';
const PUBLIC_KEY = import.meta.env.VITE_RSA_PUBLIC_KEY;
configureSecureAPI({
publicKey: PUBLIC_KEY,
});A public key is not a secret. It is safe to expose in frontend code or frontend environment variables.
JSON Payload
const payload = {
name: 'Anoop Tripathi',
email: '[email protected]',
phone: '9999999999',
};
const response = await APICallPayload('https://api.example.com/users', payload);
const data = await response.json();
console.log(data);The original JSON payload is encrypted before it is sent to the API.
POST Request
const response = await APICallPayload(
'https://api.example.com/users',
{
name: 'Anoop Tripathi',
email: '[email protected]',
},
{
method: 'POST',
}
);
const data = await response.json();PUT Request
const response = await APICallPayload(
'https://api.example.com/users/123',
{
name: 'Updated Name',
email: '[email protected]',
},
{
method: 'PUT',
}
);
const data = await response.json();PATCH Request
const response = await APICallPayload(
'https://api.example.com/users/123',
{
email: '[email protected]',
},
{
method: 'PATCH',
}
);
const data = await response.json();DELETE Request
const response = await APICallPayload(
'https://api.example.com/users/123',
{
reason: 'Account deletion',
},
{
method: 'DELETE',
}
);
const data = await response.json();Custom Headers
const response = await APICallPayload(
'https://api.example.com/users',
{
name: 'Anoop',
},
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'X-App-Version': '1.0.0',
},
}
);
const data = await response.json();FormData
const formData = new FormData();
formData.append('name', 'Anoop Tripathi');
formData.append('email', '[email protected]');
const response = await APICallPayload(
'https://api.example.com/users',
formData
);
const data = await response.json();
console.log(data);File Upload
const formData = new FormData();
formData.append('name', 'Anoop Tripathi');
formData.append('document', file);
const response = await APICallPayload(
'https://api.example.com/upload',
formData
);
const data = await response.json();
console.log(data);Authentication
Encryption does not replace authentication.
You can continue using your existing authentication mechanism.
const response = await APICallPayload(
'https://api.example.com/users',
{
name: 'Anoop',
},
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);The backend should independently validate the authentication token.
How It Works
Frontend
|
| JSON / FormData
v
Secure API Payload
|
| Generate random AES-256 key
|
| Encrypt payload using AES-256-GCM
|
| Encrypt AES key using RSA-OAEP
v
Encrypted Payload
|
| HTTPS
v
Backend
|
| RSA Private Key
|
| Decrypt AES Key
|
| Decrypt Payload
v
Original PayloadEncryption
| Purpose | Algorithm | | ------------------ | -------------- | | Payload encryption | AES-256-GCM | | AES key encryption | RSA-OAEP | | RSA hash | SHA-256 | | Cryptography API | Web Crypto API |
Backend Decryption
The backend must have the RSA private key corresponding to the public key used by the frontend. The private key must never be exposed to the frontend or committed to Git.
The backend uses Node.js built-in crypto and fs modules to decrypt the AES key and payload.
Decryption Code
import crypto from 'crypto';
import fs from 'fs';
const privateKey = fs.readFileSync('./keys/private.pem', 'utf8');
function decryptPayload(payload) {
const encryptedKey = Buffer.from(payload.key, 'base64');
const aesKey = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
encryptedKey
);
const iv = Buffer.from(payload.iv, 'base64');
const encrypted = Buffer.from(payload.data, 'base64');
const authTagLength = 16;
const authTag = encrypted.subarray(encrypted.length - authTagLength);
const ciphertext = encrypted.subarray(0, encrypted.length - authTagLength);
const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
return JSON.parse(decrypted.toString('utf8'));
}Express Example
import express from 'express';
import { decryptPayload } from './decryptPayload.js';
const app = express();
app.use(express.json({ limit: '20mb' }));
app.post('/api/test', (req, res) => {
try {
const payload = decryptPayload(req.body);
console.log('Decrypted payload:', payload);
res.json({
success: true,
payload,
});
} catch (error) {
console.error('Payload decryption failed:', error.message);
res.status(400).json({
success: false,
message: 'Invalid encrypted payload',
});
}
});
app.listen(8080, () => {
console.log('Backend running on http://localhost:8080');
});Backend Key Location
backend/
├── keys/
│ └── private.pem
├── decryptPayload.js
├── server.js
└── package.jsonAdd the following to the backend .gitignore:
*.pem
*.key
keys/
.env
.env.*Decryption Flow
Encrypted Request
|
+---- key
| |
| +--> RSA-OAEP + Private Key
| |
| v
| AES-256 Key
|
+---- iv
|
+---- data
|
+--> AES-256-GCM + AES Key + IV
|
v
Original PayloadThe decrypted result will match the normalized payload generated by the frontend. For example, a JSON request returns a structure such as:
{
type: 'json',
data: {
name: 'Anoop',
email: '[email protected]',
},
}For FormData, the backend receives the normalized structure containing type: 'form-data' and its fields.
Important: Always use HTTPS in production. Frontend payload encryption does not replace TLS, authentication, authorization, input validation, rate limiting, or other backend security controls.
Browser Network Request
Without encryption, the browser Network tab may show:
{
"name": "Anoop Tripathi",
"email": "[email protected]"
}With Secure API Payload, the application payload is encrypted before being sent.
The request body will contain encrypted values similar to:
{
"key": "encrypted-aes-key",
"iv": "random-iv",
"data": "encrypted-payload"
}HTTPS is still required. This package does not replace TLS.
Security
This package does not replace:
- HTTPS/TLS
- Authentication
- Authorization
- Input validation
- Rate limiting
- CSRF protection
- Backend security controls
Always validate and authorize requests on the backend.
Important Security Limitation
Frontend encryption does not make the frontend trusted.
A user controlling the browser can potentially:
- Inspect JavaScript
- Debug JavaScript
- Modify JavaScript
- Read data before encryption
- Hook encryption functions
- Construct custom API requests
Therefore, never rely on frontend encryption alone for:
- Authentication
- Authorization
- Permissions
- Pricing
- Business logic
- Fraud prevention
All important security controls must be implemented on the backend.
Project Structure
secure-api-payload/
│
├── .husky/
│ └── pre-commit
│
├── .vscode/
│ └── settings.json
│
├── src/
│ ├── api/
│ │ └── APICallPayload.js
│ │
│ ├── config/
│ │ └── config.js
│ │
│ ├── crypto/
│ │ ├── aes.js
│ │ ├── base64.js
│ │ ├── encrypt.js
│ │ ├── nonce.js
│ │ └── rsa.js
│ │
│ ├── payload/
│ │ └── normalize.js
│ │
│ └── index.js
│
├── .gitignore
├── .prettierignore
├── .prettierrc
├── package.json
├── package-lock.json
├── vite.config.js
└── README.mdLicense
MIT
