PEM.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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 Cryptodome.Hash import MD5
  37. from Cryptodome.Util.Padding import pad, unpad
  38. from Cryptodome.Cipher import DES, DES3, AES
  39. from Cryptodome.Protocol.KDF import PBKDF1
  40. from Cryptodome.Random import get_random_bytes
  41. from Cryptodome.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 decode(pem_data, passphrase=None):
  85. """Decode a PEM block into binary.
  86. Args:
  87. pem_data (string):
  88. The PEM block.
  89. passphrase (byte string):
  90. If given and the PEM block is encrypted,
  91. the key will be derived from the passphrase.
  92. Returns:
  93. A tuple with the binary data, the marker string, and a boolean to
  94. indicate if decryption was performed.
  95. Raises:
  96. ValueError: if decoding fails, if the PEM file is encrypted and no passphrase has
  97. been provided or if the passphrase is incorrect.
  98. """
  99. # Verify Pre-Encapsulation Boundary
  100. r = re.compile(r"\s*-----BEGIN (.*)-----\s+")
  101. m = r.match(pem_data)
  102. if not m:
  103. raise ValueError("Not a valid PEM pre boundary")
  104. marker = m.group(1)
  105. # Verify Post-Encapsulation Boundary
  106. r = re.compile(r"-----END (.*)-----\s*$")
  107. m = r.search(pem_data)
  108. if not m or m.group(1) != marker:
  109. raise ValueError("Not a valid PEM post boundary")
  110. # Removes spaces and slit on lines
  111. lines = pem_data.replace(" ", '').split()
  112. # Decrypts, if necessary
  113. if lines[1].startswith('Proc-Type:4,ENCRYPTED'):
  114. if not passphrase:
  115. raise ValueError("PEM is encrypted, but no passphrase available")
  116. DEK = lines[2].split(':')
  117. if len(DEK) != 2 or DEK[0] != 'DEK-Info':
  118. raise ValueError("PEM encryption format not supported.")
  119. algo, salt = DEK[1].split(',')
  120. salt = unhexlify(tobytes(salt))
  121. if algo == "DES-CBC":
  122. # This is EVP_BytesToKey in OpenSSL
  123. key = PBKDF1(passphrase, salt, 8, 1, MD5)
  124. objdec = DES.new(key, DES.MODE_CBC, salt)
  125. elif algo == "DES-EDE3-CBC":
  126. # Note that EVP_BytesToKey is note exactly the same as PBKDF1
  127. key = PBKDF1(passphrase, salt, 16, 1, MD5)
  128. key += PBKDF1(key + passphrase, salt, 8, 1, MD5)
  129. objdec = DES3.new(key, DES3.MODE_CBC, salt)
  130. elif algo == "AES-128-CBC":
  131. key = PBKDF1(passphrase, salt[:8], 16, 1, MD5)
  132. objdec = AES.new(key, AES.MODE_CBC, salt)
  133. else:
  134. raise ValueError("Unsupport PEM encryption algorithm (%s)." % algo)
  135. lines = lines[2:]
  136. else:
  137. objdec = None
  138. # Decode body
  139. data = a2b_base64(''.join(lines[1:-1]))
  140. enc_flag = False
  141. if objdec:
  142. data = unpad(objdec.decrypt(data), objdec.block_size)
  143. enc_flag = True
  144. return (data, marker, enc_flag)