Skip to content

Instantly share code, notes, and snippets.

@ve3
Created February 6, 2023 14:37
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ve3/b16b2dfdceb0e4e24ecd9b9078042197 to your computer and use it in GitHub Desktop.
Save ve3/b16b2dfdceb0e4e24ecd9b9078042197 to your computer and use it in GitHub Desktop.
Cross languages encrypt/decrypt with AES-256-GCM
'use strict';
/**
* Encryption class for encrypt/decrypt data.
*/
export default class Encryption {
/**
* @see constructor()
* @property {object} options The options.
*/
#options = {};
/**
* Class constructor.
*
* @link https://crypto.stackexchange.com/questions/41601/aes-gcm-recommended-iv-size-why-12-bytes IV size.
* @param {object} options The options.
* @param {boolean} options.debug Debugging message.
* @param {string} options.algorithm Algorithm. Example: 'aes-256-gcm'.
* @param {int} options.ivByteLength The Initialization vector (IV). ( byte to bit = 1*8; so 16*8 = 96 )
*/
constructor({} = {}) {
const defaults = {
debug: false,
algorithm: 'aes-256-gcm',
ivByteLength: 12,
};
const options = {
...defaults,
...arguments[0],
}
this.#options = options;
}// constructor
/**
* Base64 encoded string to ArrayBuffer.
*
* @link https://stackoverflow.com/a/41106346/128761 Original source.
* @param {string} base64 Base64 encoded string.
* @returns {ArrayBuffer}
*/
#base64ToBuffer(base64) {
return Uint8Array.from(
window.atob(base64),
(c) => {
return c.charCodeAt(0);
}
)
}// #base64ToBuffer
/**
* Concat Array Buffer.
*
* @link https://pilabor.com/series/dotnet/js-gcm-encrypt-dotnet-decrypt/ Original source.
* @param {ArrayBuffer} iv
* @param {ArrayBuffer} encrypted
* @returns {ArrayBuffer}
*/
#concatArrayBuffer(iv, encrypted) {
let tmp = new Uint8Array(iv.byteLength + encrypted.byteLength);
tmp.set(new Uint8Array(iv), 0);
tmp.set(new Uint8Array(encrypted), iv.byteLength);
return tmp.buffer;
}// #concatArrayBuffer
/**
* Disjoin (un-concatenate) ArrayBuffer.
*
* @param {ArrayBuffer} arrayBuffer
* @returns {Array}
*/
#disJoinArrayBuffer(arrayBuffer) {
const iv = arrayBuffer.slice(0, this.#options.ivByteLength);
const encryptedMessage = arrayBuffer.slice(-(arrayBuffer.byteLength - this.#options.ivByteLength)).buffer;
return [iv, encryptedMessage];
}// disJoinArrayBuffer
/**
* Get algorithm parts.
*
* @returns {mixed} Returns object with 'cipher', 'length', 'mode' if found valid algorithm string, returns the algorithm string as in parameter if invalid.
*/
#getAlgoParts() {
const algorithm = this.#options.algorithm;
const regex = /(?<cipher>[a-z]+)\-(?<length>\d+)\-(?<mode>[a-z]+)/gmi;
const matches = regex.exec(algorithm);
if (
typeof(matches.groups?.cipher) !== 'undefined' &&
typeof(matches.groups?.length) !== 'undefined' &&
typeof(matches.groups?.mode) !== 'undefined'
) {
return matches.groups;
}
throw new Error('Invalid algorithm.');
}// #getAlgoParts
/**
* Array Buffer to Base 64.
*
* @link https://pilabor.com/series/dotnet/js-gcm-encrypt-dotnet-decrypt/ Original source.
* @param {ArrayBuffer} arrayBuffer
* @returns {string} Returns base64 encoded string.
*/
bufferToBase64(arrayBuffer) {
return window.btoa(
String.fromCharCode(
...new Uint8Array(arrayBuffer)
)
);
}// bufferToBase64
/**
* Decrypt the data.
*
* @link https://gist.github.com/themikefuller/aca9491f960cbb8d94cdd7236698f0cd Original source.
* @param {string} data The encrypted data to be decrypted.
* @param {CryptoKey} key The secret key or passphrase.
* @returns {string} Return the decrypted string on success
*/
async decrypt(data, key) {
const encryptedArrayBuffer = this.#base64ToBuffer(data);
const [iv, ciphertext] = this.#disJoinArrayBuffer(encryptedArrayBuffer);
if (this.#options.debug === true) {
console.debug(' disjoined IV: ', iv, this.bufferToBase64(iv));
console.debug(' disjoined ciphertext: ', ciphertext, new Uint8Array(ciphertext), this.bufferToBase64(ciphertext));
}
const algoParts = this.#getAlgoParts();
let decrypted = await crypto.subtle.decrypt(
{
'name': algoParts.cipher.toUpperCase() + '-' + algoParts.mode.toUpperCase(),
'iv': iv,
},
key,
ciphertext
);
const decoder = new TextDecoder();
let decoded = decoder.decode(decrypted);
return decoded;
}// decrypt
/**
* Encrypt the data.
*
* @link https://gist.github.com/themikefuller/aca9491f960cbb8d94cdd7236698f0cd Original source.
* @async
* @param {mixed} data The data to be encrypted.
* @param {CryptoKey} key The secret key or passphrase.
* @param {Uint8Array} iv Initialization Vector. Leave undefined to auto generated.
* @return {string} Return base64 encoded of encrypted data
*/
async encrypt(data, key, iv) {
const encoder = new TextEncoder();
let encodedData = encoder.encode(data);
if (this.#options.debug === true) {
console.debug(' data encoded: ', encodedData, this.bufferToBase64(encodedData));
}
const algoParts = this.#getAlgoParts();
if (typeof(iv) === 'undefined') {
iv = this.getIV();
}
if (this.#options.debug === true) {
console.debug(' IV: ', iv, this.bufferToBase64(iv));
}
const ciphertext = await crypto.subtle.encrypt(
{
'name': algoParts.cipher.toUpperCase() + '-' + algoParts.mode.toUpperCase(),
'iv': iv,
},
key,
encodedData
);
if (this.#options.debug === true) {
console.debug(' ciphertext: ', ciphertext, new Uint8Array(ciphertext), this.bufferToBase64(ciphertext));
}
return this.bufferToBase64(
this.#concatArrayBuffer(iv.buffer, ciphertext)
);
}// encrypt
/**
* Generate key.
*
* @link https://gist.github.com/themikefuller/aca9491f960cbb8d94cdd7236698f0cd Original source.
* @see https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey
* @async
* @returns {Promise<CryptoKey>} Returns a `Promise` with a CryptoKey.
*/
async generateKey() {
const algoParts = this.#getAlgoParts();
const name = algoParts.cipher.toUpperCase() + '-' + algoParts.mode.toUpperCase();
const length = parseInt(algoParts.length);
return await crypto.subtle.generateKey(
{
'name': name,
'length': length
},
true,
['encrypt', 'decrypt']
);
}// generateKey
/**
* Get CryptoKey from string.
*
* @link https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a Original source.
* @param {string} key The secret key or passphrase.
* @returns {CrytoKey} Return `CryptoKey` object of the secret key.
*/
async getCryptoKeyFromString(key) {
const algoParts = this.#getAlgoParts();
const encoder = new TextEncoder();
let encodedkey = encoder.encode(key);
const keyHashed = await crypto.subtle.digest('SHA-256', encodedkey);
return await crypto.subtle.importKey(
'raw',
keyHashed,
{
'name':algoParts.cipher.toUpperCase() + '-' + algoParts.mode.toUpperCase()
},
false,
['encrypt', 'decrypt']
);
}// getCryptoKeyFromString
/**
* Get Initialization Vector (IV)
*
* @link https://gist.github.com/themikefuller/aca9491f960cbb8d94cdd7236698f0cd Original source.
* @returns {Uint8Array}
*/
getIV() {
const ivByteLength = this.#options.ivByteLength;
return crypto.getRandomValues(new Uint8Array(ivByteLength));
}// getIV
}
<?php
/**
* Encryption for encrypt and decrypt data.
*
* @author Vee W.
* @license MIT
*/
class Encryption
{
/**
* @see __construct()
* @var array The options.
*/
protected $options = [];
/**
* Class constructor.
*
* @param array $options Associative array keys:<br>
* 'algorithm' (string) Algorithm. Example: 'aes-256-gcm'.<br>
* 'keyLength' (int) secret key or passphrase length. Default is 32.<br>
* 'tagLength' (int) The length of the authentication tag. Read more at https://www.php.net/manual/en/function.openssl-encrypt.php
*/
public function __construct(array $options = [])
{
$defaults = [
'algorithm' => 'aes-256-gcm',
'keyLength' => 32,
'tagLength' => 16,
];
$options = array_merge($defaults, $options);
if (!in_array($options['algorithm'], openssl_get_cipher_methods())) {
throw new \Exception(
'The algorithm is not supported.'
);
}
$this->options = $options;
}// __construct
/**
* Decrypt the data.
*
* @param string $data The encrypted data to be decrypted.
* @param string $key The secret key or passphrase. The key should hashed from `getKeyHashed()` method.
* @return string|false Return the decrypted string on success, `false` on failure.
*/
public function decrypt(string $data, string $key)
{
$b64Decoded = base64_decode($data);
if (false === $b64Decoded) {
return false;
}
$ivLength = $this->getIVLength();
if (!is_numeric($ivLength)) {
return false;
}
$iv = substr($b64Decoded, 0, $ivLength);
$ciphertext = substr($b64Decoded, $ivLength, -$this->options['tagLength']);
$tag = substr($b64Decoded, -$this->options['tagLength']);
return openssl_decrypt($ciphertext, $this->options['algorithm'], $key, OPENSSL_RAW_DATA, $iv, $tag);
}// decrypt
/**
* Encrypt the data.
*
* @param string $data The data to be encrypted.
* @param string $key The secret key or passphrase. The key should hashed from `getKeyHashed()` method.
* @param string|null $iv Initialization Vector. Set to `null` to auto generate.
* @return string Return base64 encoded of encrypted data.
*/
public function encrypt(string $data, string $key, string $iv = null): string
{
if (is_null($iv)) {
$iv = $this->getIV();
}
$tag = '';
$ciphertext = openssl_encrypt($data, $this->options['algorithm'], $key, OPENSSL_RAW_DATA, $iv, $tag, '', $this->options['tagLength']);
return base64_encode($iv . $ciphertext . $tag);
}// encrypt
/**
* Get Initialization Vector.
*
* @return string Return Initialization Vector string.
*/
public function getIV(): string
{
$ivLength = $this->getIVLength();
if (!is_numeric($ivLength)) {
return '';
}
return openssl_random_pseudo_bytes($ivLength);
}// getIV
/**
* Get Initialization Vector length.
*
* @return int|false Return `int` on success, `false` on failure.
*/
protected function getIVLength()
{
return openssl_cipher_iv_length($this->options['algorithm']);
}// getIVLength
/**
* Get input secret key as hashed.
*
* @param string $key The secret key or passphrase.
* @return string Return hashed key and cut to the length.
*/
public function getKeyHashed(string $key): string
{
$hashed = hash('sha256', $key, true);
return substr($hashed, 0, $this->options['keyLength']);
}// getKeyHashed
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JS encrypt/decrypt</title>
</head>
<body>
<h1>Encrypt/Decrypt use Encryption class.</h1>
<h3>Original:</h3>
<p id="original-text">Hello world</p>
<h3>Encrypted:</h3>
<p id="encrypted-text"></p>
<h3>Decrypted:</h3>
<p id="decrypted-text"></p>
<script type="module">
import Encryption from './Encryption.js';
const encryptionObj = new Encryption({
debug: true,
});
const secretKey = 'my secret';
window.addEventListener('DOMContentLoaded', async () => {
if (location.protocol !== 'https:') {
alert('Please open via HTTPS.');
}
const originalText = document.getElementById('original-text').innerText;
const encryptedText = document.getElementById('encrypted-text');
const decryptedText = document.getElementById('decrypted-text');
const key = await encryptionObj.generateKey();
console.log('generateKey: ', key);
console.log('getKeyFromPassword');
const keyFromPw = await encryptionObj.getCryptoKeyFromString(secretKey);
console.log(keyFromPw);
const iv = encryptionObj.getIV();
console.log('getIV: ', iv, encryptionObj.bufferToBase64(iv));
console.log('-------------');
console.log('encrypt():');
const encryptedVal = await encryptionObj.encrypt(originalText, keyFromPw, iv);
console.log('encrypted: ', encryptedVal);
encryptedText.innerHTML = encryptedVal;
console.log('-------------');
console.log('decrypt():');
const decryptedVal = await encryptionObj.decrypt(encryptedVal, keyFromPw);
console.log('decrypted: ', decryptedVal);
decryptedText.innerHTML = decryptedVal;
console.log('-------------');
});
</script>
</body>
</html>
<?php
$secretKey = 'my secret';
$originalString = 'Hello world';
require_once 'Encryption.php';
$Encryption = new Encryption();
$keyFromPw = $Encryption->getKeyHashed($secretKey);
$iv = $Encryption->getIV();
$encryptedVal = $Encryption->encrypt($originalString, $keyFromPw, $iv);
$decryptedVal = $Encryption->decrypt($encryptedVal, $keyFromPw);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PHP encrypt/decrypt</title>
</head>
<body>
<h1>Encrypt/Decrypt use Encryption class.</h1>
<h3>Original:</h3>
<p id="original-text"><?php echo ($originalString ?? ''); ?></p>
<h3>Encrypted:</h3>
<p id="encrypted-text"><?php echo ($encryptedVal ?? ''); ?></p>
<h3>Decrypted:</h3>
<p id="decrypted-text"><?php echo ($decryptedVal ?? ''); ?></p>
<h3>Debug:</h3>
<pre>
<?php
echo 'key from secret (passphrase): ' . $keyFromPw . PHP_EOL;
echo 'iv: ' . $iv . PHP_EOL;
?>
</pre>
</body>
</html>
@ve3
Copy link
Author

ve3 commented Jun 9, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment