crossmool 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
crossmool/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from crossmool.sm2_util import SM2Util
2
+ from crossmool.sm3_util import SM3Util
3
+ from crossmool.sm4_util import SM4Util
4
+
5
+ __all__ = [
6
+ "SM2Util",
7
+ "SM3Util",
8
+ "SM4Util",
9
+ ]
10
+
11
+ __version__ = "1.3.0"
crossmool/der.py ADDED
@@ -0,0 +1,84 @@
1
+ """
2
+ Minimal DER reader to parse SPKI (X.509) and PKCS8 used by crossmool JS/Java utils.
3
+ This is a small port of the project's JS `DerReader` used to extract raw EC keys.
4
+ """
5
+ from typing import Tuple
6
+
7
+
8
+ class DerReader:
9
+ def __init__(self, data: bytes):
10
+ self.data = data
11
+ self.pos = 0
12
+ self.len = len(data)
13
+
14
+ def _read_length(self) -> int:
15
+ first = self._read(1)[0]
16
+ if first & 0x80 == 0:
17
+ return first
18
+ num = first & 0x7F
19
+ val = 0
20
+ for _ in range(num):
21
+ val = (val << 8) | self._read(1)[0]
22
+ return val
23
+
24
+ def _read(self, n: int) -> bytes:
25
+ if self.pos + n > self.len:
26
+ raise ValueError('Truncated DER')
27
+ out = self.data[self.pos:self.pos + n]
28
+ self.pos += n
29
+ return out
30
+
31
+ def read_sequence(self) -> 'DerReader':
32
+ tag = self._read(1)[0]
33
+ if tag != 0x30:
34
+ raise ValueError('Expected SEQUENCE')
35
+ length = self._read_length()
36
+ content = self._read(length)
37
+ return DerReader(content)
38
+
39
+ def read_integer_bytes(self) -> bytes:
40
+ tag = self._read(1)[0]
41
+ if tag != 0x02:
42
+ raise ValueError('Expected INTEGER')
43
+ length = self._read_length()
44
+ return self._read(length)
45
+
46
+ def read_oid(self) -> str:
47
+ tag = self._read(1)[0]
48
+ if tag != 0x06:
49
+ raise ValueError('Expected OID')
50
+ length = self._read_length()
51
+ oid_bytes = self._read(length)
52
+ # decode OID
53
+ first = oid_bytes[0]
54
+ oid_nodes = [str(first // 40), str(first % 40)]
55
+ value = 0
56
+ for b in oid_bytes[1:]:
57
+ value = (value << 7) | (b & 0x7F)
58
+ if not (b & 0x80):
59
+ oid_nodes.append(str(value))
60
+ value = 0
61
+ return '.'.join(oid_nodes)
62
+
63
+ def read_bit_string(self) -> Tuple[int, bytes]:
64
+ tag = self._read(1)[0]
65
+ if tag != 0x03:
66
+ raise ValueError('Expected BIT STRING')
67
+ length = self._read_length()
68
+ content = self._read(length)
69
+ unused_bits = content[0]
70
+ data = content[1:]
71
+ return unused_bits, data
72
+
73
+ def read_octet_string(self) -> bytes:
74
+ tag = self._read(1)[0]
75
+ if tag != 0x04:
76
+ raise ValueError('Expected OCTET STRING')
77
+ length = self._read_length()
78
+ return self._read(length)
79
+
80
+ def read(self, n: int) -> bytes:
81
+ return self._read(n)
82
+
83
+ def eof(self) -> bool:
84
+ return self.pos >= self.len
crossmool/encoding.py ADDED
@@ -0,0 +1,56 @@
1
+ import base64
2
+ import re
3
+
4
+
5
+ def is_hex_string(s: str) -> bool:
6
+ if not isinstance(s, str):
7
+ return False
8
+ s = s.strip()
9
+ return bool(re.fullmatch(r"[0-9a-fA-F]+", s))
10
+
11
+
12
+ def is_base64_string(s: str) -> bool:
13
+ if not isinstance(s, str):
14
+ return False
15
+ s = s.strip()
16
+ try:
17
+ # len check: base64 length divisible by 4 (with padding)
18
+ if len(s) % 4 != 0:
19
+ return False
20
+ base64.b64decode(s, validate=True)
21
+ return True
22
+ except Exception:
23
+ return False
24
+
25
+
26
+ def hex_to_bytes(hex_str: str) -> bytes:
27
+ return bytes.fromhex(hex_str.strip())
28
+
29
+
30
+ def bytes_to_hex(b: bytes) -> str:
31
+ return b.hex()
32
+
33
+
34
+ def base64_to_bytes(s: str) -> bytes:
35
+ return base64.b64decode(s)
36
+
37
+
38
+ def bytes_to_base64(b: bytes) -> str:
39
+ return base64.b64encode(b).decode('ascii')
40
+
41
+
42
+ def detect_string_encoding(s: str) -> str:
43
+ s = s.strip()
44
+ if is_hex_string(s):
45
+ return 'hex'
46
+ if is_base64_string(s):
47
+ return 'base64'
48
+ return 'unknown'
49
+
50
+
51
+ def utf8_to_bytes(s: str) -> bytes:
52
+ return s.encode('utf-8')
53
+
54
+
55
+ def bytes_to_utf8(b: bytes) -> str:
56
+ return b.decode('utf-8')
crossmool/sm2_util.py ADDED
@@ -0,0 +1,497 @@
1
+ from gmssl import sm2, func
2
+ from crossmool.encoding import (
3
+ is_hex_string,
4
+ is_base64_string,
5
+ hex_to_bytes,
6
+ bytes_to_hex,
7
+ base64_to_bytes,
8
+ bytes_to_base64,
9
+ utf8_to_bytes,
10
+ )
11
+ from crossmool.der import DerReader
12
+
13
+ # SM2 signature/verification user ID (kept identical to Java BouncyCastle)
14
+ USER_ID = b"1234567812345678"
15
+
16
+ def _ensure_uncompressed_pubhex(hexstr: str) -> str:
17
+ """Ensure the public key is in uncompressed form (04 + X + Y, 66 bytes / 130 hex chars)."""
18
+ s = hexstr.strip().lower()
19
+ if len(s) == 128:
20
+ return '04' + s
21
+ if len(s) == 130 and s.startswith('04'):
22
+ return s
23
+ raise ValueError('Unsupported public key hex length')
24
+
25
+ def sm2_public_key_to_raw_hex(key_input) -> str:
26
+ """
27
+ Convert a public key to raw uncompressed form (04 + X + Y, 66 bytes / 130 hex chars).
28
+ Supports X.509 SPKI DER (HEX/Base64/bytes) and raw 64/128-char HEX.
29
+ """
30
+ if isinstance(key_input, bytes):
31
+ b = key_input
32
+ else:
33
+ s = key_input.strip()
34
+ if is_hex_string(s):
35
+ b = hex_to_bytes(s)
36
+ elif is_base64_string(s):
37
+ b = base64_to_bytes(s)
38
+ else:
39
+ return _ensure_uncompressed_pubhex(s)
40
+
41
+ # X.509 SPKI DER format (starts with 0x30)
42
+ if len(b) > 2 and b[0] == 0x30:
43
+ try:
44
+ spki = DerReader(b).read_sequence()
45
+ # Read algorithm identifier (SEQUENCE)
46
+ alg = spki.read_sequence()
47
+ try:
48
+ _ = alg.read_oid() # EC OID
49
+ except Exception:
50
+ pass
51
+ try:
52
+ _ = alg.read_oid() # Curve OID
53
+ except Exception:
54
+ pass
55
+ # Read public key BIT STRING
56
+ unused_bits, data = spki.read_bit_string()
57
+ if unused_bits != 0:
58
+ raise ValueError('Unsupported BIT STRING with unused bits')
59
+ raw = bytes_to_hex(data)
60
+ return _ensure_uncompressed_pubhex(raw)
61
+ except Exception as e:
62
+ raise ValueError(f'Failed to parse X.509 public key: {e}')
63
+
64
+ # fallback: assume hex string
65
+ return _ensure_uncompressed_pubhex(bytes_to_hex(b))
66
+
67
+ def sm2_private_key_to_raw_hex(key_input) -> str:
68
+ """
69
+ Convert a private key to raw 32-byte HEX form.
70
+ Supports PKCS#8 DER (HEX/Base64/bytes) and raw 64-char HEX.
71
+ """
72
+ if isinstance(key_input, bytes):
73
+ b = key_input
74
+ else:
75
+ s = key_input.strip()
76
+ if is_hex_string(s):
77
+ if s.startswith('30'):
78
+ b = hex_to_bytes(s)
79
+ else:
80
+ if len(s) == 64:
81
+ return s.lower()
82
+ b = hex_to_bytes(s)
83
+ elif is_base64_string(s):
84
+ b = base64_to_bytes(s)
85
+ else:
86
+ raise ValueError('Unsupported private key format')
87
+
88
+ if len(b) > 2 and b[0] == 0x30:
89
+ # parse PKCS#8 DER format
90
+ try:
91
+ pki = DerReader(b).read_sequence()
92
+ _ = pki.read_integer_bytes() # version
93
+ _ = pki.read_sequence() # algorithm
94
+ private_octets = pki.read_octet_string()
95
+ # Parse ECPrivateKey from OCTET STRING
96
+ ec = DerReader(private_octets).read_sequence()
97
+ _ = ec.read_integer_bytes() # version
98
+ d_octets = ec.read_octet_string() # private key bytes
99
+ d_hex = bytes_to_hex(d_octets)
100
+ if len(d_hex) < 64:
101
+ d_hex = d_hex.rjust(64, '0')
102
+ return d_hex
103
+ except Exception as e:
104
+ raise ValueError(f'Failed to parse PKCS#8 private key: {e}')
105
+
106
+ # fallback: assume raw bytes
107
+ hex_result = bytes_to_hex(b)
108
+ if len(hex_result) < 64:
109
+ hex_result = hex_result.rjust(64, '0')
110
+ return hex_result
111
+
112
+ def _der_encode_int(i_bytes: bytes) -> bytes:
113
+ """DER-encode an INTEGER."""
114
+ bi = i_bytes.lstrip(b'\x00')
115
+ if not bi:
116
+ bi = b'\x00'
117
+ if bi[0] & 0x80:
118
+ bi = b'\x00' + bi
119
+ length = len(bi)
120
+ if length < 128:
121
+ return b'\x02' + bytes([length]) + bi
122
+ len_bytes = length.to_bytes((length.bit_length() + 7) // 8, 'big')
123
+ return b'\x02' + bytes([0x80 | len(len_bytes)]) + len_bytes + bi
124
+
125
+ def _der_encode_sequence(elements: bytes) -> bytes:
126
+ """DER-encode a SEQUENCE."""
127
+ length = len(elements)
128
+ if length < 128:
129
+ return b'\x30' + bytes([length]) + elements
130
+ len_bytes = length.to_bytes((length.bit_length() + 7) // 8, 'big')
131
+ return b'\x30' + bytes([0x80 | len(len_bytes)]) + len_bytes + elements
132
+
133
+ def raw_rs_to_der(r_hex: str, s_hex: str) -> bytes:
134
+ """Convert raw r||s into a DER-encoded signature."""
135
+ r = bytes.fromhex(r_hex)
136
+ s = bytes.fromhex(s_hex)
137
+ r_enc = _der_encode_int(r)
138
+ s_enc = _der_encode_int(s)
139
+ return _der_encode_sequence(r_enc + s_enc)
140
+
141
+ # ────────────────── DER key encoding helpers ──────────────────
142
+
143
+ OID_EC_PUBLIC_KEY = '1.2.840.10045.2.1'
144
+ OID_SM2_CURVE = '1.2.156.10197.1.301'
145
+
146
+ def _der_encode_oid(oid: str) -> bytes:
147
+ """DER encode an OID string."""
148
+ parts = [int(x) for x in oid.split('.')]
149
+ result = [parts[0] * 40 + parts[1]]
150
+ for v in parts[2:]:
151
+ stack = [v & 0x7f]
152
+ v >>= 7
153
+ while v > 0:
154
+ stack.insert(0, (v & 0x7f) | 0x80)
155
+ v >>= 7
156
+ result.extend(stack)
157
+ content = bytes(result)
158
+ if len(content) < 128:
159
+ return b'\x06' + bytes([len(content)]) + content
160
+ len_bytes = len(content).to_bytes((len(content).bit_length() + 7) // 8, 'big')
161
+ return b'\x06' + bytes([0x80 | len(len_bytes)]) + len_bytes + content
162
+
163
+ def _der_encode_octet_string(data: bytes) -> bytes:
164
+ if len(data) < 128:
165
+ return b'\x04' + bytes([len(data)]) + data
166
+ len_bytes = len(data).to_bytes((len(data).bit_length() + 7) // 8, 'big')
167
+ return b'\x04' + bytes([0x80 | len(len_bytes)]) + len_bytes + data
168
+
169
+ def _der_encode_bit_string(data: bytes, unused_bits: int = 0) -> bytes:
170
+ content = bytes([unused_bits]) + data
171
+ if len(content) < 128:
172
+ return b'\x03' + bytes([len(content)]) + content
173
+ len_bytes = len(content).to_bytes((len(content).bit_length() + 7) // 8, 'big')
174
+ return b'\x03' + bytes([0x80 | len(len_bytes)]) + len_bytes + content
175
+
176
+ def _der_encode_context_specific(tag_number: int, content: bytes) -> bytes:
177
+ tag = 0xa0 | tag_number
178
+ if len(content) < 128:
179
+ return bytes([tag, len(content)]) + content
180
+ len_bytes = len(content).to_bytes((len(content).bit_length() + 7) // 8, 'big')
181
+ return bytes([tag, 0x80 | len(len_bytes)]) + len_bytes + content
182
+
183
+ def _encode_public_key_to_der(pub_hex: str) -> bytes:
184
+ """Encode raw-hex public key (130 chars, 04+X+Y) to X.509 SPKI DER."""
185
+ pub_bytes = bytes.fromhex(pub_hex) # 65 bytes
186
+ alg_seq = _der_encode_sequence(
187
+ _der_encode_oid(OID_EC_PUBLIC_KEY) + _der_encode_oid(OID_SM2_CURVE)
188
+ )
189
+ return _der_encode_sequence(alg_seq + _der_encode_bit_string(pub_bytes, 0))
190
+
191
+ def _encode_private_key_to_der(priv_hex: str, pub_hex: str) -> bytes:
192
+ """Encode raw-hex keys to PKCS#8 PrivateKeyInfo DER."""
193
+ d = bytes.fromhex(priv_hex) # 32 bytes
194
+ pub = bytes.fromhex(pub_hex) # 65 bytes
195
+ # ECPrivateKey (SEC1)
196
+ ec_priv = _der_encode_sequence(
197
+ _der_encode_int(b'\x01') +
198
+ _der_encode_octet_string(d) +
199
+ _der_encode_context_specific(1, _der_encode_bit_string(pub, 0))
200
+ )
201
+ # AlgorithmIdentifier
202
+ alg_seq = _der_encode_sequence(
203
+ _der_encode_oid(OID_EC_PUBLIC_KEY) + _der_encode_oid(OID_SM2_CURVE)
204
+ )
205
+ return _der_encode_sequence(
206
+ _der_encode_int(b'\x00') + alg_seq + _der_encode_octet_string(ec_priv)
207
+ )
208
+
209
+ def der_to_raw_rs(der_sig: bytes) -> (str, str):
210
+ """Convert a DER-encoded signature back to r||s (strip DER INTEGER sign padding)."""
211
+ d = DerReader(der_sig).read_sequence()
212
+ r = d.read_integer_bytes()
213
+ s = d.read_integer_bytes()
214
+ # strip the leading 0x00 that DER INTEGER may add to represent a positive number
215
+ r = r.lstrip(b'\x00') or b'\x00'
216
+ s = s.lstrip(b'\x00') or b'\x00'
217
+ r_hex = bytes_to_hex(r).rjust(64, '0')
218
+ s_hex = bytes_to_hex(s).rjust(64, '0')
219
+ return r_hex, s_hex
220
+
221
+ def _new_crypt(private_key='', public_key_hex=None, mode=0):
222
+ """
223
+ Construct a gmssl CryptSM2 instance while bypassing its __init__ lstrip("04")
224
+ bug (which strips every leading '0'/'4' char instead of only the "04" prefix).
225
+ gmssl internally expects the public key as 128 hex chars (X+Y, no 04 prefix).
226
+ """
227
+ crypt = sm2.CryptSM2(private_key=private_key, public_key='', mode=mode)
228
+ if public_key_hex is not None:
229
+ pub = public_key_hex.strip()
230
+ if pub.startswith('04') and len(pub) == 130:
231
+ pub = pub[2:]
232
+ crypt.public_key = pub
233
+ return crypt
234
+
235
+ class SM2Util:
236
+ """
237
+ SM2 encryption/decryption/signature utility class.
238
+ Compatible with Java BouncyCastle SM2Engine (C1C3C2) and SM2Signer.
239
+
240
+ Fully based on the low-level gmssl library to guarantee interop with Java BouncyCastle.
241
+ """
242
+
243
+ @staticmethod
244
+ def generate_keypair_raw() -> (str, str):
245
+ """Generate a raw keypair (private_hex, public_hex)."""
246
+ priv = func.random_hex(64)
247
+ temp_crypt = sm2.CryptSM2(private_key=priv, public_key='04' + '00' * 64)
248
+ pub = temp_crypt._kg(int(priv, 16), temp_crypt.ecc_table['g'])
249
+ if not pub.startswith('04'):
250
+ pub = '04' + pub
251
+ return priv.lower(), pub.lower()
252
+
253
+ @staticmethod
254
+ def generate_keypair_hex() -> dict:
255
+ """Generate a HEX keypair, returning {'publicKey': ..., 'privateKey': ...} (public key 130 chars, private key 64 chars)."""
256
+ priv_hex, pub_hex = SM2Util.generate_keypair_raw()
257
+ return {'publicKey': pub_hex, 'privateKey': priv_hex}
258
+
259
+ @staticmethod
260
+ def generate_keypair_base64() -> dict:
261
+ """Generate a Base64 keypair, returning {'publicKey': ..., 'privateKey': ...}."""
262
+ priv_hex, pub_hex = SM2Util.generate_keypair_raw()
263
+ return {
264
+ 'publicKey': bytes_to_base64(hex_to_bytes(pub_hex)),
265
+ 'privateKey': bytes_to_base64(hex_to_bytes(priv_hex))
266
+ }
267
+
268
+ @staticmethod
269
+ def encrypt_to_hex(content: bytes, public_key) -> str:
270
+ """
271
+ Encrypt SM2 plaintext (C1C3C2 format, compatible with Java/Go).
272
+
273
+ Standard C1C3C2 layout: 04||X(32)||Y(32)||C3(32)||C2(msg_len).
274
+ gmssl's built-in encrypt returns a 64-byte C1 (no 04 prefix);
275
+ we prepend the 04 prefix here to match the standard Go/Java C1C3C2 format.
276
+ """
277
+ pub_hex = sm2_public_key_to_raw_hex(public_key)
278
+ crypt = _new_crypt(private_key='', public_key_hex=pub_hex, mode=1)
279
+ enc_bytes = crypt.encrypt(content)
280
+ # gmssl: C1(64 bytes) + C3(32) + C2(msg_len)
281
+ # -> standard: C1(65 bytes with 04) + C3(32) + C2(msg_len)
282
+ enc_bytes = b'\x04' + enc_bytes[:64] + enc_bytes[64:]
283
+ return enc_bytes.hex()
284
+
285
+ @staticmethod
286
+ def encrypt_to_bytes(content: bytes, public_key) -> bytes:
287
+ """Encrypt SM2 plaintext and return the C1C3C2 ciphertext bytes."""
288
+ return bytes.fromhex(SM2Util.encrypt_to_hex(content, public_key))
289
+
290
+ @staticmethod
291
+ def decrypt_from_bytes(cipher_bytes: bytes, private_key) -> bytes:
292
+ """Decrypt C1C3C2 ciphertext bytes and return the plaintext bytes."""
293
+ return SM2Util.decrypt_from_hex(cipher_bytes.hex(), private_key)
294
+
295
+ @staticmethod
296
+ def encrypt_to_base64(content: bytes, public_key) -> str:
297
+ hex_out = SM2Util.encrypt_to_hex(content, public_key)
298
+ return bytes_to_base64(bytes.fromhex(hex_out))
299
+
300
+ @staticmethod
301
+ def decrypt_from_hex(cipher_hex: str, private_key, public_key=None) -> bytes:
302
+ """
303
+ Decrypt SM2 ciphertext (C1C3C2 format, compatible with Java/Go/Python).
304
+
305
+ Supports two C1C3C2 layouts:
306
+ - standard (Java/Go): C1(65 bytes, 04+X+Y) + C3(32) + C2(msg_len)
307
+ - gmssl format: C1(64 bytes, X+Y) + C3(32) + C2(msg_len)
308
+ Auto-detects and handles both.
309
+ """
310
+ priv_hex = sm2_private_key_to_raw_hex(private_key)
311
+
312
+ # get or derive the public key
313
+ if public_key is not None:
314
+ pub_hex = sm2_public_key_to_raw_hex(public_key)
315
+ else:
316
+ temp_crypt = sm2.CryptSM2(private_key=priv_hex, public_key='04' + '00' * 64)
317
+ pub_hex = temp_crypt._kg(int(priv_hex, 16), temp_crypt.ecc_table['g'])
318
+ if not pub_hex.startswith('04'):
319
+ pub_hex = '04' + pub_hex
320
+
321
+ cipher_bytes = bytes.fromhex(cipher_hex)
322
+
323
+ # gmssl's built-in decrypt expects C1 without the 04 prefix (64 bytes)
324
+ # if the input is the standard format (C1 starts with 04, 65 bytes), strip the 04 prefix
325
+ if len(cipher_bytes) > 0 and cipher_bytes[0] == 0x04:
326
+ # standard: 04||X(32)||Y(32)||C3(32)||C2(msg_len) = 1+32+32+32+msg_len
327
+ # -> gmssl: X(32)||Y(32)||C3(32)||C2(msg_len) = 32+32+32+msg_len
328
+ cipher_bytes = cipher_bytes[1:65] + cipher_bytes[65:97] + cipher_bytes[97:]
329
+
330
+ crypt = _new_crypt(private_key=priv_hex, public_key_hex=pub_hex, mode=1)
331
+ result = crypt.decrypt(cipher_bytes)
332
+ if isinstance(result, str):
333
+ result = result.encode('latin-1')
334
+ return bytes(result)
335
+
336
+ @staticmethod
337
+ def decrypt_from_base64(cipher_b64: str, private_key) -> bytes:
338
+ cipher_bytes = base64_to_bytes(cipher_b64)
339
+ return SM2Util.decrypt_from_hex(bytes_to_hex(cipher_bytes), private_key)
340
+
341
+ @staticmethod
342
+ def sign_to_der(content: bytes, private_key) -> bytes:
343
+ """
344
+ Sign SM2 plaintext and return a DER-encoded signature.
345
+ Java: SM2Signer uses USER_ID="1234567812345678".
346
+ gmssl's sign_with_sm3 uses USER_ID = b"1234567812345678" by default.
347
+ """
348
+ priv_hex = sm2_private_key_to_raw_hex(private_key)
349
+
350
+ # gmssl sign requires public_key; derive it from the private key first
351
+ temp_crypt = sm2.CryptSM2(private_key=priv_hex, public_key='04' + '00' * 64)
352
+ pub_hex = temp_crypt._kg(int(priv_hex, 16), temp_crypt.ecc_table['g'])
353
+ if not pub_hex.startswith('04'):
354
+ pub_hex = '04' + pub_hex
355
+
356
+ crypt = _new_crypt(private_key=priv_hex, public_key_hex=pub_hex)
357
+ # sign_with_sm3 uses SM3 hash and auto-generates random K
358
+ sig_raw = crypt.sign_with_sm3(content) # returns hex r||s
359
+
360
+ if len(sig_raw) % 2 != 0:
361
+ raise ValueError('Invalid signature length')
362
+ half = len(sig_raw) // 2
363
+ r = sig_raw[:half]
364
+ s = sig_raw[half:]
365
+ return raw_rs_to_der(r, s)
366
+
367
+ @staticmethod
368
+ def sign_to_hex(content: bytes, private_key) -> str:
369
+ return SM2Util.sign_to_der(content, private_key).hex()
370
+
371
+ @staticmethod
372
+ def sign_to_base64(content: bytes, private_key) -> str:
373
+ return bytes_to_base64(SM2Util.sign_to_der(content, private_key))
374
+
375
+ @staticmethod
376
+ def verify(content: bytes, public_key, signature) -> bool:
377
+ """
378
+ Verify an SM2 signature.
379
+ Java: SM2Signer uses USER_ID="1234567812345678".
380
+ gmssl's verify uses USER_ID = b"1234567812345678" by default.
381
+ """
382
+ pub_hex = sm2_public_key_to_raw_hex(public_key)
383
+
384
+ # signature may be hex/base64/bytes; convert to raw r||s hex
385
+ sig_bytes = None
386
+ if isinstance(signature, bytes):
387
+ sig_bytes = signature
388
+ else:
389
+ s = signature.strip()
390
+ if is_hex_string(s):
391
+ sig_bytes = bytes.fromhex(s)
392
+ elif is_base64_string(s):
393
+ sig_bytes = base64_to_bytes(s)
394
+ else:
395
+ raise ValueError('Unsupported signature encoding')
396
+
397
+ # if DER (starts with 0x30) decode
398
+ if len(sig_bytes) > 0 and sig_bytes[0] == 0x30:
399
+ r_hex, s_hex = der_to_raw_rs(sig_bytes)
400
+ sig_raw = (r_hex + s_hex)
401
+ else:
402
+ sig_raw = sig_bytes.hex()
403
+
404
+ crypt = _new_crypt(private_key=None, public_key_hex=pub_hex)
405
+ return crypt.verify_with_sm3(sig_raw, content)
406
+
407
+ # convenience wrappers similar to Java API
408
+ @staticmethod
409
+ def encrypt(content: str, public_key, to_base64: bool = False) -> str:
410
+ b = utf8_to_bytes(content)
411
+ return SM2Util.encrypt_to_base64(b, public_key) if to_base64 else SM2Util.encrypt_to_hex(b, public_key)
412
+
413
+ @staticmethod
414
+ def decrypt(cipher_text: str, private_key) -> str:
415
+ s = cipher_text.strip()
416
+ if is_hex_string(s):
417
+ dec = SM2Util.decrypt_from_hex(s, private_key)
418
+ else:
419
+ dec = SM2Util.decrypt_from_base64(s, private_key)
420
+ return dec.decode('utf-8')
421
+
422
+ @staticmethod
423
+ def sign(content: str, private_key, to_base64: bool = False) -> str:
424
+ b = utf8_to_bytes(content)
425
+ if to_base64:
426
+ return SM2Util.sign_to_base64(b, private_key)
427
+ return SM2Util.sign_to_hex(b, private_key)
428
+
429
+ @staticmethod
430
+ def verify_str(content: str, public_key, signature: str) -> bool:
431
+ return SM2Util.verify(utf8_to_bytes(content), public_key, signature)
432
+
433
+ # ────────────────── Encrypt+Sign / Verify+Decrypt ──────────────────
434
+
435
+ @staticmethod
436
+ def encrypt_sign(content: str, enc_public_key, sign_private_key, to_base64: bool = False) -> dict:
437
+ """Encrypt and sign UTF-8 content. Returns {'ciphertext': ..., 'signature': ...}."""
438
+ ct = SM2Util.encrypt(content, enc_public_key, to_base64=to_base64)
439
+ sig = SM2Util.sign(ct, sign_private_key, to_base64=to_base64)
440
+ return {'ciphertext': ct, 'signature': sig}
441
+
442
+ @staticmethod
443
+ def verify_decrypt(ciphertext: str, signature: str, sign_public_key, dec_private_key) -> str:
444
+ """Verify signature then decrypt. Returns UTF-8 plaintext."""
445
+ if not SM2Util.verify_str(ciphertext, sign_public_key, signature):
446
+ raise ValueError('SM2 signature verification failed')
447
+ return SM2Util.decrypt(ciphertext, dec_private_key)
448
+
449
+ # ────────────────── PEM Export / Import ──────────────────
450
+
451
+ @staticmethod
452
+ def export_public_key_pem(pub_hex: str) -> str:
453
+ """Export raw-hex public key (130 chars) as PEM (X.509 SubjectPublicKeyInfo)."""
454
+ der = _encode_public_key_to_der(pub_hex)
455
+ b64 = bytes_to_base64(der)
456
+ lines = ['-----BEGIN PUBLIC KEY-----']
457
+ for i in range(0, len(b64), 64):
458
+ lines.append(b64[i:i + 64])
459
+ lines.append('-----END PUBLIC KEY-----')
460
+ return '\n'.join(lines)
461
+
462
+ @staticmethod
463
+ def export_private_key_pem(priv_hex: str, pub_hex: str) -> str:
464
+ """Export raw-hex keys as PEM (PKCS#8 PrivateKeyInfo)."""
465
+ der = _encode_private_key_to_der(priv_hex, pub_hex)
466
+ b64 = bytes_to_base64(der)
467
+ lines = ['-----BEGIN PRIVATE KEY-----']
468
+ for i in range(0, len(b64), 64):
469
+ lines.append(b64[i:i + 64])
470
+ lines.append('-----END PRIVATE KEY-----')
471
+ return '\n'.join(lines)
472
+
473
+ @staticmethod
474
+ def import_public_key_pem(pem: str) -> str:
475
+ """Import PEM (X.509 SPKI) and return raw-hex public key (130 chars)."""
476
+ begin = '-----BEGIN PUBLIC KEY-----'
477
+ end = '-----END PUBLIC KEY-----'
478
+ s = pem.find(begin)
479
+ e = pem.find(end)
480
+ if s == -1 or e == -1:
481
+ raise ValueError('Invalid PEM: missing PUBLIC KEY boundaries')
482
+ b64 = pem[s + len(begin):e].replace('\n', '').replace('\r', '').replace(' ', '')
483
+ der = base64_to_bytes(b64)
484
+ return sm2_public_key_to_raw_hex(der)
485
+
486
+ @staticmethod
487
+ def import_private_key_pem(pem: str) -> str:
488
+ """Import PEM (PKCS#8) and return raw-hex private key (64 chars)."""
489
+ begin = '-----BEGIN PRIVATE KEY-----'
490
+ end = '-----END PRIVATE KEY-----'
491
+ s = pem.find(begin)
492
+ e = pem.find(end)
493
+ if s == -1 or e == -1:
494
+ raise ValueError('Invalid PEM: missing PRIVATE KEY boundaries')
495
+ b64 = pem[s + len(begin):e].replace('\n', '').replace('\r', '').replace(' ', '')
496
+ der = base64_to_bytes(b64)
497
+ return sm2_private_key_to_raw_hex(der)
crossmool/sm3_util.py ADDED
@@ -0,0 +1,117 @@
1
+ from gmssl import sm3
2
+ from os import urandom
3
+ from crossmool.encoding import (
4
+ bytes_to_hex, bytes_to_base64, hex_to_bytes, base64_to_bytes,
5
+ is_hex_string, is_base64_string
6
+ )
7
+
8
+ class SM3Util:
9
+ @staticmethod
10
+ def digest(data: bytes) -> bytes:
11
+ return bytes.fromhex(sm3.sm3_hash(list(data)))
12
+
13
+ @staticmethod
14
+ def digest_to_hex(data: bytes) -> str:
15
+ return sm3.sm3_hash(list(data))
16
+
17
+ @staticmethod
18
+ def digest_to_bytes(data: bytes) -> bytes:
19
+ return SM3Util.digest(data)
20
+
21
+ @staticmethod
22
+ def digest_to_base64(data: bytes) -> str:
23
+ return bytes_to_base64(SM3Util.digest(data))
24
+
25
+ @staticmethod
26
+ def digest_to_str(data: bytes, to_base64: bool = False) -> str:
27
+ digest_bytes = SM3Util.digest(data)
28
+ if to_base64:
29
+ return bytes_to_base64(digest_bytes)
30
+ return bytes_to_hex(digest_bytes)
31
+
32
+ @staticmethod
33
+ def digest_str(input_str: str, to_base64: bool = False) -> str:
34
+ if not input_str or not input_str.strip():
35
+ raise ValueError("input_str cannot be empty")
36
+ data = input_str.encode('utf-8')
37
+ digest_bytes = SM3Util.digest(data)
38
+ if to_base64:
39
+ return bytes_to_base64(digest_bytes)
40
+ return bytes_to_hex(digest_bytes)
41
+
42
+ @staticmethod
43
+ def hmac(key: bytes, data: bytes) -> bytes:
44
+ block_size = 64
45
+ if len(key) > block_size:
46
+ key = SM3Util.digest(key)
47
+ if len(key) < block_size:
48
+ key = key + b"\x00" * (block_size - len(key))
49
+ o_key = bytes((b ^ 0x5C) for b in key)
50
+ i_key = bytes((b ^ 0x36) for b in key)
51
+ inner = sm3.sm3_hash(list(i_key + data))
52
+ inner_bytes = bytes.fromhex(inner)
53
+ outer = sm3.sm3_hash(list(o_key + inner_bytes))
54
+ return bytes.fromhex(outer)
55
+
56
+ @staticmethod
57
+ def hmac_from_str(key_str: str, data_str: str) -> bytes:
58
+ if not key_str or not key_str.strip():
59
+ raise ValueError("key_str cannot be empty")
60
+ if not data_str or not data_str.strip():
61
+ raise ValueError("data_str cannot be empty")
62
+
63
+ if is_hex_string(key_str):
64
+ key_bytes = hex_to_bytes(key_str)
65
+ elif is_base64_string(key_str):
66
+ key_bytes = base64_to_bytes(key_str)
67
+ else:
68
+ raise ValueError("key_str must be Hex string or Base64 string")
69
+
70
+ if is_hex_string(data_str):
71
+ data_bytes = hex_to_bytes(data_str)
72
+ elif is_base64_string(data_str):
73
+ data_bytes = base64_to_bytes(data_str)
74
+ else:
75
+ data_bytes = data_str.encode('utf-8')
76
+
77
+ return SM3Util.hmac(key_bytes, data_bytes)
78
+
79
+ @staticmethod
80
+ def hmac_to_hex(key: bytes, data: bytes) -> str:
81
+ return bytes_to_hex(SM3Util.hmac(key, data))
82
+
83
+ @staticmethod
84
+ def hmac_to_base64(key: bytes, data: bytes) -> str:
85
+ return bytes_to_base64(SM3Util.hmac(key, data))
86
+
87
+ @staticmethod
88
+ def hmac_to_str(key: bytes, data: bytes, to_base64: bool = False) -> str:
89
+ hmac_bytes = SM3Util.hmac(key, data)
90
+ if to_base64:
91
+ return bytes_to_base64(hmac_bytes)
92
+ return bytes_to_hex(hmac_bytes)
93
+
94
+ @staticmethod
95
+ def hmac_str(key_str: str, data_str: str, to_base64: bool = False) -> str:
96
+ hmac_bytes = SM3Util.hmac_from_str(key_str, data_str)
97
+ if to_base64:
98
+ return bytes_to_base64(hmac_bytes)
99
+ return bytes_to_hex(hmac_bytes)
100
+
101
+ @staticmethod
102
+ def generate_hmac_key(key_length: int = 32) -> bytes:
103
+ if key_length < 32:
104
+ raise ValueError("key_length must be >= 32")
105
+ return urandom(key_length)
106
+
107
+ @staticmethod
108
+ def generate_hmac_key_to_str(key_length: int = 32, to_base64: bool = False) -> str:
109
+ key_bytes = SM3Util.generate_hmac_key(key_length)
110
+ if to_base64:
111
+ return bytes_to_base64(key_bytes)
112
+ return bytes_to_hex(key_bytes)
113
+
114
+ @staticmethod
115
+ def hmac_verify(key_str: str, data_str: str, received_hmac: str) -> bool:
116
+ """Verify HMAC by computing expected value and comparing."""
117
+ return SM3Util.hmac_str(key_str, data_str) == received_hmac
crossmool/sm4_util.py ADDED
@@ -0,0 +1,186 @@
1
+ from os import urandom
2
+ from crossmool.encoding import (
3
+ bytes_to_hex, hex_to_bytes, bytes_to_base64, base64_to_bytes,
4
+ is_hex_string, is_base64_string
5
+ )
6
+ from gmssl import sm4
7
+
8
+ GCM_IV_SIZE = 12 # 96 bits
9
+ GCM_TAG_SIZE = 16 # 128 bits
10
+ BLOCK_SIZE = 16
11
+
12
+ def _xor_bytes(a: bytes, b: bytes) -> bytes:
13
+ return bytes(x ^ y for x, y in zip(a, b))
14
+
15
+ def _inc32(counter: bytes) -> bytes:
16
+ """GCM counter increment (increment the rightmost 32 bits)"""
17
+ counter_int = int.from_bytes(counter, 'big')
18
+ low_32 = (counter_int & 0xFFFFFFFF) + 1
19
+ result = (counter_int & ~0xFFFFFFFF) | (low_32 & 0xFFFFFFFF)
20
+ return result.to_bytes(16, 'big')
21
+
22
+ def _gf128_mul(x: int, y: int) -> int:
23
+ """Multiply two elements in GF(2^128) with reduction polynomial x^128+x^7+x^2+x+1"""
24
+ R = 0xE1000000000000000000000000000000
25
+ MASK = (1 << 128) - 1
26
+ z = 0
27
+ v = y
28
+ for i in range(128):
29
+ if x & (1 << (127 - i)):
30
+ z ^= v
31
+ if v & 1:
32
+ v = (v >> 1) ^ R
33
+ else:
34
+ v >>= 1
35
+ return z & MASK
36
+
37
+ def _ghash(h: bytes, aad: bytes, ciphertext: bytes) -> bytes:
38
+ """GHASH computation for GCM"""
39
+ h_int = int.from_bytes(h, 'big')
40
+
41
+ aad_padded = aad + b'\x00' * ((16 - len(aad) % 16) % 16)
42
+ ct_padded = ciphertext + b'\x00' * ((16 - len(ciphertext) % 16) % 16)
43
+ len_block = (len(aad) * 8).to_bytes(8, 'big') + (len(ciphertext) * 8).to_bytes(8, 'big')
44
+
45
+ y = 0
46
+ for i in range(0, len(aad_padded), 16):
47
+ y ^= int.from_bytes(aad_padded[i:i + 16], 'big')
48
+ y = _gf128_mul(y, h_int)
49
+ for i in range(0, len(ct_padded), 16):
50
+ y ^= int.from_bytes(ct_padded[i:i + 16], 'big')
51
+ y = _gf128_mul(y, h_int)
52
+ y ^= int.from_bytes(len_block, 'big')
53
+ y = _gf128_mul(y, h_int)
54
+
55
+ return y.to_bytes(16, 'big')
56
+
57
+ def _sm4_ecb_encrypt_block(key: bytes, block: bytes) -> bytes:
58
+ """Encrypt a single 16-byte block using SM4 ECB (no padding)"""
59
+ crypt = sm4.CryptSM4()
60
+ crypt.set_key(key, sm4.SM4_ENCRYPT)
61
+ return crypt.crypt_ecb(block)[:16]
62
+
63
+ def _sm4_ctr_encrypt(key: bytes, counter: bytes, plaintext: bytes) -> bytes:
64
+ """SM4 CTR mode encryption"""
65
+ result = bytearray()
66
+ for i in range(0, len(plaintext), BLOCK_SIZE):
67
+ keystream = _sm4_ecb_encrypt_block(key, counter)
68
+ block = plaintext[i:i + BLOCK_SIZE]
69
+ result.extend(_xor_bytes(keystream[:len(block)], block))
70
+ counter = _inc32(counter)
71
+ return bytes(result)
72
+
73
+ def _detect_and_decode(s: str) -> bytes:
74
+ """Auto-detect Hex/Base64 encoding and decode to bytes"""
75
+ if is_hex_string(s):
76
+ return hex_to_bytes(s)
77
+ if is_base64_string(s):
78
+ return base64_to_bytes(s)
79
+ raise ValueError("Input string must be Hex or Base64 encoded")
80
+
81
+ class SM4Util:
82
+ @staticmethod
83
+ def generate_key() -> bytes:
84
+ return urandom(16)
85
+
86
+ @staticmethod
87
+ def generate_key_hex() -> str:
88
+ return bytes_to_hex(SM4Util.generate_key())
89
+
90
+ @staticmethod
91
+ def generate_key_base64() -> str:
92
+ return bytes_to_base64(SM4Util.generate_key())
93
+
94
+ @staticmethod
95
+ def parse_hex_key(hex_key: str) -> bytes:
96
+ return hex_to_bytes(hex_key)
97
+
98
+ @staticmethod
99
+ def encrypt_gcm(key: bytes, plaintext: bytes, aad: bytes = b'', to_base64: bool = False) -> str:
100
+ """
101
+ SM4 GCM encrypt; returns Hex(IV[12] + ciphertext + Tag[16]) or Base64.
102
+ """
103
+ if len(key) != 16:
104
+ raise ValueError('SM4 key must be 16 bytes')
105
+
106
+ iv = urandom(GCM_IV_SIZE)
107
+ j0 = iv + b'\x00\x00\x00\x01'
108
+ h = _sm4_ecb_encrypt_block(key, b'\x00' * 16)
109
+ ciphertext = _sm4_ctr_encrypt(key, _inc32(j0), plaintext)
110
+ ghash_result = _ghash(h, aad, ciphertext)
111
+ j0_encrypted = _sm4_ecb_encrypt_block(key, j0)
112
+ tag = _xor_bytes(ghash_result, j0_encrypted)
113
+
114
+ combined = iv + ciphertext + tag
115
+ if to_base64:
116
+ return bytes_to_base64(combined)
117
+ return bytes_to_hex(combined)
118
+
119
+ @staticmethod
120
+ def decrypt_gcm(key: bytes, ciphertext_str: str, aad: bytes = b'') -> bytes:
121
+ """
122
+ SM4 GCM decrypt; auto-detects Hex/Base64 input encoding.
123
+ """
124
+ if len(key) != 16:
125
+ raise ValueError('SM4 key must be 16 bytes')
126
+
127
+ combined = _detect_and_decode(ciphertext_str)
128
+ if len(combined) < GCM_IV_SIZE + GCM_TAG_SIZE:
129
+ raise ValueError('Invalid GCM ciphertext: too short')
130
+
131
+ iv = combined[:GCM_IV_SIZE]
132
+ tag = combined[-GCM_TAG_SIZE:]
133
+ ciphertext = combined[GCM_IV_SIZE:-GCM_TAG_SIZE]
134
+
135
+ j0 = iv + b'\x00\x00\x00\x01'
136
+ h = _sm4_ecb_encrypt_block(key, b'\x00' * 16)
137
+ ghash_result = _ghash(h, aad, ciphertext)
138
+ j0_encrypted = _sm4_ecb_encrypt_block(key, j0)
139
+ computed_tag = _xor_bytes(ghash_result, j0_encrypted)
140
+
141
+ if computed_tag != tag:
142
+ raise ValueError('SM4 GCM decryption failed (authentication failed)')
143
+
144
+ return _sm4_ctr_encrypt(key, _inc32(j0), ciphertext)
145
+
146
+ @staticmethod
147
+ def encrypt_gcm_hex(key: bytes, plaintext: bytes, aad: bytes = b'') -> str:
148
+ return SM4Util.encrypt_gcm(key, plaintext, aad, to_base64=False)
149
+
150
+ @staticmethod
151
+ def decrypt_gcm_from_hex(key: bytes, hex_str: str, aad: bytes = b'') -> bytes:
152
+ if not is_hex_string(hex_str):
153
+ raise ValueError("Input must be Hex encoded")
154
+ return SM4Util.decrypt_gcm(key, hex_str, aad)
155
+
156
+ @staticmethod
157
+ def encrypt_gcm_string(key: bytes, plain_text: str, to_base64: bool = False) -> str:
158
+ return SM4Util.encrypt_gcm(key, plain_text.encode('utf-8'), to_base64=to_base64)
159
+
160
+ @staticmethod
161
+ def decrypt_gcm_to_string(key: bytes, ciphertext_str: str) -> str:
162
+ return SM4Util.decrypt_gcm(key, ciphertext_str).decode('utf-8')
163
+
164
+ if __name__ == '__main__':
165
+ key = SM4Util.generate_key()
166
+ hex_key = SM4Util.generate_key_hex()
167
+ print(f'Key (HEX): {hex_key}')
168
+
169
+ plain_text = 'Hello, 這是国密SM4測試數據'
170
+ print(f'Plaintext: {plain_text}')
171
+
172
+ encrypted_hex = SM4Util.encrypt_gcm_string(key, plain_text)
173
+ print(f'Encrypted (Hex): {encrypted_hex}')
174
+
175
+ encrypted_b64 = SM4Util.encrypt_gcm_string(key, plain_text, to_base64=True)
176
+ print(f'Encrypted (Base64): {encrypted_b64}')
177
+
178
+ decrypted = SM4Util.decrypt_gcm_to_string(key, encrypted_hex)
179
+ print(f'Decrypted from Hex: {decrypted}')
180
+
181
+ decrypted_b64 = SM4Util.decrypt_gcm_to_string(key, encrypted_b64)
182
+ print(f'Decrypted from Base64: {decrypted_b64}')
183
+
184
+ assert decrypted == plain_text, 'Decryption failed!'
185
+ assert decrypted_b64 == plain_text, 'Decryption from Base64 failed!'
186
+ print('GCM test passed!')
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: crossmool
3
+ Version: 1.3.0
4
+ Summary: Cross-language SM2/SM3/SM4 cryptographic utilities, interoperable with the Java/Go/Rust/C/C++/C#/JavaScript implementations
5
+ Author: crossmool
6
+ License-Expression: LGPL-3.0-only
7
+ Project-URL: Homepage, https://gitee.com/iilic/crossmool
8
+ Project-URL: Repository, https://gitee.com/iilic/crossmool
9
+ Keywords: sm2,sm3,sm4,cryptography,gmssl,sm-crypto,chinese-cryptography,encryption
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security :: Cryptography
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: gmssl>=3.2.2
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=7.0; extra == "test"
27
+
28
+ # crossmool
29
+
30
+ Cross-language SM2/SM3/SM4 cryptographic utilities. The Python implementation is fully interoperable with the Java/Go/Rust/C/C++/C#/JavaScript implementations in the [crossmool](https://gitee.com/iilic/crossmool) project.
31
+
32
+ ## Features
33
+
34
+ - **SM2** — asymmetric encryption/decryption and signature (C1C3C2 ciphertext, X.509 / PKCS#8 DER keys, DER signatures).
35
+ - **SM3** — hash and HMAC-SM3.
36
+ - **SM4** — symmetric GCM encryption/decryption.
37
+ - Hex and Base64 encodings supported throughout.
38
+ - Guaranteed cross-language interop (simplified Chinese, traditional Chinese and English Unicode input).
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install crossmool
44
+ ```
45
+
46
+ Requires Python 3.8+.
47
+
48
+ ## Quick start
49
+
50
+ ### SM2 — encrypt / decrypt / sign / verify
51
+
52
+ ```python
53
+ from crossmool import SM2Util
54
+
55
+ priv, pub = SM2Util.generate_keypair_raw()
56
+
57
+ # encrypt then decrypt (Hex)
58
+ cipher = SM2Util.encrypt('hello', pub)
59
+ assert SM2Util.decrypt(cipher, priv) == 'hello'
60
+
61
+ # sign then verify
62
+ sig = SM2Util.sign(cipher, priv)
63
+ assert SM2Util.verify_str(cipher, pub, sig)
64
+ ```
65
+
66
+ ### SM3 — hash / HMAC
67
+
68
+ ```python
69
+ from crossmool import SM3Util
70
+
71
+ digest = SM3Util.digest_str('hello 国密')
72
+ key = SM3Util.generate_hmac_key_to_str()
73
+ mac = SM3Util.hmac_str(key, 'hello 国密')
74
+ assert SM3Util.hmac_verify(key, 'hello 国密', mac)
75
+ ```
76
+
77
+ ### SM4 — GCM encrypt / decrypt
78
+
79
+ ```python
80
+ from crossmool import SM4Util
81
+
82
+ key = SM4Util.generate_key()
83
+ cipher = SM4Util.encrypt_gcm_string(key, 'hello 国密')
84
+ plain = SM4Util.decrypt_gcm_to_string(key, cipher)
85
+ assert plain == 'hello 国密'
86
+ ```
87
+
88
+ ## Development
89
+
90
+ Editable install for development:
91
+
92
+ ```bash
93
+ cd <repo-root>
94
+ pip install -e python/crossmool
95
+ ```
96
+
97
+ Run the test suite:
98
+
99
+ ```bash
100
+ cd python/crossmool
101
+ pytest -q
102
+ ```
103
+
104
+ ## License
105
+
106
+ LGPL-v3. See the [LICENSE](https://gitee.com/iilic/crossmool) file for details.
@@ -0,0 +1,10 @@
1
+ crossmool/__init__.py,sha256=tvacNRkKMlEiwnsI9iCrUxay76-CRrG79Ero76ddKWI,200
2
+ crossmool/der.py,sha256=JU_nsecWDM3GIew5skqbHpT5zccRznOHSzt5esf4k1U,2591
3
+ crossmool/encoding.py,sha256=8TFB878y7VJDAIzz4ic1SabrFNK5LggjuR3Qr6RVy8Q,1195
4
+ crossmool/sm2_util.py,sha256=xDsfreRd8Gb6ckRTjhqQpF7Q-16_JhUvOcRLNpvM-n0,20641
5
+ crossmool/sm3_util.py,sha256=vpncOd9FJ__fyrohNDDipGnU9xK28WF5UvYYx3cgiCM,4123
6
+ crossmool/sm4_util.py,sha256=cyM4WniSngDGK-jJzhMeshJJ8OHJD4CENIQQ2EVT3Cc,6625
7
+ crossmool-1.3.0.dist-info/METADATA,sha256=Hp0Gy-KaKjRcedNCOJv_TtWXjQ5Srj6z7zXqlKFPpZ4,3109
8
+ crossmool-1.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
9
+ crossmool-1.3.0.dist-info/top_level.txt,sha256=8ct3CncA4ykxKOlngLTUo6I02vZ2YVnKrSHaUwtgd6I,10
10
+ crossmool-1.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ crossmool