The example here shows how to encrypt and decrypt data using python in a way that is fully compatible with openssl aes-256-cbc. It is based on the work that I did in C++ Cipher class that is published on this site. It works for both python-2.7 and python-3.x.
The key idea is based on the way that openssl generates the key and iv data from password as well as the “Salted__” prefix that it uses.
The complete routine can be downloaded here: mycrypt.py.
When you download the script from this section you will be able to perform operations like this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$ # Encrypt and decrypt using mycrypt.py. $ echo 'Lorem ipsum dolor sit amet' |\ ./mycrypt.py -e -p secret |\ ./mycrypt.py -d -p secret Lorem ipsum dolor sit amet $ # Encrypt using mycrypt.py and decrypt using openssl $ # with the SHA512 message digest. $ echo 'Lorem ipsum dolor sit amet' |\ ./mycrypt.py -e -m sha512 -p secret |\ openssl enc -d -aes-256-cbc -md sha512 -base64 -salt -pass pass:secret Lorem ipsum dolor sit amet $ # Encrypt using openssl and decrypt using mycrypt.py. $ echo 'Lorem ipsum dolor sit amet' |\ openssl enc -e -aes-256-cbc -md md5 -base64 -salt -pass pass:secret |\ ./mycrypt.py -d -p secret Lorem ipsum dolor sit amet $ # Encrypt and decrypt using openssl. openssl enc -e -aes-256-cbc -md md5 -base64 -salt -pass pass:secret |\ openssl enc -d -aes-256-cbc -md md5 -base64 -salt -pass pass:secret Lorem ipsum dolor sit amet |
Basic Routines
The code shown below implements the encrypt and decrypt routines.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
#!/usr/bin/env python ''' Implement openssl compatible AES-256 CBC mode encryption/decryption. This module provides encrypt() and decrypt() functions that are compatible with the openssl algorithms. This is basically a python encoding of my C++ work on the Cipher class using the Crypto.Cipher.AES class. URL: http://projects.joelinoff.com/cipher-1.1/doxydocs/html/ ''' # LICENSE # # MIT Open Source # # Copyright (c) 2014 Joe Linoff # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import argparse import base64 import os import re import hashlib import inspect import sys from getpass import getpass from Crypto.Cipher import AES VERSION='1.2' # ================================================================ # debug # ================================================================ def _debug(msg, lev=1): ''' Print a debug message with some context. ''' sys.stderr.write('DEBUG:{} ofp {}\n'.format(inspect.stack()[lev][2], msg)) # ================================================================ # get_key_and_iv # ================================================================ def get_key_and_iv(password, salt, klen=32, ilen=16, msgdgst='md5'): ''' Derive the key and the IV from the given password and salt. This is a niftier implementation than my direct transliteration of the C++ code although I modified to support different digests. CITATION: http://stackoverflow.com/questions/13907841/implement-openssl-aes-encryption-in-python @param password The password to use as the seed. @param salt The salt. @param klen The key length. @param ilen The initialization vector length. @param msgdgst The message digest algorithm to use. ''' # equivalent to: # from hashlib import <mdi> as mdf # from hashlib import md5 as mdf # from hashlib import sha512 as mdf mdf = getattr(__import__('hashlib', fromlist=[msgdgst]), msgdgst) password = password.encode('ascii', 'ignore') # convert to ASCII try: maxlen = klen + ilen keyiv = mdf(password + salt).digest() tmp = [keyiv] while len(tmp) < maxlen: tmp.append( mdf(tmp[-1] + password + salt).digest() ) keyiv += tmp[-1] # append the last byte key = keyiv[:klen] iv = keyiv[klen:klen+ilen] return key, iv except UnicodeDecodeError: return None, None # ================================================================ # encrypt # ================================================================ def encrypt(password, plaintext, chunkit=True, msgdgst='md5'): ''' Encrypt the plaintext using the password using an openssl compatible encryption algorithm. It is the same as creating a file with plaintext contents and running openssl like this: $ cat plaintext <plaintext> $ openssl enc -e -aes-256-cbc -base64 -salt \\ -pass pass:<password> -n plaintext @param password The password. @param plaintext The plaintext to encrypt. @param chunkit Flag that tells encrypt to split the ciphertext into 64 character (MIME encoded) lines. This does not affect the decrypt operation. @param msgdgst The message digest algorithm. ''' salt = os.urandom(8) key, iv = get_key_and_iv(password, salt, msgdgst=msgdgst) if key is None: return None # PKCS#7 padding padding_len = 16 - (len(plaintext) % 16) if isinstance(plaintext, str): padded_plaintext = plaintext + (chr(padding_len) * padding_len) else: # assume bytes padded_plaintext = plaintext + (bytearray([padding_len] * padding_len)) # Encrypt cipher = AES.new(key, AES.MODE_CBC, iv) ciphertext = cipher.encrypt(padded_plaintext) # Make openssl compatible. # I first discovered this when I wrote the C++ Cipher class. # CITATION: http://projects.joelinoff.com/cipher-1.1/doxydocs/html/ openssl_ciphertext = b'Salted__' + salt + ciphertext b64 = base64.b64encode(openssl_ciphertext) if not chunkit: return b64 LINELEN = 64 chunk = lambda s: b'\n'.join(s[i:min(i+LINELEN, len(s))] for i in range(0, len(s), LINELEN)) return chunk(b64) # ================================================================ # decrypt # ================================================================ def decrypt(password, ciphertext, msgdgst='md5'): ''' Decrypt the ciphertext using the password using an openssl compatible decryption algorithm. It is the same as creating a file with ciphertext contents and running openssl like this: $ cat ciphertext # ENCRYPTED <ciphertext> $ egrep -v '^#|^$' | \\ openssl enc -d -aes-256-cbc -base64 -salt -pass pass:<password> -in ciphertext @param password The password. @param ciphertext The ciphertext to decrypt. @param msgdgst The message digest algorithm. @returns the decrypted data. ''' # unfilter -- ignore blank lines and comments if isinstance(ciphertext, str): filtered = '' nl = '\n' re1 = r'^\s*$' re2 = r'^\s*#' else: filtered = b'' nl = b'\n' re1 = b'^\\s*$' re2 = b'^\\s*#' for line in ciphertext.split(nl): line = line.strip() if re.search(re1,line) or re.search(re2, line): continue filtered += line + nl # Base64 decode raw = base64.b64decode(filtered) assert(raw[:8] == b'Salted__' ) salt = raw[8:16] # get the salt # Now create the key and iv. key, iv = get_key_and_iv(password, salt, msgdgst=msgdgst) if key is None: return None # The original ciphertext ciphertext = raw[16:] # Decrypt cipher = AES.new(key, AES.MODE_CBC, iv) padded_plaintext = cipher.decrypt(ciphertext) if isinstance(padded_plaintext, str): padding_len = ord(padded_plaintext[-1]) else: padding_len = padded_plaintext[-1] plaintext = padded_plaintext[:-padding_len] return plaintext |
The Rest of the Program
The following code shows how to use the previous code to create tool that will encrypt and decrypt in the command line.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# include the code above ... # ================================================================ # _open_ios # ================================================================ def _open_ios(args): ''' Open the IO files. ''' ifp = sys.stdin ofp = sys.stdout if args.input is not None: try: ifp = open(args.input, 'rb') except IOError: print('ERROR: cannot read file: {}'.format(args.input)) sys.exit(1) if args.output is not None: try: ofp = open(args.output, 'wb') except IOError: print('ERROR: cannot write file: {}'.format(args.output)) sys.exit(1) return ifp, ofp # ================================================================ # _close_ios # ================================================================ def _close_ios(ifp, ofp): ''' Close the IO files if necessary. ''' if ifp != sys.stdin: ifp.close() if ofp != sys.stdout: ofp.close() # ================================================================ # _write # ================================================================ def _write(ofp, out, newline=False): ''' Write out the data in the correct format. ''' if ofp == sys.stdout and isinstance(out, bytes): out = out.decode('utf-8', 'ignored') ofp.write(out) if newline: ofp.write('\n') elif isinstance(out, str): ofp.write(out) if newline: ofp.write('\n') else: # assume bytes ofp.write(out) if newline: ofp.write(b'\n') # ================================================================ # _write # ================================================================ def _read(ifp): ''' Read the data in the correct format. ''' return ifp.read() # ================================================================ # _runenc # ================================================================ def _runenc(args): ''' Encrypt data. ''' if args.passphrase is None: while True: passphrase = getpass('Passphrase: ') tmp = getpass('Re-enter passphrase: ') if passphrase == tmp: break print('') print('Passphrases do not match, please try again.') else: passphrase = args.passphrase ifp, ofp = _open_ios(args) text = _read(ifp) out = encrypt(passphrase, text, msgdgst=args.msgdgst) _write(ofp, out, True) _close_ios(ifp, ofp) # ================================================================ # _rundec # ================================================================ def _rundec(args): ''' Decrypt data. ''' if args.passphrase is None: passphrase = getpass('Passphrase: ') else: passphrase = args.passphrase ifp, ofp = _open_ios(args) text = _read(ifp) out = decrypt(passphrase, text, msgdgst=args.msgdgst) _write(ofp, out, False) _close_ios(ifp, ofp) # ================================================================ # _runtest # ================================================================ def _runtest(args): ''' Run a series of iteration where each iteration generates a random password from 8-32 characters and random text from 20 to 256 characters. The encrypts and decrypts the random data. It then compares the results to make sure that everything works correctly. The test output looks like this: $ crypt 2000 2000 of 2000 100.00% 15 139 2000 0 $ # ^ ^ ^ ^ ^ ^ $ # | | | | | +-- num failed $ # | | | | +---------- num passed $ # | | | +-------------- size of text for a test $ # | | +----------------- size of passphrase for a test $ # | +-------------------------- percent completed $ # +------------------------------- total # #+------------------------------------ current test @param args The args parse arguments. ''' import string import random from random import randint # Encrypt/decrypt N random sets of plaintext and passwords. num = args.test ofp = sys.stdout if args.output is not None: try: ofp = open(args.output, 'w') except IOError: print('ERROR: can open file for writing: {}'.format(args.output)) sys.exit(1) chset = string.printable passed = 0 failed = [] maxlen = len(str(num)) for i in range(num): ran1 = randint(8,32) password = ''.join(random.choice(chset) for x in range(ran1)) ran2 = randint(20, 256) plaintext = ''.join(random.choice(chset) for x in range(ran2)) ciphertext = encrypt(password, plaintext, msgdgst=args.msgdgst) verification = decrypt(password, ciphertext, msgdgst=args.msgdgst) if plaintext != verification: failed.append( [password, plaintext] ) else: passed += 1 output = '%*d of %d %6.2f%% %3d %3d %*d %*d %s' % (maxlen,i+1, num, 100*(i+1)/num, len(password), len(plaintext), maxlen, passed, maxlen, len(failed), args.msgdgst) if args.output is None: ofp.write('\b'*80) ofp.write(output) ofp.flush() else: ofp.write(output+'\n') ofp.write('\n') if len(failed): for i in range(len(failed)): ofp.write('%3d %2d %-34s %3d %s\n' % (i, len(failed[i][0]), '"'+failed[i][0]+'"', len(failed[i][1]), '"'+failed[i][1]+'"')) ofp.write('\n') if args.output is not None: ofp.close() # ================================================================ # _cli_opts # ================================================================ def _cli_opts(): ''' Parse command line options. @returns the arguments ''' mepath = os.path.abspath(sys.argv[0]).encode('utf-8') mebase = os.path.basename(mepath) description = ''' Implements encryption/decryption that is compatible with openssl AES-256 CBC mode. You can use it as follows: EXAMPLE 1: {0} -> {0} (MD5) $ # Encrypt and decrypt using {0}. $ echo 'Lorem ipsum dolor sit amet' | \\ {0} -e -p secret | \\ {0} -d -p secret Lorem ipsum dolor sit amet EXAMPLE 2: {0} -> openssl (MD5) $ # Encrypt using {0} and decrypt using openssl. $ echo 'Lorem ipsum dolor sit amet' | \\ {0} -e -p secret | \\ openssl enc -d -aes-256-cbc -md md5 -base64 -salt -pass pass:secret Lorem ipsum dolor sit amet EXAMPLE 3: openssl -> {0} (MD5) $ # Encrypt using openssl and decrypt using {0} $ echo 'Lorem ipsum dolor sit amet' | \\ openssl enc -e -aes-256-cbc -md md5 -base64 -salt -pass pass:secret {0} -d -p secret Lorem ipsum dolor sit amet EXAMPLE 4: openssl -> openssl (MD5) $ # Encrypt and decrypt using openssl $ echo 'Lorem ipsum dolor sit amet' | \\ openssl enc -e -aes-256-cbc -md md5 -base64 -salt -pass pass:secret openssl enc -d -aes-256-cbc -md md5 -base64 -salt -pass pass:secret Lorem ipsum dolor sit amet EXAMPLE 5: {0} -> {0} (SHA512) $ # Encrypt and decrypt using {0}. $ echo 'Lorem ipsum dolor sit amet' | \\ {0} -e -m sha512 -p secret | \\ {0} -d -m sha512 -p secret Lorem ipsum dolor sit amet EXAMPLE 6: {0} -> openssl (SHA512) $ # Encrypt using {0} and decrypt using openssl. $ echo 'Lorem ipsum dolor sit amet' | \\ {0} -e -m sha512 -p secret | \\ openssl enc -d -aes-256-cbc -md sha1=512 -base64 -salt -pass pass:secret Lorem ipsum dolor sit amet EXAMPLE 7: $ # Run internal tests. $ {0} -t 2000 2000 of 2000 100.00%% 21 104 2000 0 md5 $ # ^ ^ ^ ^ ^ ^ ^ $ # | | | | | | +- message digest $ # | | | | | +--- num failed $ # | | | | +----------- num passed $ # | | | +--------------- size of text for a test $ # | | +------------------ size of passphrase for a test $ # | +--------------------------- percent completed $ # +-------------------------------- total # #+------------------------------------- current test '''.format(mebase.decode('ascii', 'ignore')) parser = argparse.ArgumentParser(prog=mebase, formatter_class=argparse.RawDescriptionHelpFormatter, description=description, ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument('-d', '--decrypt', action='store_true', help='decryption mode') group.add_argument('-e', '--encrypt', action='store_true', help='encryption mode') parser.add_argument('-i', '--input', action='store', help='input file, default is stdin') parser.add_argument('-m', '--msgdgst', action='store', default='md5', help='message digest (md5, sha, sha1, sha256, sha512), default is md5') parser.add_argument('-o', '--output', action='store', help='output file, default is stdout') parser.add_argument('-p', '--passphrase', action='store', help='passphrase for encrypt/decrypt operations') group.add_argument('-t', '--test', action='store', default=-1, type=int, help='test mode (TEST is an integer)') parser.add_argument('-v', '--verbose', action='count', help='the level of verbosity') parser.add_argument('-V', '--version', action='version', version='%(prog)s '+VERSION) args = parser.parse_args() return args # ================================================================ # main # ================================================================ def main(): args = _cli_opts() if args.test > 0: if args.input is not None: print('WARNING: input argument will be ignored.') if args.passphrase is not None: print('WARNING: passphrase argument will be ignored.') _runtest(args) elif args.encrypt: _runenc(args) elif args.decrypt: _rundec(args) # ================================================================ # MAIN # ================================================================ if __name__ == "__main__": main() |
Updated to version 1.1 to support different message digests.
Hi,
Thanks great, If I want decrypt a message with non ASCII secert, what do I do?
Is it possible similar -i option for password?
_regards
You might consider using the binascii package to hexlify it.