Skip to content

Instantly share code, notes, and snippets.

@ve3
Last active August 3, 2025 17:58
Show Gist options
  • Save ve3/b16b2dfdceb0e4e24ecd9b9078042197 to your computer and use it in GitHub Desktop.
Save ve3/b16b2dfdceb0e4e24ecd9b9078042197 to your computer and use it in GitHub Desktop.

Revisions

  1. ve3 revised this gist Aug 3, 2025. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions Encryption.php
    Original file line number Diff line number Diff line change
    @@ -81,9 +81,9 @@ public function decrypt(string $data, string $key)
    * @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
    public function encrypt(string $data, string $key, $iv = null): string
    {
    if (is_null($iv)) {
    if (is_null($iv) || !is_string($iv)) {
    $iv = $this->getIV();
    }

  2. ve3 created this gist Feb 6, 2023.
    264 changes: 264 additions & 0 deletions Encryption.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,264 @@

    '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


    }
    137 changes: 137 additions & 0 deletions Encryption.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,137 @@
    <?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


    }
    67 changes: 67 additions & 0 deletions test.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,67 @@
    <!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>
    40 changes: 40 additions & 0 deletions test.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,40 @@
    <?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>