Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
181 lines (138 sloc) 4.71 KB
a key wrapping/unwrapping pair using bcrypt as a PBKDF
arbitrary sized payload -- protect any number of key bits -- allows anti-forensic splitting
no static fingerprint -- hashed fingerprint is optional
# Public Domain:
# To the extent possible under law, Terrel Shumway (
# has waived all copyright and related or neighboring rights to this
# programming example.
import time
import struct
from bcrypt import kdf as bckdf
from hashlib import sha256
from nacl.utils import random
from nacl.secret import SecretBox
# some lovely constants for this "encoding"
MAGIC = '\x08\xc1/g\x1e\xb9\xea'
MAXWF = 32
def wrapkey( password, key, workfactor=DEFWF ):
"""encrypt a key using a password and bcrypt
returns magic,salt,ciphertext
assert 0<=workfactor<MAXWF
salt = random(SALT_SIZE)
# we want to hash the magic to avoid leaving a fingerprint on the wrapped key data
magic = sha256(salt + MAGIC + chr(workfactor)).digest()[:MAGIC_SIZE]
kek = bckdf(password,salt,SecretBox.KEY_SIZE,1<<workfactor)
sbox = SecretBox(kek)
nonce = random(sbox.NONCE_SIZE)
payload = sbox.encrypt(key,nonce)
return magic , salt , payload
def splitwrapped( wrapped, magic=True):
if magic:
magic = wrapped[:MAGIC_SIZE]
magic = ""
salt = wrapped[i:i+SALT_SIZE]
payload = wrapped[i:]
return magic,salt,payload
def unwrapkey( password, (magic,salt,payload), workfactor=MAXWF ):
"""decrypt the key encrypted by the password-based key derived with bcrypt
if magic:
# brute force search for the correct workfactor
for workfactor in range(MAXWF+1):
# XXX: use constant time compare instead
if magic == sha256(salt + MAGIC + chr(workfactor)).digest()[:MAGIC_SIZE]:
if workfactor >= MAXWF:
# if we reach here, the magic is bad, but we'll pretend it was good
# to mitigate timing attacks
workfactor = 9 # fixed forever
kek = bckdf(password,salt,SecretBox.KEY_SIZE,1<<workfactor)
sbox = SecretBox(kek)
return sbox.decrypt(payload)
class BCryptBox(object):
def __init__(self,password,workfactor=DEFWF):
self.password = password
self.workfactor = workfactor
def encrypt(self,key):
return "".join(wrapkey(self.password,key,self.workfactor))
def decrypt(self,wrapped):
return unwrapkey(self.password,splitwrapped(wrapped),self.workfactor)
import AfSplitter
def afsplit(data,blocks,stripes,hash):
dlen = len(data)
stride = dlen/blocks
while i<dlen:
j = i+stride
block = data[i:j]
fat = AfSplitter.AFSplit(block,stripes,hash)
yield fat
i = j
def afmerge(data,blocks,stripes,hash):
dlen = len(data)
stride = dlen/blocks
k = 0
while i<dlen:
j = i+stride
fat = data[i:j]
block = AfSplitter.AFMerge(fat,stripes,hash)
yield block
i = j
class AFBCryptBox(BCryptBox):
"""A BCryptBox that expands the keyfile to 16 kiB to
thwart forensic recovery after deletion
def __init__(self,password,workfactor=DEFWF,blocks=4,stripes=256,hash="sha1"):
"""default values will expand 128 B to 16 kiB
self.blocks = blocks
self.stripes = stripes
self.hash = hash
def encrypt(self,key):
data = super(AFBCryptBox,self).encrypt(key)
return "".join(afsplit(data,self.blocks,self.stripes,self.hash))
def decrypt(self,wrapped):
data = "".join(afmerge(wrapped,self.blocks,self.stripes,self.hash))
return super(AFBCryptBox,self).decrypt(data)
except ImportError:
if __name__=="__main__":
from nacl.encoding import Base64Encoder
e64 = Base64Encoder.encode
pw = "password"
key = random()
print len(key),e64(key)
w = wrapkey(pw,key)
ww = "".join(w)
print len(ww),e64(ww)
k = unwrapkey(pw,w)
print len(k),e64(k)
key = random(64)
bbox = BCryptBox(pw)
w = bbox.encrypt(key)
print len(w)
k = bbox.decrypt(w)
assert key == k
key = random(128)
afbox = AFBCryptBox(pw,blocks=16)
w = afbox.encrypt(key)
print len(w)
k = afbox.decrypt(w)
assert key == k