Created
February 6, 2023 14:37
-
-
Save ve3/b16b2dfdceb0e4e24ecd9b9078042197 to your computer and use it in GitHub Desktop.
Cross languages encrypt/decrypt with AES-256-GCM
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is an updated of previous code https://gist.github.com/ve3/0f77228b174cf92a638d81fddb17189d