Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,11 @@ gmssl.Sm4Gcm(key, iv, aad, taglen = SM4_GCM_DEFAULT_TAG_SIZE, encrypt = True)

GCM模式和CBC、CTR、HMAC不同之处还在于可选的IV长度和MAC长度,其中IV的长度必须在`SM4_GCM_MIN_IV_SIZE`和`SM4_GCM_MAX_IV_SIZE`之间,长度为`SM4_GCM_DEFAULT_IV_SIZE`有最佳的计算效率。MAC的长度也是可选的,通过`init`方法中的`taglen`设定,其长度不应低于8字节,不应长于`SM4_GCM_DEFAULT_TAG_SIZE = 16`字节。

下面例子展示SM4-GCM加密和解密的过程。
`Sm4Gcm` 提供了两种 API 模式:

#### 推荐用法:无状态 API (线程安全)

对于大多数场景,推荐使用 `encrypt` 和 `decrypt` 类方法。它们是无状态的,并且可以安全地在多线程环境中使用。

```python
>>> from gmssl import Sm4Gcm, rand_bytes
Expand All @@ -624,7 +628,7 @@ GCM模式和CBC、CTR、HMAC不同之处还在于可选的IV长度和MAC长度
>>> assert decrypted == plaintext
```

通过上面的例子可以看出,SM4-GCM加密模式中可以通过指定了一个不需要加密的字段`aad`,注意`aad`是不会在`update`中输出的。由于GCM模式输出个外的完整性标签,因此`update`和`finish`输出的总密文长度会比总的输入明文长度多`taglen`个字节。
通过上面的例子可以看出,SM4-GCM加密模式中可以通过指定了一个不需要加密的字段`aad`,注意`aad`是不会在`update`中输出的。由于GCM模式输出额外的完整性标签,因此`update`和`finish`输出的总密文长度会比总的输入明文长度多`taglen`个字节。

### Zuc序列密码

