Skip to content

Commit 282b53b

Browse files
committed
initial commit
1 parent 3f332be commit 282b53b

20 files changed

+809
-3
lines changed

.gitattributes

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
*.js text eol=lf
2+
*.sh text eol=lf
3+
*.json text eol=lf
4+
*.ts text eol=lf
5+
*.html text eol=lf
6+
*.txt text eol=lf
7+
*.min.js binary
8+
vendor/* binary
9+
10+
.github export-ignore
11+
.gitattributes export-ignore
12+
.gitignore export-ignore
13+
.npmignore export-ignore
14+
.npmrc export-ignore
15+
build export-ignore
16+
tests export-ignore
17+
playwright.config.ts export-ignore
18+
tsconfig.json export-ignore
19+
package-lock.json export-ignore

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.idea
2+
.npmrc
3+
node_modules
4+
package-lock.json
5+
/test-results/
6+
/playwright-report/
7+
/playwright/.cache/

.npmignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*
2+
!dist/*
3+
!src/*
4+
!CHANGELOG.md

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2024 Roland Eigelsreiter
3+
Copyright (c) 2024 BrainFooLong (Roland Eigelsreiter)
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,47 @@
1-
# js-aes-php
2-
Native AES (No CryptoJS required) encryption/decryption on client side with Javascript and on server side with PHP
1+
# Slim native AES encryption/decryption on client side with Javascript and on server side with PHP
2+
3+
A tool to AES encrypt/decrypt data in javascript and/or PHP. You can use it for PHP only, for Javascript only or mix it together.
4+
5+
It uses `aes-256-cbc` implementation with random salts and random initialization vector. This library does not support other ciphers or modes.
6+
7+
This library is the successor to my previous [CryptoJs-Aea-Php](https://github.com/brainfoolong/cryptojs-aes-php) encryption library that required CryptoJS. This library does not require any third party dependency as modern browsers and Node now have proper crypto tools built in. Attention: This library does output different encryption values to my previous library, it cannot be a drop-in replacement.
8+
9+
### Features
10+
* Encrypt any value in Javascript (objects/array/etc...) - Everything that can be passed to JSON.stringify
11+
* Encrypt any value in PHP (object/array/etc...) - Everything that can be passed to json_encode
12+
* Decrypt in PHP/Javascript, doesn't matter where you have encrypted the values
13+
* It is safe to store and transfer the encrypted values, as the encrypted output only contains hex characters (0-9 A-F)
14+
* Small footprint: 5kb unzipped Javascript file
15+
16+
### Requirements
17+
* For Javascript: Any [recent Browser](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt#browser_compatibility) or Node environment (15+)
18+
* For Typescript: Use `src/ts/js-aes-php.ts`
19+
* For PHP: 8.0 or above with OpenSSL extension enabled
20+
21+
22+
### PHP - How to use
23+
```php
24+
$value = ['foobar' => 'l`î', 'emojiiii' => '😊'];
25+
$password = '😊Blub';
26+
$encrypted = JsAesPhp::encrypt($value, $password);
27+
$decrypted = JsAesPhp::decrypt($encrypted, $password);
28+
```
29+
30+
### Javascript/Typescript - How to use
31+
```javascript
32+
const value = { 'foobar': 'l`î', 'emojiiii': '😊' }
33+
const password = '😊Blub'
34+
const encrypted = await JsAesPhp.encrypt(value, password)
35+
const decrypted = await JsAesPhp.decrypt(encrypted, password)
36+
```
37+
38+
### Security Notes
39+
40+
This library use AES-256-CBC encryption, which is still good and safe but there are (maybe) better alternatives for your use case. If you require really high security, you should invest more time for what is suitable for you.
41+
42+
Also, there's a good article about PHP issues/info related to this
43+
library: https://stackoverflow.com/questions/16600708/how-do-you-encrypt-and-decrypt-a-php-string/30159120#30159120
44+
45+
### Alternatives - ASCON
46+
47+
You may wonder if there are alternatives to AES encryption that you can use in PHP/JS. ASCON is a newer, lightweight cipher that have been selected in 2023 by the [NIST](https://csrc.nist.gov/projects/lightweight-cryptography) as the new standard for lightweight cryptography, which may suite your needs. I have created libraries for both PHP and JS which you can find at https://github.com/brainfoolong/php-ascon and https://github.com/brainfoolong/js-ascon

SECURITY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Security Policy
2+
3+
You can use GitHub to privately notify me about security issues.
4+
5+
## Supported Versions
6+
7+
Always just the latest release version.

build/dist.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// create all required dist files
2+
const fs = require('fs')
3+
4+
const packageJson = require('../package.json')
5+
const srcFile = __dirname + '/../dist/js-aes-php.js'
6+
const srcFileModule = __dirname + '/../dist/js-aes-php.module.js'
7+
let contents = fs.readFileSync(srcFile).toString().replace('export default class JsAesPhp', 'class JsAesPhp')
8+
contents = '// JsAesPhp v' + packageJson.version + ' @ ' + packageJson.homepage + '\n' + contents
9+
contents += `
10+
if (typeof module !== 'undefined' && module.exports) {
11+
module.exports = JsAesPhp
12+
}
13+
`
14+
let contentsCommonJs = contents
15+
fs.writeFileSync(srcFile, contentsCommonJs)
16+
let contentsModule = contents.replace(/^class JsAesPhp/m, 'export default class JsAesPhp')
17+
fs.writeFileSync(srcFileModule, contentsModule)

dist/js-aes-php.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// JsAesPhp v1.0.1 @ https://github.com/brainfoolong/js-aes-php
2+
class JsAesPhp {
3+
/**
4+
* Encrypt a given value which can be of any kind that can be JSON.stringify'd
5+
* @param {any} value
6+
* @param {string} password The raw password
7+
* @param {number} hashIterations The number of iterations to use for the password hash
8+
* This can be subject to change in the future. The number of iterations will be contained
9+
* in the output, so changing the number doesn't break the decrypt function
10+
* @return {Promise<string>}
11+
*/
12+
static async encrypt(value, password, hashIterations = 100000) {
13+
const crypto = this.getCrypto();
14+
const iv = this.getCrypto().getRandomValues(new Uint8Array(16));
15+
const salt = crypto.getRandomValues(new Uint8Array(16));
16+
const key = await this.generateKey(password, salt, hashIterations);
17+
const encoded = new TextEncoder().encode(JSON.stringify(value));
18+
const encrypted = new Uint8Array(await crypto.subtle.encrypt({
19+
name: 'AES-CBC',
20+
iv: iv,
21+
}, key, encoded));
22+
return hashIterations.toString().padStart(10, '0') + this.byteArrayToHex(iv) + this.byteArrayToHex(salt) + this.byteArrayToHex(encrypted);
23+
}
24+
/**
25+
* Decrypt any previously JsAesPhp.encrypt() value
26+
* @param {string} encryptedValue
27+
* @param {string} password The raw password
28+
* @return {Promise<any>}
29+
*/
30+
static async decrypt(encryptedValue, password) {
31+
if (typeof encryptedValue !== 'string' || encryptedValue.length <= 74 || encryptedValue.match(/[^0-9a-f]/)) {
32+
throw new Error('Invalid encryptedValue');
33+
}
34+
const crypto = this.getCrypto();
35+
const key = await this.generateKey(password, this.hexToByteArray(encryptedValue.substring(42, 74)), parseInt(encryptedValue.substring(0, 10)));
36+
return JSON.parse(new TextDecoder().decode(new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv: this.hexToByteArray(encryptedValue.substring(10, 42)) }, key, this.hexToByteArray(encryptedValue.substring(74))))));
37+
}
38+
/**
39+
* Generate crypto key
40+
* @param {string} password The raw password
41+
* @param {Uint8Array} salt
42+
* @param {number} hashIterations
43+
* @return {Promise<CryptoKey>}
44+
* @private
45+
*/
46+
static async generateKey(password, salt, hashIterations = 100000) {
47+
if (hashIterations >= 10000000000) {
48+
throw new Error('Hash iterations limit exceeded');
49+
}
50+
const crypto = this.getCrypto();
51+
const passwordBytes = new TextEncoder().encode(password);
52+
const initialKey = await crypto.subtle.importKey('raw', passwordBytes, { name: 'PBKDF2', hash: 'SHA-256' }, false, ['deriveKey']);
53+
return crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations: hashIterations, hash: 'SHA-256' }, initialKey, { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']);
54+
}
55+
/**
56+
* Get crypto instance depending on the environment
57+
* @return {Crypto}
58+
* @private
59+
*/
60+
static getCrypto() {
61+
if (typeof this.crypto !== 'undefined') {
62+
return this.crypto;
63+
}
64+
if (typeof Uint8Array === 'undefined') {
65+
throw new Error('Unsupported environment, Uint8Array missing');
66+
}
67+
if (typeof TextEncoder === 'undefined') {
68+
throw new Error('Unsupported environment, TextEncoder missing');
69+
}
70+
if (typeof window === 'undefined') {
71+
// @ts-ignore
72+
if (typeof module !== 'undefined' && module.exports) {
73+
// @ts-ignore
74+
this.crypto = require('node:crypto');
75+
return this.crypto;
76+
}
77+
}
78+
if (!window.crypto) {
79+
throw new Error('Unsupported environment, window.crypto missing');
80+
}
81+
this.crypto = window.crypto;
82+
return this.crypto;
83+
}
84+
/**
85+
* Convert given byte array to visual hex representation with leading 0x
86+
* @param {Uint8Array} byteArray
87+
* @return {string}
88+
*/
89+
static byteArrayToHex(byteArray) {
90+
return Array.from(byteArray).map(x => x.toString(16).padStart(2, '0')).join('');
91+
}
92+
/**
93+
* Convert a hex string into given byte array to visual hex representation
94+
* @param {str} str
95+
* @return {string}
96+
*/
97+
static hexToByteArray(str) {
98+
return Uint8Array.from((str.match(/.{1,2}/g) || []).map((byte) => parseInt(byte, 16)));
99+
}
100+
}
101+
102+
if (typeof module !== 'undefined' && module.exports) {
103+
module.exports = JsAesPhp
104+
}

