Skip to content

Instantly share code, notes, and snippets.

@pmNiko
Last active May 8, 2024 14:27
Show Gist options
  • Save pmNiko/7657320a20a375c59ee86a92ee230a56 to your computer and use it in GitHub Desktop.
Save pmNiko/7657320a20a375c59ee86a92ee230a56 to your computer and use it in GitHub Desktop.
Enciptación asimétrica React CSR

RSA - Enciptación de datos entre cliente y servidor

---

Note

Objectivo:

  • Enviar las consultas cifradas al server.
  • El mensaaje solo debe ser decifrado en el server.
  • La clave pública RSA debe ser solicitada en tiempo de ejecución.
  • Las clave RSA deben ser generadas en el server.
  • La clave AES-Key se debe generar en el client de manera dinámica.



Diagrama

Caution

Esto es solo una idea de como podria solucionarse esta problemática. Un mejor enfoque seria usar algun framework como Next JS, Remix, etc que nos permitiera realizar SSR por los cual la mayor parte de las consultas se quedarian del lado del servidor. Dicho esto pasemos a la implementación de ejemplo.


Tip

Client:

  • Vite -> React Project + TS
  • "crypto-js": "4.2.0" // Enciptado simétrico
  • "jsencrypt": "3.3.2" // Encriptado asimétrico

Server:

  • "crypto-js": "4.2.0" // Enciptado simétrico
  • "js-encrypt": "2.3.4" // Encriptado asimétrico

Consideraciones
  • Con esto en mente se propone la siguiente solución.
  • Tanto en el cliente como el servidor la implementación de encriptado y desencriptado debe ser análoga para poder revertir el proceso de cifrado y recuperar el mensaje original.
Implentación en el server
// Enciptación simétrica
const CryptoJS = require('crypto-js');

 const simetricEncrypt = (data, AESKey) => {
  return CryptoJS.AES.encrypt(data, AESKey).toString();
}


const simetricDecrypt = (hash, AESKey) => {
  const bytes = CryptoJS.AES.decrypt(hash, AESKey);
  return bytes.toString(CryptoJS.enc.Utf8);
}

// Enciptación asimétrica
const {generateKeyPairSync} = require('node:crypto')
const { JSEncrypt } = require('js-encrypt')

const { publicKey: PUBLIC_KEY, privateKey: PRIVATE_KEY } = generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: {
    type: 'spki',
    format: 'pem'
  },
  privateKeyEncoding: {
    type: 'pkcs8',
    format: 'pem'
  }
});

const Encrypter = new JSEncrypt()

const asimetricEncrypt = ( data ) => {
  Encrypter.setPublicKey(PUBLIC_KEY)
  return Encrypter.encrypt(JSON.stringify(data));
}

const asimetricDecrypt = ( hash ) => {
  Encrypter.setPrivateKey(PRIVATE_KEY)
  return Encrypter.decrypt(hash);
}
  • server

```js
import cors from 'cors';
import express from 'express';
import { PRIVATE_KEY, PUBLIC_KEY, asimetricDecrypt } from './JsEncrypt.js';
import { simetricDecrypt } from './simetricEncyption.js';

const app = express();

app.use(express.json());

app.use(express.urlencoded({ extended: false }));

app.use(cors());

app.post('/cipher-key', (_ , res) => {
  res.json({keyEncode: PUBLIC_KEY})
})

app.post('/encoded', (req , res) => {
  try {
    const {payload, encoded} = req.body;
  
    const AES_KEY = asimetricDecrypt(encoded, PRIVATE_KEY);
    
    const decrypted = simetricDecrypt(payload, AES_KEY);

    const {target, params} = JSON.parse(decrypted)
    
    const query = JSON.stringify({target, params});

    console.log(query);
  
    res.json({success: true})
  } catch (error) {
    console.log(error);
    res.status(502).json({error})
  }
})


app.listen(3000, () => {
  console.log('Server on port 3000');
});



Implentación en el cliente
// Enciptación simétrica
import CryptoJS from 'crypto-js'

const AES_KEY_OPTIONS = {
  nBytes: 128/8,
  keySize: 512/32,
  iterations: 2,
}

export const generateAESKey = (passord: string) => {
  const salt = CryptoJS.lib.WordArray.random(AES_KEY_OPTIONS.nBytes);
  
  const AES_KEY = CryptoJS.PBKDF2(passord, salt, { 
    keySize: AES_KEY_OPTIONS.keySize, 
    iterations: AES_KEY_OPTIONS.iterations 
  });    
  
  return AES_KEY.toString(CryptoJS.enc.Base64);
}

export  const simetricEncrypt = (data:string, AESKey:string) => {
  return CryptoJS.AES.encrypt(data, AESKey).toString();
}


export const simetricDecrypt = (hash:string, AESKey:string) => {
  const bytes = CryptoJS.AES.decrypt(hash, AESKey);
  return bytes.toString(CryptoJS.enc.Utf8);
}

// Enciptación asimétrica
import { JSEncrypt } from 'jsencrypt'

const Encrypter = new JSEncrypt()

export const asimetricEncrypt = ( AES_KEY: string, keyEncode: string ) => {
  Encrypter.setPublicKey(keyEncode);
  return Encrypter.encrypt(AES_KEY);
}
import { useEffect, useId, useState } from 'react';
import { generateAESKey, simetricEncrypt } from './libs/AES_KEY';
import {  asimetricEncrypt } from './libs';
import './Example.css'

const requestHttp = async (endpoint: string, data = {}) => {
  const response = await fetch(`http://localhost:3000/${endpoint}`, { 
    method: 'POST', 
    headers: { 'Content-Type': 'application/json'},
    body: JSON.stringify(data)
  })
  
  return await response.json();
}