Expand Down
66 changes: 50 additions & 16 deletions src/gmssl/_sm4.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from gmssl._constants import (
_SM4_NUM_ROUNDS,
DO_DECRYPT,
DO_ENCRYPT,
SM4_BLOCK_SIZE,
SM4_GCM_DEFAULT_TAG_SIZE,
Expand Down Expand Up @@ -196,20 +197,33 @@ class Sm4Gcm(Structure):
"""
SM4-GCM (Galois/Counter Mode) authenticated encryption.

WARNING: This class is NOT thread-safe due to underlying GmSSL library
implementation. If you need to use SM4-GCM in a multi-threaded environment,
you must protect each instance with a lock (threading.Lock).

Example:
# Single-threaded usage (safe)
sm4_gcm = Sm4Gcm(key, iv, aad, taglen, DO_ENCRYPT)
ciphertext = sm4_gcm.update(plaintext) + sm4_gcm.finish()

# Multi-threaded usage (requires lock)
lock = threading.Lock()
with lock:
sm4_gcm = Sm4Gcm(key, iv, aad, taglen, DO_ENCRYPT)
ciphertext = sm4_gcm.update(plaintext) + sm4_gcm.finish()
This class provides two modes of operation:

1. Stateless, Thread-Safe API (Recommended):
Use the `Sm4Gcm.encrypt()` and `Sm4Gcm.decrypt()` class methods for
one-shot encryption/decryption. These methods are simple, efficient,
and thread-safe.

Example:
ciphertext = Sm4Gcm.encrypt(key, iv, aad, plaintext)
decrypted = Sm4Gcm.decrypt(key, iv, aad, ciphertext)

2. Stateful, Streaming API (Advanced):
For encrypting/decrypting large data streams, you can create an
instance of `Sm4Gcm` and use the `update()` and `finish()` methods.

WARNING: The stateful API is NOT thread-safe. If you need to use a
single instance in a multi-threaded environment, you MUST protect the
entire sequence of operations (from `__init__` to `finish`) with a
`threading.Lock`.

Example (with external lock):
lock = threading.Lock()
with lock:
sm4_gcm = Sm4Gcm(key, iv, aad, taglen, DO_ENCRYPT)
ciphertext = sm4_gcm.update(chunk1)
ciphertext += sm4_gcm.update(chunk2)
ciphertext += sm4_gcm.finish()
"""

_fields_ = [
Expand Down Expand Up @@ -264,7 +278,7 @@ def update(self, data):
checked.sm4_gcm_decrypt_update(
byref(self), data, c_size_t(len(data)), outbuf, byref(outlen)
)
return outbuf[0 : outlen.value]
return outbuf[0:outlen.value]

def finish(self):
outbuf = create_string_buffer(SM4_BLOCK_SIZE + SM4_GCM_MAX_TAG_SIZE)
Expand All @@ -273,4 +287,24 @@ def finish(self):
checked.sm4_gcm_encrypt_finish(byref(self), outbuf, byref(outlen))
else:
checked.sm4_gcm_decrypt_finish(byref(self), outbuf, byref(outlen))
return outbuf[: outlen.value]
return outbuf[:outlen.value]

@classmethod
def encrypt(cls, key, iv, aad, plaintext, taglen=SM4_GCM_DEFAULT_TAG_SIZE):
"""
Encrypts and authenticates data in a single, thread-safe operation.
"""
enc = cls(key, iv, aad, taglen, DO_ENCRYPT)
ciphertext = enc.update(plaintext)
ciphertext += enc.finish()
return ciphertext

@classmethod
def decrypt(cls, key, iv, aad, ciphertext, taglen=SM4_GCM_DEFAULT_TAG_SIZE):
"""
Decrypts and verifies data in a single, thread-safe operation.
"""
dec = cls(key, iv, aad, taglen, DO_DECRYPT)
decrypted = dec.update(ciphertext)
decrypted += dec.finish()
return decrypted
43 changes: 43 additions & 0 deletions tests/test_additional_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
DO_SIGN,
DO_VERIFY,
SM9_MAX_PLAINTEXT_SIZE,
NativeError,
Sm2Certificate,
Sm4Gcm,
Sm9EncKey,
Sm9EncMasterKey,
Sm9Signature,
Expand Down Expand Up @@ -474,3 +476,44 @@ def test_sm9_sign_full_workflow_with_key_export():
verify = Sm9Signature(DO_VERIFY)
verify.update(b"Message to sign")
assert verify.verify(sig, master_pub, "Bob")


# =============================================================================
# SM4-GCM One-Shot API Tests
# =============================================================================


def test_sm4_gcm_one_shot_api():
"""
Test the one-shot, thread-safe Sm4Gcm.encrypt and Sm4Gcm.decrypt methods.
"""
key = b"1234567890123456"
iv = b"123456789012"
aad = b"aad data"
plaintext = b"This is a test message for one-shot GCM."

# Encrypt
ciphertext = Sm4Gcm.encrypt(key, iv, aad, plaintext)

# Decrypt and verify
decrypted = Sm4Gcm.decrypt(key, iv, aad, ciphertext)
assert decrypted == plaintext


def test_sm4_gcm_one_shot_api_auth_failure():
"""
Test that one-shot Sm4Gcm.decrypt raises an error on authentication failure.
"""
key = b"1234567890123456"
iv = b"123456789012"
aad = b"aad data"
plaintext = b"This is a test message."

ciphertext = Sm4Gcm.encrypt(key, iv, aad, plaintext)

# Tamper with the ciphertext (flip the first byte)
tampered_ciphertext = bytes([ciphertext[0] ^ 0xFF]) + ciphertext[1:]

# Decryption should fail with a NativeError
with pytest.raises(NativeError, match="sm4_gcm_decrypt_finish failed"):
Sm4Gcm.decrypt(key, iv, aad, tampered_ciphertext)
23 changes: 23 additions & 0 deletions tests/test_thread_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
Sm3,
Sm3Hmac,
Sm4Cbc,
Sm4Gcm,
Sm9EncMasterKey,
Zuc,
rand_bytes,
Expand Down Expand Up @@ -146,6 +147,28 @@ def encrypt_decrypt():
assert all(result == plaintext for result in results)


def test_sm4_gcm_one_shot_thread_safety():
"""
Test that the one-shot Sm4Gcm class methods are thread-safe.
"""
num_threads = 20
key = b"1234567890123456"
iv = b"123456789012"
aad = b"aad data"
plaintext = b"This is a test message for one-shot GCM thread safety."

def encrypt_decrypt_task():
ciphertext = Sm4Gcm.encrypt(key, iv, aad, plaintext)
decrypted = Sm4Gcm.decrypt(key, iv, aad, ciphertext)
return decrypted

with ThreadPoolExecutor(max_workers=num_threads) as executor:
futures = [executor.submit(encrypt_decrypt_task) for _ in range(num_threads)]
results = [future.result() for future in as_completed(futures)]

assert all(result == plaintext for result in results)


# =============================================================================
# ZUC Stream Cipher Thread Safety
# =============================================================================
Expand Down
Loading