0
# Symmetric Ciphers
1
2
Low-level symmetric encryption algorithms and cipher modes. These are building blocks for custom cryptographic protocols - for most applications, use high-level APIs like Fernet or AEAD ciphers instead.
3
4
## Core Imports
5
6
```python
7
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
8
```
9
10
## Capabilities
11
12
### Cipher Interface
13
14
The `Cipher` class provides a consistent interface for all symmetric ciphers.
15
16
```python { .api }
17
class Cipher:
18
def __init__(self, algorithm, mode, backend=None):
19
"""
20
Create cipher with algorithm and mode.
21
22
Args:
23
algorithm: Cipher algorithm (AES(), ChaCha20(), etc.)
24
mode: Cipher mode (CBC(), CTR(), GCM(), etc.)
25
backend: Cryptographic backend (usually None)
26
"""
27
28
def encryptor(self) -> CipherContext:
29
"""
30
Create encryption context.
31
32
Returns:
33
CipherContext: Context for encryption operations
34
"""
35
36
def decryptor(self) -> CipherContext:
37
"""
38
Create decryption context.
39
40
Returns:
41
CipherContext: Context for decryption operations
42
"""
43
44
class CipherContext:
45
def update(self, data: bytes) -> bytes:
46
"""
47
Process data through cipher.
48
49
Args:
50
data (bytes): Data to encrypt/decrypt
51
52
Returns:
53
bytes: Processed data
54
"""
55
56
def finalize(self) -> bytes:
57
"""
58
Finalize cipher operation.
59
60
Returns:
61
bytes: Final processed data
62
"""
63
```
64
65
### Cipher Algorithms
66
67
```python { .api }
68
class AES:
69
def __init__(self, key: bytes):
70
"""
71
AES algorithm with 128, 192, or 256-bit key.
72
73
Args:
74
key (bytes): 16, 24, or 32-byte key
75
"""
76
77
@property
78
def block_size(self) -> int:
79
"""AES block size (16 bytes)"""
80
return 16
81
82
class ChaCha20:
83
def __init__(self, key: bytes, nonce: bytes):
84
"""
85
ChaCha20 stream cipher.
86
87
Args:
88
key (bytes): 32-byte key
89
nonce (bytes): 16-byte nonce
90
"""
91
92
class Camellia:
93
def __init__(self, key: bytes):
94
"""
95
Camellia block cipher (128, 192, or 256-bit key).
96
97
Args:
98
key (bytes): 16, 24, or 32-byte key
99
"""
100
101
@property
102
def block_size(self) -> int:
103
return 16
104
105
class SM4:
106
def __init__(self, key: bytes):
107
"""
108
SM4 block cipher (Chinese national standard).
109
110
Args:
111
key (bytes): 16-byte key
112
"""
113
114
@property
115
def block_size(self) -> int:
116
return 16
117
```
118
119
### Cipher Modes
120
121
```python { .api }
122
class ECB:
123
"""
124
Electronic Codebook mode (insecure - don't use).
125
Each block encrypted independently.
126
"""
127
128
class CBC:
129
def __init__(self, initialization_vector: bytes):
130
"""
131
Cipher Block Chaining mode.
132
133
Args:
134
initialization_vector (bytes): IV same size as block size
135
"""
136
137
class CTR:
138
def __init__(self, nonce: bytes):
139
"""
140
Counter mode (stream cipher mode).
141
142
Args:
143
nonce (bytes): Nonce for counter initialization
144
"""
145
146
class CFB:
147
def __init__(self, initialization_vector: bytes):
148
"""
149
Cipher Feedback mode.
150
151
Args:
152
initialization_vector (bytes): IV same size as block size
153
"""
154
155
class OFB:
156
def __init__(self, initialization_vector: bytes):
157
"""
158
Output Feedback mode.
159
160
Args:
161
initialization_vector (bytes): IV same size as block size
162
"""
163
164
class GCM:
165
def __init__(self, initialization_vector: bytes, tag: bytes = None, min_tag_length: int = 16):
166
"""
167
Galois/Counter Mode (authenticated encryption).
168
169
Args:
170
initialization_vector (bytes): IV (96 bits recommended)
171
tag (bytes, optional): Authentication tag for decryption
172
min_tag_length (int): Minimum tag length (4-16 bytes)
173
"""
174
```
175
176
## Usage Examples
177
178
### Basic AES-CBC Encryption
179
180
```python
181
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
182
from cryptography.hazmat.primitives import padding
183
import os
184
185
# Generate key and IV
186
key = os.urandom(32) # 256-bit key
187
iv = os.urandom(16) # 128-bit IV
188
189
# Create cipher
190
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
191
192
# Encrypt data
193
plaintext = b"Secret message that needs padding for block cipher"
194
195
# Pad data to block size
196
padder = padding.PKCS7(128).padder() # 128-bit block size
197
padded_data = padder.update(plaintext)
198
padded_data += padder.finalize()
199
200
# Encrypt
201
encryptor = cipher.encryptor()
202
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
203
204
print(f"Encrypted: {ciphertext.hex()}")
205
206
# Decrypt
207
decryptor = cipher.decryptor()
208
padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
209
210
# Remove padding
211
unpadder = padding.PKCS7(128).unpadder()
212
plaintext_recovered = unpadder.update(padded_plaintext)
213
plaintext_recovered += unpadder.finalize()
214
215
print(f"Decrypted: {plaintext_recovered}")
216
```
217
218
### AES-CTR Mode (Stream Cipher)
219
220
```python
221
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
222
import os
223
224
# CTR mode doesn't require padding
225
key = os.urandom(32)
226
nonce = os.urandom(16)
227
228
cipher = Cipher(algorithms.AES(key), modes.CTR(nonce))
229
230
plaintext = b"This message doesn't need padding in CTR mode"
231
232
# Encrypt
233
encryptor = cipher.encryptor()
234
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
235
236
# Decrypt
237
decryptor = cipher.decryptor()
238
decrypted_text = decryptor.update(ciphertext) + decryptor.finalize()
239
240
print(f"Original: {plaintext}")
241
print(f"Decrypted: {decrypted_text}")
242
print(f"Match: {plaintext == decrypted_text}")
243
```
244
245
### ChaCha20 Stream Cipher
246
247
```python
248
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
249
import os
250
251
# ChaCha20 is a stream cipher
252
key = os.urandom(32) # 256-bit key
253
nonce = os.urandom(16) # 128-bit nonce
254
255
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None)
256
257
message = b"Stream cipher message - no padding needed"
258
259
# Encrypt
260
encryptor = cipher.encryptor()
261
ciphertext = encryptor.update(message) + encryptor.finalize()
262
263
# Decrypt
264
decryptor = cipher.decryptor()
265
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
266
267
print(f"ChaCha20 encryption successful: {message == plaintext}")
268
```
269
270
### File Encryption with AES-CBC
271
272
```python
273
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
274
from cryptography.hazmat.primitives import padding
275
import os
276
277
def encrypt_file(input_path, output_path, key):
278
"""Encrypt file with AES-CBC"""
279
iv = os.urandom(16)
280
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
281
encryptor = cipher.encryptor()
282
283
padder = padding.PKCS7(128).padder()
284
285
with open(input_path, 'rb') as infile, open(output_path, 'wb') as outfile:
286
# Write IV first
287
outfile.write(iv)
288
289
# Encrypt file in chunks with padding
290
while True:
291
chunk = infile.read(8192)
292
if not chunk:
293
# Final chunk with padding
294
padded_chunk = padder.finalize()
295
if padded_chunk:
296
outfile.write(encryptor.update(padded_chunk))
297
break
298
299
if len(chunk) < 8192:
300
# Last chunk - apply padding
301
padded_chunk = padder.update(chunk) + padder.finalize()
302
outfile.write(encryptor.update(padded_chunk))
303
break
304
else:
305
# Full chunk - no padding yet
306
padded_chunk = padder.update(chunk)
307
outfile.write(encryptor.update(padded_chunk))
308
309
# Finalize encryption
310
outfile.write(encryptor.finalize())
311
312
def decrypt_file(input_path, output_path, key):
313
"""Decrypt file encrypted with AES-CBC"""
314
with open(input_path, 'rb') as infile:
315
# Read IV
316
iv = infile.read(16)
317
318
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
319
decryptor = cipher.decryptor()
320
321
# Read and decrypt remaining data
322
ciphertext = infile.read()
323
324
padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
325
326
# Remove padding
327
unpadder = padding.PKCS7(128).unpadder()
328
plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
329
330
with open(output_path, 'wb') as outfile:
331
outfile.write(plaintext)
332
333
# Usage
334
key = os.urandom(32)
335
336
# Create test file
337
with open('test.txt', 'w') as f:
338
f.write("This is a test file for encryption\n" * 100)
339
340
# Encrypt file
341
encrypt_file('test.txt', 'test.txt.enc', key)
342
print("File encrypted")
343
344
# Decrypt file
345
decrypt_file('test.txt.enc', 'test_decrypted.txt', key)
346
print("File decrypted")
347
348
# Verify
349
with open('test.txt', 'rb') as f1, open('test_decrypted.txt', 'rb') as f2:
350
print(f"Files match: {f1.read() == f2.read()}")
351
```
352
353
## Security Considerations
354
355
- **Mode Selection**: Never use ECB mode for real applications
356
- **IV/Nonce Management**: Always use random, unique IVs/nonces
357
- **Padding**: Required for block ciphers with non-block-sized data
358
- **Authentication**: Low-level ciphers don't provide authentication - use AEAD instead
359
- **Key Management**: Use proper key derivation, never reuse keys inappropriately
360
- **Stream Cipher Risks**: Never reuse key/nonce pairs with stream ciphers
361
- **High-Level APIs**: Prefer Fernet or AEAD ciphers for most applications