dist/js-aes-php.module.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// JsAesPhp v1.0.1 @ https://github.com/brainfoolong/js-aes-php
2+
export default class JsAesPhp {
3+
/**
4+
* Encrypt a given value which can be of any kind that can be JSON.stringify'd
5+
* @param {any} value
6+
* @param {string} password The raw password
7+
* @param {number} hashIterations The number of iterations to use for the password hash
8+
* This can be subject to change in the future. The number of iterations will be contained
9+
* in the output, so changing the number doesn't break the decrypt function
10+
* @return {Promise<string>}
11+
*/
12+
static async encrypt(value, password, hashIterations = 100000) {
13+
const crypto = this.getCrypto();
14+
const iv = this.getCrypto().getRandomValues(new Uint8Array(16));
15+
const salt = crypto.getRandomValues(new Uint8Array(16));
16+
const key = await this.generateKey(password, salt, hashIterations);
17+
const encoded = new TextEncoder().encode(JSON.stringify(value));
18+
const encrypted = new Uint8Array(await crypto.subtle.encrypt({
19+
name: 'AES-CBC',
20+
iv: iv,
21+
}, key, encoded));
22+
return hashIterations.toString().padStart(10, '0') + this.byteArrayToHex(iv) + this.byteArrayToHex(salt) + this.byteArrayToHex(encrypted);
23+
}
24+
/**
25+
* Decrypt any previously JsAesPhp.encrypt() value
26+
* @param {string} encryptedValue
27+
* @param {string} password The raw password
28+
* @return {Promise<any>}
29+
*/
30+
static async decrypt(encryptedValue, password) {
31+
if (typeof encryptedValue !== 'string' || encryptedValue.length <= 74 || encryptedValue.match(/[^0-9a-f]/)) {
32+
throw new Error('Invalid encryptedValue');
33+
}
34+
const crypto = this.getCrypto();
35+
const key = await this.generateKey(password, this.hexToByteArray(encryptedValue.substring(42, 74)), parseInt(encryptedValue.substring(0, 10)));
36+
return JSON.parse(new TextDecoder().decode(new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv: this.hexToByteArray(encryptedValue.substring(10, 42)) }, key, this.hexToByteArray(encryptedValue.substring(74))))));
37+
}
38+
/**
39+
* Generate crypto key
40+
* @param {string} password The raw password
41+
* @param {Uint8Array} salt
42+
* @param {number} hashIterations
43+
* @return {Promise<CryptoKey>}
44+
* @private
45+
*/
46+
static async generateKey(password, salt, hashIterations = 100000) {
47+
if (hashIterations >= 10000000000) {
48+
throw new Error('Hash iterations limit exceeded');
49+
}
50+
const crypto = this.getCrypto();
51+
const passwordBytes = new TextEncoder().encode(password);
52+
const initialKey = await crypto.subtle.importKey('raw', passwordBytes, { name: 'PBKDF2', hash: 'SHA-256' }, false, ['deriveKey']);
53+
return crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations: hashIterations, hash: 'SHA-256' }, initialKey, { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']);
54+
}
55+
/**
56+
* Get crypto instance depending on the environment
57+
* @return {Crypto}
58+
* @private
59+
*/
60+
static getCrypto() {
61+
if (typeof this.crypto !== 'undefined') {
62+
return this.crypto;
63+
}
64+
if (typeof Uint8Array === 'undefined') {
65+
throw new Error('Unsupported environment, Uint8Array missing');
66+
}
67+
if (typeof TextEncoder === 'undefined') {
68+
throw new Error('Unsupported environment, TextEncoder missing');
69+
}
70+
if (typeof window === 'undefined') {
71+
// @ts-ignore
72+
if (typeof module !== 'undefined' && module.exports) {
73+
// @ts-ignore
74+
this.crypto = require('node:crypto');
75+
return this.crypto;
76+
}
77+
}
78+
if (!window.crypto) {
79+
throw new Error('Unsupported environment, window.crypto missing');
80+
}
81+
this.crypto = window.crypto;
82+
return this.crypto;
83+
}
84+
/**
85+
* Convert given byte array to visual hex representation with leading 0x
86+
* @param {Uint8Array} byteArray
87+
* @return {string}
88+
*/
89+
static byteArrayToHex(byteArray) {
90+
return Array.from(byteArray).map(x => x.toString(16).padStart(2, '0')).join('');
91+
}
92+
/**
93+
* Convert a hex string into given byte array to visual hex representation
94+
* @param {str} str
95+
* @return {string}
96+
*/
97+
static hexToByteArray(str) {
98+
return Uint8Array.from((str.match(/.{1,2}/g) || []).map((byte) => parseInt(byte, 16)));
99+
}
100+
}
101+
102+
if (typeof module !== 'undefined' && module.exports) {
103+
module.exports = JsAesPhp
104+
}

package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "js-aes-php",
3+
"version": "1.0.1",
4+
"description": "Slim native AES encryption/decryption on client side with Javascript and on server side with PHP",
5+
"scripts": {
6+
"dist": "node ./node_modules/typescript/bin/tsc --project tsconfig.json && node build/dist.js"
7+
},
8+
"repository": {
9+
"type": "git",
10+
"url": "git+https://github.com/brainfoolong/js-aes-php.git"
11+
},
12+
"author": "BrainFooLong (Roland Eigelsreiter)",
13+
"license": "MIT",
14+
"bugs": {
15+
"url": "https://github.com/brainfoolong/js-aes-php/issues"
16+
},
17+
"homepage": "https://github.com/brainfoolong/js-aes-php",
18+
"devDependencies": {
19+
"typescript": "^5.5.4"
20+
},
21+
"engines": {
22+
"node": ">=15.0.0"
23+
},
24+
"main": "dist/js-aes-php.js"
25+
}

0 commit comments

Comments
 (0)