Last active
November 15, 2024 14:18
-
-
Save tscholl2/dc7dc15dc132ea70a98e8542fefffa28 to your computer and use it in GitHub Desktop.
simple AES encryption/decryption example with PBKDF2 key derivation in Go, Javascript, and Python
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
package main | |
import ( | |
"crypto/aes" | |
"crypto/cipher" | |
"crypto/rand" | |
"crypto/sha256" | |
"encoding/hex" | |
"fmt" | |
"strings" | |
"golang.org/x/crypto/pbkdf2" | |
) | |
func deriveKey(passphrase string, salt []byte) ([]byte, []byte) { | |
if salt == nil { | |
salt = make([]byte, 8) | |
// http://www.ietf.org/rfc/rfc2898.txt | |
// Salt. | |
rand.Read(salt) | |
} | |
return pbkdf2.Key([]byte(passphrase), salt, 1000, 32, sha256.New), salt | |
} | |
func encrypt(passphrase, plaintext string) string { | |
key, salt := deriveKey(passphrase, nil) | |
iv := make([]byte, 12) | |
// http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf | |
// Section 8.2 | |
rand.Read(iv) | |
b, _ := aes.NewCipher(key) | |
aesgcm, _ := cipher.NewGCM(b) | |
data := aesgcm.Seal(nil, iv, []byte(plaintext), nil) | |
return hex.EncodeToString(salt) + "-" + hex.EncodeToString(iv) + "-" + hex.EncodeToString(data) | |
} | |
func decrypt(passphrase, ciphertext string) string { | |
arr := strings.Split(ciphertext, "-") | |
salt, _ := hex.DecodeString(arr[0]) | |
iv, _ := hex.DecodeString(arr[1]) | |
data, _ := hex.DecodeString(arr[2]) | |
key, _ := deriveKey(passphrase, salt) | |
b, _ := aes.NewCipher(key) | |
aesgcm, _ := cipher.NewGCM(b) | |
data, _ = aesgcm.Open(nil, iv, data, nil) | |
return string(data) | |
} | |
func main() { | |
c := encrypt("hello", "world") | |
fmt.Println(c) | |
fmt.Println(decrypt("hello", c)) | |
fmt.Println(decrypt("hello", "c2932347953ad4a4-25f496d260de9c150fc9e4c6-20bc1f8439796cc914eb783b9996a8d9c32d45e2df")) | |
} |
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
/** | |
* Encodes a utf8 string as a byte array. | |
* @param {String} str | |
* @returns {Uint8Array} | |
*/ | |
function str2buf(str) { | |
return new TextEncoder("utf-8").encode(str); | |
} | |
/** | |
* Decodes a byte array as a utf8 string. | |
* @param {Uint8Array} buffer | |
* @returns {String} | |
*/ | |
function buf2str(buffer) { | |
return new TextDecoder("utf-8").decode(buffer); | |
} | |
/** | |
* Decodes a string of hex to a byte array. | |
* @param {String} hexStr | |
* @returns {Uint8Array} | |
*/ | |
function hex2buf(hexStr) { | |
return new Uint8Array(hexStr.match(/.{2}/g).map(h => parseInt(h, 16))); | |
} | |
/** | |
* Encodes a byte array as a string of hex. | |
* @param {Uint8Array} buffer | |
* @returns {String} | |
*/ | |
function buf2hex(buffer) { | |
return Array.prototype.slice | |
.call(new Uint8Array(buffer)) | |
.map(x => [x >> 4, x & 15]) | |
.map(ab => ab.map(x => x.toString(16)).join("")) | |
.join(""); | |
} | |
/** | |
* Given a passphrase, this generates a crypto key | |
* using `PBKDF2` with SHA256 and 1000 iterations. | |
* If no salt is given, a new one is generated. | |
* The return value is an array of `[key, salt]`. | |
* @param {String} passphrase | |
* @param {UInt8Array} salt [salt=random bytes] | |
* @returns {Promise<[CryptoKey,UInt8Array]>} | |
*/ | |
function deriveKey(passphrase, salt) { | |
salt = salt || crypto.getRandomValues(new Uint8Array(8)); | |
return crypto.subtle | |
.importKey("raw", str2buf(passphrase), "PBKDF2", false, ["deriveKey"]) | |
.then(key => | |
crypto.subtle.deriveKey( | |
{ name: "PBKDF2", salt, iterations: 1000, hash: "SHA-256" }, | |
key, | |
{ name: "AES-GCM", length: 256 }, | |
false, | |
["encrypt", "decrypt"], | |
), | |
) | |
.then(key => [key, salt]); | |
} | |
/** | |
* Given a passphrase and some plaintext, this derives a key | |
* (generating a new salt), and then encrypts the plaintext with the derived | |
* key using AES-GCM. The ciphertext, salt, and iv are hex encoded and joined | |
* by a "-". So the result is `"salt-iv-ciphertext"`. | |
* @param {String} passphrase | |
* @param {String} plaintext | |
* @returns {Promise<String>} | |
*/ | |
function encrypt(passphrase, plaintext) { | |
const iv = crypto.getRandomValues(new Uint8Array(12)); | |
const data = str2buf(plaintext); | |
return deriveKey(passphrase).then(([key, salt]) => | |
crypto.subtle | |
.encrypt({ name: "AES-GCM", iv }, key, data) | |
.then(ciphertext => `${buf2hex(salt)}-${buf2hex(iv)}-${buf2hex(ciphertext)}`), | |
); | |
} | |
/** | |
* Given a key and ciphertext (in the form of a string) as given by `encrypt`, | |
* this decrypts the ciphertext and returns the original plaintext | |
* @param {String} passphrase | |
* @param {String} saltIvCipherHex | |
* @returns {Promise<String>} | |
*/ | |
function decrypt(passphrase, saltIvCipherHex) { | |
const [salt, iv, data] = saltIvCipherHex.split("-").map(hex2buf); | |
return deriveKey(passphrase, salt) | |
.then(([key]) => crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data)) | |
.then(v => buf2str(new Uint8Array(v))); | |
} | |
// EXAMPLE | |
/* | |
encrypt("hello", "world") | |
.then(v => console.log("ENCRYPTED", v) || v) | |
.then(v => decrypt("hello", v)) | |
.then(v => console.log("DECRYPTED ", v) || v); | |
decrypt( | |
"hello", | |
"6102677198e41d98-84c95e2d7caf6f2d4ccbfe3c-3093cef35d0dba7a24d37f7d4580b5ad83c154329c", | |
).then(console.log); | |
*/ |
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
import hashlib | |
import os | |
from binascii import hexlify, unhexlify | |
from cryptography.hazmat.primitives.ciphers.aead import AESGCM | |
def deriveKey(passphrase: str, salt: bytes=None) -> [str, bytes]: | |
if salt is None: | |
salt = os.urandom(8) | |
return hashlib.pbkdf2_hmac("sha256", passphrase.encode("utf8"), salt, 1000), salt | |
def encrypt(passphrase: str, plaintext: str) -> str: | |
key, salt = deriveKey(passphrase) | |
aes = AESGCM(key) | |
iv = os.urandom(12) | |
plaintext = plaintext.encode("utf8") | |
ciphertext = aes.encrypt(iv, plaintext, None) | |
return "%s-%s-%s" % (hexlify(salt).decode("utf8"), hexlify(iv).decode("utf8"), hexlify(ciphertext).decode("utf8")) | |
def decrypt(passphrase: str, ciphertext: str) -> str: | |
salt, iv, ciphertext = map(unhexlify, ciphertext.split("-")) | |
key, _ = deriveKey(passphrase, salt) | |
aes = AESGCM(key) | |
plaintext = aes.decrypt(iv, ciphertext, None) | |
return plaintext.decode("utf8") | |
if __name__ == "__main__": | |
ciphertext = encrypt("hello", "world") | |
print(ciphertext) | |
print(decrypt("hello", ciphertext)) | |
print(decrypt("hello", "6102677198e41d98-84c95e2d7caf6f2d4ccbfe3c-3093cef35d0dba7a24d37f7d4580b5ad83c154329c")) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You're right. Tested and confirmed. Thanks!