aboutsummaryrefslogtreecommitdiffstats
path: root/libpwman/aes.py
blob: 50b69e3fd1634a6deacff33f63c0cb9a56822222 (plain)
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
# -*- coding: utf-8 -*-
"""
# AES wrapper
# Copyright (c) 2023-2024 Michael Büsch <m@bues.ch>
# Licensed under the GNU/GPL version 2 or later.
"""

from libpwman.exception import PWManError

import os

__all__ = [
	"AES",
]

class AES:
	"""Abstraction layer for the AES implementation.
	"""

	BLOCK_SIZE = 128 // 8
	__singleton = None

	@classmethod
	def get(cls):
		"""Get the AES singleton.
		"""
		if cls.__singleton is None:
			cls.__singleton = cls()
		return cls.__singleton

	def __init__(self):
		self.__pyaes = None
		self.__cryptodome = None

		cryptolib = os.getenv("PWMAN_CRYPTOLIB", "").lower().strip()

		if cryptolib in ("", "cryptodome"):
			# Try to use Cryptodome
			try:
				import Cryptodome
				import Cryptodome.Cipher.AES
				import Cryptodome.Util.Padding
				self.__cryptodome = Cryptodome
				return
			except ImportError as e:
				pass

		if cryptolib in ("", "pyaes"):
			# Try to use pyaes
			try:
				import pyaes
				self.__pyaes = pyaes
				return
			except ImportError as e:
				pass

		msg = "Python module import error."
		if cryptolib == "":
			msg += "\nNeither 'Cryptodome' nor 'pyaes' is installed."
		else:
			msg += "\n'PWMAN_CRYPTOLIB=%s' is not supported or not installed." % cryptolib
		raise PWManError(msg)

	def encrypt(self, key, iv, data):
		"""Encrypt data.
		"""

		# Check parameters.
		if len(key) != 256 // 8:
			raise PWManError("AES: Invalid key length.")
		if len(iv) != self.BLOCK_SIZE:
			raise PWManError("AES: Invalid iv length.")
		if len(data) <= 0:
			raise PWManError("AES: Invalid data length.")

		try:
			if self.__cryptodome is not None:
				# Use Cryptodome
				padData = self.__cryptodome.Util.Padding.pad(
					data_to_pad=data,
					block_size=self.BLOCK_SIZE,
					style="pkcs7")
				cipher = self.__cryptodome.Cipher.AES.new(
					key=key,
					mode=self.__cryptodome.Cipher.AES.MODE_CBC,
					iv=iv)
				encData = cipher.encrypt(padData)
				return encData

			if self.__pyaes is not None:
				# Use pyaes
				mode = self.__pyaes.AESModeOfOperationCBC(key=key, iv=iv)
				padding = self.__pyaes.PADDING_DEFAULT
				enc = self.__pyaes.Encrypter(mode=mode, padding=padding)
				encData = enc.feed(data)
				encData += enc.feed()
				return encData

		except Exception as e:
			raise PWManError("AES error: %s: %s" % (type(e), str(e)))
		raise PWManError("AES not implemented.")

	def decrypt(self, key, iv, data, legacyPadding=False):
		"""Decrypt data.
		"""

		# Check parameters.
		if len(key) != 256 // 8:
			raise PWManError("AES: Invalid key length.")
		if len(iv) != self.BLOCK_SIZE:
			raise PWManError("AES: Invalid iv length.")
		if len(data) <= 0:
			raise PWManError("AES: Invalid data length.")

		try:
			if self.__cryptodome is not None:
				# Use Cryptodome
				cipher = self.__cryptodome.Cipher.AES.new(
					key=key,
					mode=self.__cryptodome.Cipher.AES.MODE_CBC,
					iv=iv)
				decData = cipher.decrypt(data)
				if legacyPadding:
					unpadData = self.__unpadLegacy(decData)
				else:
					unpadData = self.__cryptodome.Util.Padding.unpad(
						padded_data=decData,
						block_size=self.BLOCK_SIZE,
						style="pkcs7")
				return unpadData

			if self.__pyaes is not None:
				# Use pyaes
				mode = self.__pyaes.AESModeOfOperationCBC(key=key, iv=iv)
				if legacyPadding:
					padding = self.__pyaes.PADDING_NONE
				else:
					padding = self.__pyaes.PADDING_DEFAULT
				dec = self.__pyaes.Decrypter(mode=mode, padding=padding)
				decData = dec.feed(data)
				decData += dec.feed()
				if legacyPadding:
					unpadData = self.__unpadLegacy(decData)
				else:
					unpadData = decData
				return unpadData

		except Exception as e:
			raise PWManError("AES error: %s: %s" % (type(e), str(e)))
		raise PWManError("AES not implemented.")

	@staticmethod
	def __unpadLegacy(data):
		"""Strip legacy padding.
		"""
		index = data.rfind(b"\xFF")
		if index < 0 or index >= len(data):
			raise PWManError("Legacy padding: Did not find start.")
		return data[:index]

	@classmethod
	def quickSelfTest(cls):
		inst = cls.get()
		enc = inst.encrypt(key=(b"_keykey_" * 4), iv=(b"iv" * 8), data=b"pwman")
		if enc != bytes.fromhex("cf73a286509e1265d26490a76dcbb2fd"):
			raise PWManError("AES encrypt: Quick self test failed.")
		dec = inst.decrypt(key=(b"_keykey_" * 4), iv=(b"iv" * 8), data=enc)
		if dec != b"pwman":
			raise PWManError("AES decrypt: Quick self test failed.")
bues.ch cgit interface