Skip to content

Instantly share code, notes, and snippets.

@tscholl2
Last active Aug 9, 2021
Embed
What would you like to do?
simple AES encryption/decryption example with PBKDF2 key derivation in Go, Javascript, and Python
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"))
}
/**
* 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);
*/
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"))
@ghost

This comment has been minimized.

Copy link

@ghost ghost commented Feb 1, 2018

I don't see a license here. Is this freely usable? Thanks.

@tscholl2

This comment has been minimized.

Copy link
Owner Author

@tscholl2 tscholl2 commented Mar 20, 2018

Sorry I didn't see this comment. You're welcome to use any of this for free. It's not really anything besides how to use the standard libraries.

@ghost

This comment has been minimized.

Copy link

@ghost ghost commented Apr 1, 2018

No problem. Thank you for the answer.

@alidehghan

This comment has been minimized.

Copy link

@alidehghan alidehghan commented May 12, 2018

Many thanks for this useful gist

@meetgyn

This comment has been minimized.

Copy link

@meetgyn meetgyn commented Jul 9, 2020

Could I decrypt this password?
:pbkdf2:sha256:10000:128:tMG5uKJ5egwoZu8SEy9xlw==:U+5GmBqgfuD/GjUe7zy/n2MAdU64E6oO9SUT7qjsG1mw2ezSiW2WH2lPgxPKZA7ZHXs3+4Birh01GKRG+V8DeEXlo8DlcgWcIx+NaeWWTMN8u3nzPdgah548UWksu8SIH9iGmjnA3UKCfs8qFCQ7OPGA+rZejrfoTUAJywKTaHM=

@OGmetamonkey

This comment has been minimized.

Copy link

@OGmetamonkey OGmetamonkey commented Aug 9, 2021

I notice the python implementation of AESGCM uses the hmac modifier rather than just pbkdf2. Would this not lead to problems with the other provided implementations? Specifically the js implementation that utilizes crypto-subtle's PBKDF2 key derivation but does not include the HMAC modifier?

@tscholl2

This comment has been minimized.

Copy link
Owner Author

@tscholl2 tscholl2 commented Aug 9, 2021

I notice the python implementation of AESGCM uses the hmac modifier rather than just pbkdf2. Would this not lead to problems with the other provided implementations? Specifically the js implementation that utilizes crypto-subtle's PBKDF2 key derivation but does not include the HMAC modifier?

Good question. I believe the JS implementation is using HMAC in the PRF. I have two reasons to believe this:

  1. The prf in the pbkdf2-params object is a MAC generation function https://www.w3.org/TR/WebCryptoAPI/#pbkdf2-params
  2. All the implementations should be able to encrypt/decrypt the same data. If the python and js versions used a different key derivation method, we should not expect that to work. To test this yourself, you can run the code samples and see. If they don't both decrypt the same string to the same value using the same password, that would be a problem!
@OGmetamonkey

This comment has been minimized.

Copy link

@OGmetamonkey OGmetamonkey commented Aug 9, 2021

I notice the python implementation of AESGCM uses the hmac modifier rather than just pbkdf2. Would this not lead to problems with the other provided implementations? Specifically the js implementation that utilizes crypto-subtle's PBKDF2 key derivation but does not include the HMAC modifier?

Good question. I believe the JS implementation is using HMAC in the PRF. I have two reasons to believe this:

  1. The prf in the pbkdf2-params object is a MAC generation function https://www.w3.org/TR/WebCryptoAPI/#pbkdf2-params
  2. All the implementations should be able to encrypt/decrypt the same data. If the python and js versions used a different key derivation method, we should not expect that to work. To test this yourself, you can run the code samples and see. If they don't both decrypt the same string to the same value using the same password, that would be a problem!

You're right. Tested and confirmed. Thanks!

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