PEM.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. #
  2. # Util/PEM.py : Privacy Enhanced Mail utilities
  3. #
  4. # ===================================================================
  5. #
  6. # Copyright (c) 2014, Legrandin <helderijs@gmail.com>
  7. # All rights reserved.
  8. #
  9. # Redistribution and use in source and binary forms, with or without
  10. # modification, are permitted provided that the following conditions
  11. # are met:
  12. #
  13. # 1. Redistributions of source code must retain the above copyright
  14. # notice, this list of conditions and the following disclaimer.
  15. # 2. Redistributions in binary form must reproduce the above copyright
  16. # notice, this list of conditions and the following disclaimer in
  17. # the documentation and/or other materials provided with the
  18. # distribution.
  19. #
  20. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
  23. # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
  24. # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
  25. # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
  26. # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  27. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  28. # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  29. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  30. # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  31. # POSSIBILITY OF SUCH DAMAGE.
  32. # ===================================================================
  33. __all__ = ['encode', 'decode']
  34. import re
  35. from binascii import a2b_base64, b2a_base64, hexlify, unhexlify
  36. from Crypto.Hash import MD5
  37. from Crypto.Util.Padding import pad, unpad
  38. from Crypto.Cipher import DES, DES3, AES
  39. from Crypto.Protocol.KDF import PBKDF1
  40. from Crypto.Random import get_random_bytes
  41. from Crypto.Util.py3compat import tobytes, tostr
  42. def encode(data, marker, passphrase=None, randfunc=None):
  43. """Encode a piece of binary data into PEM format.
  44. Args:
  45. data (byte string):
  46. The piece of binary data to encode.
  47. marker (string):
  48. The marker for the PEM block (e.g. "PUBLIC KEY").
  49. Note that there is no official master list for all allowed markers.
  50. Still, you can refer to the OpenSSL_ source code.
  51. passphrase (byte string):
  52. If given, the PEM block will be encrypted. The key is derived from
  53. the passphrase.
  54. randfunc (callable):
  55. Random number generation function; it accepts an integer N and returns
  56. a byte string of random data, N bytes long. If not given, a new one is
  57. instantiated.
  58. Returns:
  59. The PEM block, as a string.
  60. .. _OpenSSL: https://github.com/openssl/openssl/blob/master/include/openssl/pem.h
  61. """
  62. if randfunc is None:
  63. randfunc = get_random_bytes
  64. out = "-----BEGIN %s-----\n" % marker
  65. if passphrase:
  66. # We only support 3DES for encryption
  67. salt = randfunc(8)
  68. key = PBKDF1(passphrase, salt, 16, 1, MD5)
  69. key += PBKDF1(key + passphrase, salt, 8, 1, MD5)
  70. objenc = DES3.new(key, DES3.MODE_CBC, salt)
  71. out += "Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,%s\n\n" %\
  72. tostr(hexlify(salt).upper())
  73. # Encrypt with PKCS#7 padding
  74. data = objenc.encrypt(pad(data, objenc.block_size))
  75. elif passphrase is not None:
  76. raise ValueError("Empty password")
  77. # Each BASE64 line can take up to 64 characters (=48 bytes of data)
  78. # b2a_base64 adds a new line character!
  79. chunks = [tostr(b2a_base64(data[i:i + 48]))
  80. for i in range(0, len(data), 48)]
  81. out += "".join(chunks)
  82. out += "-----END %s-----" % marker
  83. return out
  84. def _EVP_BytesToKey(data, salt, key_len):
  85. d = [ b'' ]
  86. m = (key_len + 15 ) // 16
  87. for _ in range(m):
  88. nd = MD5.new(d[-1] + data + salt).digest()
  89. d.append(nd)
  90. return b"".join(d)[:key_len]
  91. def decode(pem_data, passphrase=None):
  92. """Decode a PEM block into binary.
  93. Args:
  94. pem_data (string):
  95. The PEM block.
  96. passphrase (byte string):
  97. If given and the PEM block is encrypted,
  98. the key will be derived from the passphrase.
  99. Returns:
  100. A tuple with the binary data, the marker string, and a boolean to
  101. indicate if decryption was performed.
  102. Raises:
  103. ValueError: if decoding fails, if the PEM file is encrypted and no passphrase has
  104. been provided or if the passphrase is incorrect.
  105. """
  106. # Verify Pre-Encapsulation Boundary
  107. r = re.compile(r"\s*-----BEGIN (.*)-----\s+")
  108. m = r.match(pem_data)
  109. if not m:
  110. raise ValueError("Not a valid PEM pre boundary")
  111. marker = m.group(1)
  112. # Verify Post-Encapsulation Boundary
  113. r = re.compile(r"-----END (.*)-----\s*$")
  114. m = r.search(pem_data)
  115. if not m or m.group(1) != marker:
  116. raise ValueError("Not a valid PEM post boundary")
  117. # Removes spaces and slit on lines
  118. lines = pem_data.replace(" ", '').split()
  119. if len(lines) < 3:
  120. raise ValueError("A PEM file must have at least 3 lines")
  121. # Decrypts, if necessary
  122. if lines[1].startswith('Proc-Type:4,ENCRYPTED'):
  123. if not passphrase:
  124. raise ValueError("PEM is encrypted, but no passphrase available")
  125. DEK = lines[2].split(':')
  126. if len(DEK) != 2 or DEK[0] != 'DEK-Info':
  127. raise ValueError("PEM encryption format not supported.")
  128. algo, salt = DEK[1].split(',')
  129. salt = unhexlify(tobytes(salt))
  130. padding = True
  131. if algo == "DES-CBC":
  132. key = _EVP_BytesToKey(passphrase, salt, 8)
  133. objdec = DES.new(key, DES.MODE_CBC, salt)
  134. elif algo == "DES-EDE3-CBC":
  135. key = _EVP_BytesToKey(passphrase, salt, 24)
  136. objdec = DES3.new(key, DES3.MODE_CBC, salt)
  137. elif algo == "AES-128-CBC":
  138. key = _EVP_BytesToKey(passphrase, salt[:8], 16)
  139. objdec = AES.new(key, AES.MODE_CBC, salt)
  140. elif algo == "AES-192-CBC":
  141. key = _EVP_BytesToKey(passphrase, salt[:8], 24)
  142. objdec = AES.new(key, AES.MODE_CBC, salt)
  143. elif algo == "AES-256-CBC":
  144. key = _EVP_BytesToKey(passphrase, salt[:8], 32)
  145. objdec = AES.new(key, AES.MODE_CBC, salt)
  146. elif algo.lower() == "id-aes256-gcm":
  147. key = _EVP_BytesToKey(passphrase, salt[:8], 32)
  148. objdec = AES.new(key, AES.MODE_GCM, nonce=salt)
  149. padding = False
  150. else:
  151. raise ValueError("Unsupport PEM encryption algorithm (%s)." % algo)
  152. lines = lines[2:]
  153. else:
  154. objdec = None
  155. # Decode body
  156. data = a2b_base64(''.join(lines[1:-1]))
  157. enc_flag = False
  158. if objdec:
  159. if padding:
  160. data = unpad(objdec.decrypt(data), objdec.block_size)
  161. else:
  162. # There is no tag, so we don't use decrypt_and_verify
  163. data = objdec.decrypt(data)
  164. enc_flag = True
  165. return (data, marker, enc_flag)