Python Cryptography Experimentation Kit
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.

PCR - Python Cryptography Experimentation Kit

Build Coverage PyPI Audited

This package provides pure python implementations of various cryptographic algorithms and protocols. Since doing cryptography in Python is inherently slow, it is meant as a study aid and experimentation kit.

Before you use PCR

You should know:

  • AES encryption in PCR is slow
  • PCR has not been independently audited
  • Using a cryptographic library securely is difficult

Make sure you read and understand this document before using PCR.

Currently, PCR provides the following:

  • AES
  • Cipher-Block Chaining Mode of Operation
  • PKCS7 Padding
  • PBKDF2 (Key Derivation)
  • Diffie-Hellman Key Exchange (with RFC3526 groups)
  • RC4 (Stream Cipher)
  • HOTP (e.g. for Google Authenticator)

Additional modules (especially regarding Public-Key cryptography) are in the works.

Note: PCR does not (and will never) work with Python 2.x.


The easiest way to install PCR is using PIP:

sudo pip3 install pcr


If you have cloned this repository, you can buld pcr in a virtualenv and run the test suite like this:

make test


PCR comes with a makefile that builds a virtualenv in your working directory. Assuming you have make, curl, and python3, you can run the test suite by typing

make test doctests

And you can build wheels by typing

make wheels

The process for making a release is as follows:

make test doctest docs

If everything checks out, update the version number in and make a release commit:

git commit -am "Release <version-number>"
git push origin master

Finally upload the distribution to pypi

bin/python bdist_wheel sdist upload
bin/python upload_docs --upload-dir=docs/pcr


AES Encryption

Here's an example of encrypting some data using AES:

>>> import os
>>> import pcr.aes
>>> import pcr.cbc
>>> import pcr.pkcs7
>>> plaintext = 'hello world!'

First we need to encode and pad our plaintext. Padding is a critical step that makes the length of our data divisble by the block size of the cipher. Note that padding has security implications, so do not invent your own. We use PKCS7 padding.

>>> plaintext = plaintext.encode()
>>> plaintext = pcr.pkcs7.pad(plaintext, pcr.aes.AES.block_size)
>>> plaintext
b'hello world!\x04\x04\x04\x04'

Next we instantiate of mode of operation, which lets us use AES on arbitrary length data. This is where we need our initialization vector (IV). The IV is not a secret. With CBC, each block is xored with the previous block so that there are no discernable patterns in your ciphertext. For the first round, when no previous block is available, the IV is used. The IV must be random and you must never use the same IV twice:

>>> iv = b'\xFF' * 16  # actually use this: os.urandom(pcr.aes.AES.block_size)
>>> key = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F'
>>> cbc = pcr.cbc.CBC(pcr.aes.AES, iv)
>>> ciphertext = cbc.encrypt(plaintext, key)
>>> ciphertext

And decryption works exactly the same way:

>>> plaintext = cbc.decrypt(ciphertext, key)
>>> plaintext
b'hello world!\x04\x04\x04\x04'

>>> pcr.pkcs7.check_padding(plaintext, pcr.aes.AES.block_size)
>>> pcr.pkcs7.unpad(plaintext).decode()
'hello world!'

The length of your key determines which variant of AES is used. A 16 byte key is AES128, 24 bytes is AES192 and 32 bytes AES256.

Always call pcr.pkcs7.check_padding() to make sure of the cipher text integrity.

The encrypted object contains not only the ciphertext but also the IV and the key derivation parameters, which we'll look at next.

PBKDF2 - Keys are not the same as passwords

In the example above, we used a 16 byte key (bytes counting up from 0 to 15). To actually do encryption, we need to derive a key from a password. Do NOT use user input as the key for AES encryption. If you want to build a secury crypto system, you absolutely must derive the key.

PCR gives you PBKDF2 ("password-based key derivation function"). It works by stretching the key, e.g. repeatedly hashing it and it uses HMAC to do this. PBKDF2 is purposefully slow.

>>> import hashlib
>>> import pcr.pbkdf2
>>> password = "correcthorsebatterystaple"

Let's set up our parameters:

>>> digestmod = hashlib.sha256
>>> salt = b'\xFF' * 16  # actually use this: os.urandom(32)
>>> count = 1000  # actually make this as large as you can tolerate
>>> dk_length = 32  # make a 32 byte key for AES256

And finally derive our key.

>>> key = pcr.pbkdf2.pbkdf2(digestmod, password.encode(), salt, count, dk_length)
>>> key

The only secret here is the password. When you store the encrypted object somewhere, you need to also store all of the parameters used to generate the key, so you can do the derivation again when you're decrypting. Do not use fixed parameters, you will need to increase the strength of your keys at some point.

In 2015, a good value for the number of iterations of pbkdf2 is 128,000. This takes about a second on a reasonable machine. Double this value every 18 months.


Encryption always needs authentication. Without authentication, the ciphertext can be modified and the entegrity of your decryption is broken.

The recommended method to authenticate encryption is Encrypt-then-MAC, where we run an HMAC on the ciphertext after encryting and before decrypting.

>>> import hmac
>>> import hashlib
>>> digestmod = hashlib.sha256
>>> mac =, ciphertext).digest()
>>> mac

The mac is stored along with your ciphertext, iv, and key derivation parameters. It is not a secret.

Before decrypting the message, we check the message authentication code:

>>> hmac.compare_digest(mac,, ciphertext).digest())

Comparing MACs

We never compare the results of an HMAC using the == operator; instead we use hmac.compare_digest() from the hmac module. In simple terms, if we were to compare message authentication code using ==, an attacker could essentially see how many bytes of the MAC are correct by seeing how long the comparison takes.

The compare_digest() function essentially goes through the values one byte at a time and continues comparing even if it knows they are unequal, ensuring that each comparison takes the same amount of time.

AES – Full Example

>>> password = "correcthorsebatterystaple"
>>> plaintext = "hello world!"

>>> import hmac
>>> import hashlib
>>> import os
>>> import pcr.aes
>>> import pcr.cbc
>>> import pcr.pkcs7
>>> import pcr.pbkdf2


>>> digestmod = hashlib.sha256
>>> salt = os.urandom(32)
>>> count = 128000
>>> dk_length = 32
>>> key = pcr.pbkdf2.pbkdf2(digestmod, password.encode(), salt, count, dk_length)
>>> plaintext = plaintext.encode()
>>> plaintext = pcr.pkcs7.pad(plaintext, pcr.aes.AES.block_size)

>>> cbc = pcr.cbc.CBC(pcr.aes.AES, iv)
>>> ciphertext = cbc.encrypt(plaintext, key)
>>> mac =, ciphertext).digest()


>>> assert hmac.compare_digest(mac,, ciphertext).digest())
>>> plaintext = cbc.decrypt(ciphertext, key)
>>> pcr.pkcs7.check_padding(plaintext, pcr.aes.AES.block_size)
>>> plaintext = pcr.pkcs7.unpad(plaintext)
>>> plaintext = plaintext.decode()
>>> plaintext
'hello world!'


Copyright (c) 2013 Stefano Palazzo

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see