DEV Community

Cover image for What exactly is the best way to keep your API secure
Helio
Helio

Posted on

What exactly is the best way to keep your API secure

In this article, we will discuss several possible ways to protect the API from abuse. As the old saying goes, there is no such thing as a completely secure system in the world. We can't guarantee that the API won't be cracked, but we can artificially make it harder.

Let's say we have an POST request to /pay, and the body like:

{
  "amount": 100
}
Enter fullscreen mode Exit fullscreen mode

We usually make an https request and we're done. I'm sure anyone who's ever worked with a computer will know how to use it.

The next step we will weed out the 60%.

Use AES encryption

Keys: 12345678. The body becomes:

lK/I57sqpDl+2G6k88XGVbMtngdLI8raGZ0W2XWW0So=
Enter fullscreen mode Exit fullscreen mode

Looks pretty good, a lot of people don't know where to start.
Maybe the rest will press F12 and skillfully search for key, finally find the key easily: 12345678.

Is it possible to remove another 20% from the remaining 40%?

Maybe HMAC

You write the sign function in some corner of nowhere, and add the sign to header. You can implement your own set of sign logic, for example, swapping several of them after MD5, which can be unusual and make it more difficult to crack.

import axios, { AxiosRequestConfig } from 'axios';
import * as crypto from 'crypto';

// Calculate MD5 hash
function calculateMD5(data: string): string {
    return crypto.createHash('md5').update(data).digest('hex');
}

// Sort a string in lexicographical order
function sortString(str: string): string {
    return str.split('').sort().join('');
}

// Swap the front and back segments of a string
function swapSegments(str: string): string {
    const midpoint = Math.ceil(str.length / 2);
    return str.substr(midpoint) + str.substr(0, midpoint);
}

// Create an Axios instance
const instance = axios.create();

// Add request interceptor
instance.interceptors.request.use((config: AxiosRequestConfig) => {
    // Check if there is request data
    if (config.data) {
        // Convert request data to a string
        const bodyString = JSON.stringify(config.data);
        // Sort request data in lexicographical order
        const sortedBody = sortString(bodyString);
        // Calculate the MD5 hash of the sorted request data
        const md5 = calculateMD5(sortedBody);
        // Swap the front and back segments of the MD5 hash
        const swappedMD5 = swapSegments(md5);
        // Modify the request body to the swapped MD5 hash
        config.data = swappedMD5;
    }

    // Create a sign from the swapped MD5 hash
    const sign = crypto.createHash('md5').update(swappedMD5).digest('hex');
    // Add the sign to the request headers
    config.headers['Sign'] = sign;

    return config;
}, (error) => {
    return Promise.reject(error);
});

// Send a request
instance.post('https://api.example.com/data', { key1: 'value1', key2: 'value2' })
    .then((response) => {
        console.log(response.data);
    })
    .catch((error) => {
        console.error(error);
    });
Enter fullscreen mode Exit fullscreen mode

Maybe someone looked for the reason for the sign field generation and spent a lot of time and then gave up.
No matter what encryption method is used, the JavaScript code that can be read is always easy to crack for somebody.

Code obfuscation

We can use variable renaming, string encoding, control flow obfuscation, and other techniques to make it more difficult to crack, but we can't escape the debugger after all.

HOTP

One of the fun ideas that came to me was to use HOTP. We can generate a one-time key every 30s to encrypt the data. However, the retry mechanism needs to be perfected, as the request is likely to be refreshing the password boundary of the refresh key.

import axios, { AxiosRequestConfig } from 'axios';
import * as crypto from 'crypto';

// Generate HOTP key
function generateHOTPKey(): string {
    const key = crypto.randomBytes(16).toString('hex'); // Generate a random key
    return key;
}

// Encrypt data using HOTP key
function encryptData(data: any, key: string): string {
    const cipher = crypto.createCipher('aes-256-ctr', key);
    let encryptedData = cipher.update(JSON.stringify(data), 'utf8', 'hex');
    encryptedData += cipher.final('hex');
    return encryptedData;
}

// Axios middleware for generating and using HOTP key
async function hotpMiddleware(config: AxiosRequestConfig): Promise<AxiosRequestConfig> {
    // Generate a new HOTP key every 30 seconds
    const key = generateHOTPKey();

    // Encrypt POST data if present
    if (config.method?.toLowerCase() === 'post' && config.data) {
        const encryptedData = encryptData(config.data, key);
        config.data = encryptedData;
    }

    return config;
}

// Create an Axios instance with the middleware
const instance = axios.create();
instance.interceptors.request.use(hotpMiddleware);

// Send a request
instance.post('https://api.example.com/data', { key1: 'value1', key2: 'value2' })
    .then((response) => {
        console.log(response.data);
    })
    .catch((error) => {
        console.error(error);
    });
Enter fullscreen mode Exit fullscreen mode

The above does not refine the logic of generating a key in 30s, please note.

Web assembly

Using WebAssembly to compile a POST data encryption program enhances data encryption security. By encapsulating both the encryption key and logic within a WebAssembly module, it effectively deters reverse engineering and decryption attempts. Here's a breakdown:

  1. Write Encryption Logic: Develop encryption logic in languages like C/C++, employing common encryption algorithms such as AES, RSA, etc. This logic will accept data and keys, returning encrypted data.

  2. Compile to WebAssembly Module: Utilize tools like Emscripten to compile C/C++ code into a WebAssembly module. This encapsulates encryption algorithms and critical keys into a separate binary file, making them less susceptible to easy viewing or modification.

  3. Integrate into Frontend App: Load and utilize the compiled WebAssembly module within the frontend application. Interact with WebAssembly via JavaScript to pass data for encryption, sending the encrypted data to the server afterward.

  4. Protect Keys: Minimize exposing keys on the frontend. Techniques like generating keys on the server and transmitting them securely to the frontend or using one-time keys can enhance security.

  5. Monitor and Update: Regularly monitor frontend application security, updating the WebAssembly module promptly to address potential vulnerabilities or weaknesses.

Do you have a better way to secure the API?

Top comments (1)

Collapse
 
go4webdev profile image
Go4WebDev • Edited

I have played with isolate API, auth and database servers from the internet. Which means there is no direct access from the browser to the API etc. You must contact the Web server and then forward the query to the "isolated safe box" by using internal IP-addresses.

This does not mean that encryption is unnecessary, but it adds an extra layer of security...