PEM.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  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 tls.Crypto.Hash import MD5
  37. from tls.Crypto.Util.Padding import pad, unpad
  38. from tls.Crypto.Cipher import DES, DES3, AES
  39. from tls.Crypto.Protocol.KDF import PBKDF1
  40. from tls.Crypto.Random import get_random_bytes
  41. from tls.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. # Decrypts, if necessary
  120. if lines[1].startswith('Proc-Type:4,ENCRYPTED'):
  121. if not passphrase:
  122. raise ValueError("PEM is encrypted, but no passphrase available")
  123. DEK = lines[2].split(':')
  124. if len(DEK) != 2 or DEK[0] != 'DEK-Info':
  125. raise ValueError("PEM encryption format not supported.")
  126. algo, salt = DEK[1].split(',')
  127. salt = unhexlify(tobytes(salt))
  128. padding = True
  129. if algo == "DES-CBC":
  130. key = _EVP_BytesToKey(passphrase, salt, 8)
  131. objdec = DES.new(key, DES.MODE_CBC, salt)
  132. elif algo == "DES-EDE3-CBC":
  133. key = _EVP_BytesToKey(passphrase, salt, 24)
  134. objdec = DES3.new(key, DES3.MODE_CBC, salt)
  135. elif algo == "AES-128-CBC":
  136. key = _EVP_BytesToKey(passphrase, salt[:8], 16)
  137. objdec = AES.new(key, AES.MODE_CBC, salt)
  138. elif algo == "AES-192-CBC":
  139. key = _EVP_BytesToKey(passphrase, salt[:8], 24)
  140. objdec = AES.new(key, AES.MODE_CBC, salt)
  141. elif algo == "AES-256-CBC":
  142. key = _EVP_BytesToKey(passphrase, salt[:8], 32)
  143. objdec = AES.new(key, AES.MODE_CBC, salt)
  144. elif algo.lower() == "id-aes256-gcm":
  145. key = _EVP_BytesToKey(passphrase, salt[:8], 32)
  146. objdec = AES.new(key, AES.MODE_GCM, nonce=salt)
  147. padding = False
  148. else:
  149. raise ValueError("Unsupport PEM encryption algorithm (%s)." % algo)
  150. lines = lines[2:]
  151. else:
  152. objdec = None
  153. # Decode body
  154. data = a2b_base64(''.join(lines[1:-1]))
  155. enc_flag = False
  156. if objdec:
  157. if padding:
  158. data = unpad(objdec.decrypt(data), objdec.block_size)
  159. else:
  160. # There is no tag, so we don't use decrypt_and_verify
  161. data = objdec.decrypt(data)
  162. enc_flag = True
  163. return (data, marker, enc_flag)