export const Example = () => {
    const id = useId()
    const [aesKey, setAesKey] = useState('');
    const [cipherKey, setCipherKey] = useState('');
    const [success, setSuccess] = useState(null);

    const startService = async () => {
      setAesKey(generateAESKey(id));  
      const result = await requestHttp('cipher-key');
      setCipherKey(result.keyEncode)
    }

    const sendData = async (data: {}) => {
      const result = await requestHttp('encoded', data)
      setSuccess(result.success)
    }

    const onsubmit = (e: any) => {
      e.preventDefault();
      const params = {
          target: "login-user",
          params: {
              username: e.target.username.value, 
              password: e.target.password.value,
              fecha: new Date().toLocaleDateString(),
          }
      }
   
      const payload = simetricEncrypt(JSON.stringify(params), aesKey)
      const encoded = asimetricEncrypt(aesKey, cipherKey)

      sendData({payload, encoded})
    }


    useEffect(() => {
      startService();
    }, []);



    return (
      <div className="login-box">
          {
            success == null 
              ? <></>
              : success 
                ? <p className='success'>Logged in successfully</p> 
                : <p className='error' >An error occurred</p> 
          }
          <h2>Login</h2>
          <form  onSubmit={onsubmit} >
            <div className="user-box">
              <input type="text" placeholder='' name="username" required/>
              <label>Username</label>
            </div>
            <div className="user-box">
              <input type="password" name="password" required/>
              <label>Password</label>
            </div>

            <button type='submit'>
              <span></span>
              <span></span>
              <span></span>
              <span></span>
              Submit
            </button>
          </form>
      </div>
    );  
}
html {
  height: 100%;
}
body {
  margin:0;
  padding:0;
  font-family: sans-serif;
  background: linear-gradient(#141e30, #243b55);
}

.login-box {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 400px;
  padding: 40px;
  transform: translate(-50%, -50%);
  background: rgba(0,0,0,.5);
  box-sizing: border-box;
  box-shadow: 0 15px 25px rgba(0,0,0,.6);
  border-radius: 10px;
}

.login-box h2 {
  margin: 0 0 30px;
  padding: 0;
  color: #fff;
  text-align: center;
}

.login-box .success {
  margin: 0 0 30px;
  background-color: green;
  padding: 10px;
  border-radius: 3px;
  color: white;
  text-align: left;
}
.login-box .error {
  margin: 0 0 30px;
  background-color: red;
  padding: 10px;
  color: white;
  text-align: left;
}

.login-box .user-box {
  position: relative;
}

.login-box .user-box input {
  width: 100%;
  padding: 10px 0;
  font-size: 16px;
  color: #fff;
  margin-bottom: 30px;
  border: none;
  border-bottom: 1px solid #fff;
  outline: none;
  background: transparent;
}
.login-box .user-box label {
  position: absolute;
  top:0;
  left: 0;
  padding: 10px 0;
  font-size: 16px;
  color: #fff;
  pointer-events: none;
  transition: .5s;
}

.login-box .user-box input:focus ~ label,
.login-box .user-box input:valid ~ label {
  top: -20px;
  left: 0;
  color: #03e9f4;
  font-size: 12px;
}

.login-box form button {
  position: relative;
  display: inline-block;
  padding: 10px 20px;
  color: #03e9f4;
  background-color: #141e30;
  font-size: 16px;
  text-decoration: none;
  text-transform: uppercase;
  overflow: hidden;
  transition: .5s;
  margin-top: 40px;
  letter-spacing: 4px
}

.login-box button:hover {
  background: #03e9f4;
  color: #fff;
  border-radius: 5px;
  box-shadow: 0 0 5px #03e9f4,
              0 0 25px #03e9f4,
              0 0 50px #03e9f4,
              0 0 100px #03e9f4;
}

.login-box button span {
  position: absolute;
  display: block;
}

.login-box button span:nth-child(1) {
  top: 0;
  left: -100%;
  width: 100%;
  height: 2px;
  background: linear-gradient(90deg, transparent, #03e9f4);
  animation: btn-anim1 1s linear infinite;
}

@keyframes btn-anim1 {
  0% {
    left: -100%;
  }
  50%,100% {
    left: 100%;
  }
}

.login-box button span:nth-child(2) {
  top: -100%;
  right: 0;
  width: 2px;
  height: 100%;
  background: linear-gradient(180deg, transparent, #03e9f4);
  animation: btn-anim2 1s linear infinite;
  animation-delay: .25s
}

@keyframes btn-anim2 {
  0% {
    top: -100%;
  }
  50%,100% {
    top: 100%;
  }
}

.login-box button span:nth-child(3) {
  bottom: 0;
  right: -100%;
  width: 100%;
  height: 2px;
  background: linear-gradient(270deg, transparent, #03e9f4);
  animation: btn-anim3 1s linear infinite;
  animation-delay: .5s
}

@keyframes btn-anim3 {
  0% {
    right: -100%;
  }
  50%,100% {
    right: 100%;
  }
}

.login-box button span:nth-child(4) {
  bottom: -100%;
  left: 0;
  width: 2px;
  height: 100%;
  background: linear-gradient(360deg, transparent, #03e9f4);
  animation: btn-anim4 1s linear infinite;
  animation-delay: .75s
}

@keyframes btn-anim4 {
  0% {
    bottom: -100%;
  }
  50%,100% {
    bottom: 100%;
  }
}


Conclusiones

  • Finalmente podemos ver los datos desenciptados en el server.
  • Debemos tener en cuenta que cada vez que se reinicie nuestro server se regeneraran las credenciales RSA.
  • La AES-Key se generará dinamicámente en cada vez que el se inicie el servicio en el cliente.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment