Design
Need Files or Filenames
We will create a file for the following:
- The Key Passphrase or
self.kp
that will be used to generate the Key to encrypt and decrypt our passwords.- It will consist of just a randomly generated string of characters.
- It will be generated by os.urandom with 64 Bytes (or 512bits).
- Passwords will be stored in and serialized so that it is not human readable.
- Python pickle will be used.
- Its like JSON, but not in clear text. Pickle still uses the key = value concept.
- Passwords will be encrypted at rest or while it’s not being used.
Salting
- All passwords, keys, and so on will all have extra bits (or salt) added to them.
- The program generate its salt or know it by using a static init salt seed, but the randomness of the salt will be the name of the password.
- For example, if the password name is a username called foobar, the salt will be generated by using the static salt SEED characters and the name of the password, “foobar”.
Define Init Variables
- SEED – Initial salt for all hashes
- KP_File – Location of the key password file
- SDB_FILE – Location of Secrets “Database” file
- PASSPHRASE_SIZE – Size in bytes
- KEY_SIZE – Key Size in bytes
- BLOCK_SIZE – Size in bytes that will pad the password
- IV_SIZE – Size of initial vector that will be used to “Salt” the cipher
- SALT_SIZE – Standard salt size in bytes
Python Functions
Load Key Password File and Load Password File
- This function or method will load the “Key Password” into memory if the file exists.
- It will create the file if it doesn’t.
- will load the passwords or SDB_FILE into memory via PICKLE.loads().
- If the file doesn’t exist, it will create one.
- All will be done when the class is instantiated.
def __init__(self):
try:
with open(self.KP_FILE) as f:
self.kp = f.read()
if len(self.kp) == 0: raise IOError
except IOError:
with open(self.KP_FILE, 'w') as f:
# Generate Random kp
self.kp = os.urandom(self.PASSPHRASE_SIZE)
f.write(base64.b64encode(self.kp))
try:
# If the kp has to be regenerated, then the old data in the SDB file can no longer be used and should be removed
if os.path.exists(self.SDB_FILE):
os.remove(self.SDB_FILE)
except:
print(traceback.format_exc())
print("There might be an error with permissions for the SDB_FILE {}".format(self.SDB_FILE))
else:
# decode from base64
self.kp = base64.b64decode(self.kp)
# Load or create SDB_FILE:
try:
with open(self.SDB_FILE) as f:
self.sdb = pickle.load(f)
# sdb will be a dictionary that will have key, value pairs
if self.sdb == {}: raise IOError
except (IOError, EOFError):
self.sdb = {}
with open(self.SDB_FILE, 'w') as f:
pickle.dump(self.sdb, f)
getSaltForPname
This function or method is used to derive the salt per key, username, pname or what ever you want to call it. It is hashed and salted from the static salt seed or SEED variable.
def getSaltForPname(self, pname):
# Salt is generated as the hash of the key with it's own salt acting like a seed value
return PBKDF2(pname, self.SEED).read(self.SALT_SIZE)
Encrypt Password
The encrypt function or method is called when you want to encrypt a new password. To encrypt a password, just run the python file by itself.
python neagan.py
It requires the following input variables:
- pname – For Password Name or could just be the username.
- Key = pname
- value = password
- p – Password being entered in clear text or the data you want encrypted
The encrypted data is:
- padded with the BLOCK_SIZE length with a bunch of spaces at the end of the data
- stored in the SDB_FILE by salting or prepending the encrypted data with a os.urandom() number of bytes called the “initial vector”.
You will be prompted to:
- enter the username
- enter the value for the username
- verify by reentering the same value
- it will then encrypt it and store in the SDB_FILE
def encrypt(self, pname, p):
' Pad p, then encrypt it with a new, randomly initialised cipher.
Will not preserve trailing whitespace in plaintext!'''
# Initialise Cipher Randomly
initVector = os.urandom(self.IV_SIZE)
salt = self.getSaltForPname(pname)
# Prepare cipher key that will be used to encrypt and decrypt
k = PBKDF2(self.kp, salt).read(self.KEY_SIZE)
# Create cipher that will be used to encrypt the data
cipher = AES.new(k, AES.MODE_CBC, initVector)
# Pad and encrypt
self.sdb[pname] = initVector + cipher.encrypt(p + ' '*(self.BLOCK_SIZE - (len(p) % self.BLOCK_SIZE)))
with open(self.SDB_FILE, 'w') as f:
pickle.dump(self.sdb, f)
Decrypt Password
The decrypt method or function is used to decrypt the data or password.
It requires the following input variables:
- pname – this is just the username or key that you want to decrypt the value, data, or password from
It will:
- Get the encrypted data from the SDB_FILE for the key or ‘pname’ provided that is made up of:
- the initial vector that is prepending the encrypted data
- the encrypted data
- the padding of spaces appended to the end of the encrypted data
- Recreate an identical cipher key or hash by:
- generating the same salt or hash by using the same pname
- using the same passphrase or key password
- This is retrieved from the KP_FILE
- Get InitVector or IV by specifying the length of the IV_SIZE:
- We know that the IV was prepended
- The length of the IV is defined by the “IV_SIZE” variable
- Get Encrypted data with padded spaces:
- We know that length of the IV was prepended
- So, everything after that, is our encrypted data with padding (or spaces)
- Recreates the cipher we used to encrypt with the cipher key above
- Finally, once the data is decrypted:
- We strip off the spaces
def decrypt(self, pname):
' Reconstruct the cipher object and decrypt. Will not preserve trailing whitespace in the retrieved value!'''
self.sdb[pname]
salt = self.getSaltForPname(pname)
# Recreate an identical cipher key:
key = PBKDF2(self.kp, salt).read(self.KEY_SIZE)
# Get initVector (salt) that was concatenated into the encrypted Data stored in the SDB_FILE
initVector = self.sdb[pname][:self.IV_SIZE]
# Get only the data you want to decrypt
encryptedData = self.sdb[pname][self.IV_SIZE:]
# Recreate cipher
cipher = AES.new(key, AES.MODE_CBC, initVector)
# Decrypt and depad
return cipher.decrypt(encryptedData).rstrip(' ')
Python Modules
- os.urandom – “Return a string of n random bytes suitable for cryptographic use”
- pickle – Serialized Data in a key = value format
- PBKDF2 – Used to make Encryption Keys but can be used for hashing too.
- pycrypto – Used to encrypt and decrypt data with a key
- It’s also has a hashing algorithm
Python Code Example
Now, let’s put it all together…. I used a class because I felt based on my use case state was important.
import os
import base64
import traceback
import pickle
from getpass import getpass
from pbkdf2 import PBKDF2
from Crypto.Cipher import AES
class WeAreNeagan(object):
SEED = 'mkhgts465wef4fwtdd' # MAKE THIS YOUR OWN RANDOM STRING
KP_FILE = './kfileNsxConfigVerfiy.p'
SDB_FILE = './sdbfileNsxConfigVerify'
PASSPHRASE_SIZE = 64 # 512-bit passphrase
KEY_SIZE = 32 # 256-bit key
BLOCK_SIZE = 16 # 16-bit blocks
IV_SIZE = 16 # 128-bits to initialize
SALT_SIZE = 8 # 64-bits of salt
def __init__(self):
try:
with open(self.KP_FILE) as f:
self.kp = f.read()
if len(self.kp) == 0: raise IOError
except IOError:
with open(self.KP_FILE, 'w') as f:
# Generate Random kp
self.kp = os.urandom(self.PASSPHRASE_SIZE)
f.write(base64.b64encode(self.kp))
try:
# If the kp has to be regenerated, then the old data in the SDB file can no longer be used and should be removed
if os.path.exists(self.SDB_FILE):
os.remove(self.SDB_FILE)
except:
print(traceback.format_exc())
print("There might be an error with permissions for the SDB_FILE {}".format(self.SDB_FILE))
else:
# decode from base64
self.kp = base64.b64decode(self.kp)
# Load or create SDB_FILE:
try:
with open(self.SDB_FILE) as f:
self.sdb = pickle.load(f)
# sdb will be a dictionary that will have key, value pairs
if self.sdb == {}: raise IOError
except (IOError, EOFError):
self.sdb = {}
with open(self.SDB_FILE, 'w') as f:
pickle.dump(self.sdb, f)
def getSaltForPname(self, pname):
# Salt is generated as the hash of the key with it's own salt acting like a seed value
return PBKDF2(pname, self.SEED).read(self.SALT_SIZE)
# Encrypt Password
def encrypt(self, pname, p):
' Pad p, then encrypt it with a new, randomly initialised cipher.
Will not preserve trailing whitespace in plaintext!'''
# Initialise Cipher Randomly
initVector = os.urandom(self.IV_SIZE)
salt = self.getSaltForPname(pname)
# Prepare cipher key that will be used to encrypt and decrypt
k = PBKDF2(self.kp, salt).read(self.KEY_SIZE)
# Create cipher that will be used to encrypt the data
cipher = AES.new(k, AES.MODE_CBC, initVector)
# Pad and encrypt
self.sdb[pname] = initVector + cipher.encrypt(p + ' '*(self.BLOCK_SIZE - (len(p) % self.BLOCK_SIZE)))
with open(self.SDB_FILE, 'w') as f:
pickle.dump(self.sdb, f)
# Decrypt Password
def decrypt(self, pname):
' Reconstruct the cipher object and decrypt. Will not preserve trailing whitespace in the retrieved value!'''
self.sdb[pname]
salt = self.getSaltForPname(pname)
# Recreate an identical cipher key:
key = PBKDF2(self.kp, salt).read(self.KEY_SIZE)
# Get initVector (salt) that was concatenated into the encrypted Data stored in the SDB_FILE
initVector = self.sdb[pname][:self.IV_SIZE]
# Get only the data you want to decrypt
encryptedData = self.sdb[pname][self.IV_SIZE:]
# Recreate cipher
cipher = AES.new(key, AES.MODE_CBC, initVector)
# Decrypt and depad
return cipher.decrypt(encryptedData).rstrip(' ')
if __name__ == '__main__':
pname = raw_input("Please Enter Username: ")
p = getpass("Please enter a value for {}: ".format(pname))
p2 = getpass("To verfiy, Please renter the value for {}: ".format(pname))
while p != p2:
print('Values DO NOT match')
p = getpass("Please enter a value for {}: ".format(pname))
p2 = getpass("To verfiy, Please reenter the value for {}: ".format(pname))
johnny = WeAreNeagan()
# Encrypting and Storing
johnny.encrypt(pname, p)
print('Encryption Complete!')
# print('Your password is...')
# print johnny.decrypt(pname)
References
I need to securely store a username and password in Python, what are my options? – Most of this is based on the post by drodgers. It’s cleaned up, converted to a class, added and removed some stuff… but I really liked the way to get the IV, salt and so on. Either way if someone gets root or the code, you are hosed either way.
Can you provide the code to pass the Encrypted String to an Oracle DB to run queries?
Hi Jason,
I assume you are talking about Python code? What type of encryption are you looking for? I read up on it and it looks like you can do base64, which isn’t encryption, but just making it non-human readable. What exactly are you looking to do? I do have a project that requires me to talk to an Oracle DB, so this might be a good time for me to answer your question.
Thanks!
Hi Daniel,
I’m not looking for anything fancy since I only have RO access anyway. Just want a script that I can call to run queries using encrypted passwords.
sqlplus login/encrypted_password < script_file
Thanks
Hi Jason, If your oracle DB supports encryption, which is sounds like it does there the password will be encrypted during the connection negotiation. From what I see you need to use the python module, “cx_oracle”. There is a lot of documentation out there. Here is the “read the docs” link… https://cx-oracle.readthedocs.io/en/latest/installation.html
What I have found to be the most useful when using python and database connections is use to an IDE that you can “break” to see what the variable is returning. I suggest downloading Pycharm, there’s a community version that is great for working with Python. Let me know if you need something more. Still have gotten access to my oracle DB so have needed to test it out yet. Sorry.
You may use Oracle wallets for that.