saro-dat 1.0.0__tar.gz

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.
saro_dat-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SARO Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: saro-dat
3
+ Version: 1.0.0
4
+ Summary: Distributed Access Token
5
+ Author-email: Marker Seoul <j@saro.me>
6
+ Project-URL: Homepage, https://dat.saro.me
7
+ Project-URL: Repository, https://github.com/saro-lab/dat-pypi
8
+ Project-URL: Documentation, https://dat.saro.me/ko/libs/pypi-saro-dat
9
+ Keywords: dat,distributed,access,token,jwt,security,authentication
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: cryptography>=47.0.0
14
+ Requires-Dist: readerwriterlock>=1.0.9
15
+ Dynamic: license-file
16
+
17
+ # DAT - Distributed Access Token
18
+
19
+ ## Document
20
+
21
+ ### [DAT Run Online](https://dat.saro.me)
22
+
23
+ ### [What is DAT](https://dat.saro.me/--/intro)
24
+
25
+ ### [Example](https://dat.saro.me/--/libs/pypi-saro-dat)
26
+
27
+ ## support signature algorithm
28
+ | name | algorithm |
29
+ |--------|------------|
30
+ | P256 | secp256r1 |
31
+ | P384 | secp384r1 |
32
+ | P521 | secp521r1 |
33
+
34
+ ## support crypto algorithm
35
+ | name | algorithm |
36
+ |------------|-----------------------------|
37
+ | AES128GCMN | aes-128-gcm n(nonce + body) |
38
+ | AES256GCMN | aes-256-cbc n(nonce + body) |
39
+
40
+
41
+ # Performance
42
+ - random plain and secure test
43
+ - mac mini m4 2024 basic (10 core)
44
+ - [test_bench.py](tests/test_bench.py)
45
+ ```
46
+ Plain: A9wAt86DfDVQCXzfijnfB7j5GWk9TOO4lapQS7AzYenPoRAELwaiqKa8IikDmT8FAfNZJFocR66Rvqcae3JcSf3OVIppE0lYDg4G
47
+ Secure: R2KJfHAFClJdS0je4TkU5JoTDfRMGjp5y5zn5sG2iCwsL6IVhjzCOXaVcQPAJwqiEJGDmd3Rdl6tCI0KwAcsjD4ZrqL3IEL5Jr2Q
48
+
49
+ Multi-Thread
50
+ P256 AES128GCMN Issue * 10000 : 192ms
51
+ P256 AES128GCMN Parse * 10000 : 171ms
52
+ P256 AES256GCMN Issue * 10000 : 191ms
53
+ P256 AES256GCMN Parse * 10000 : 168ms
54
+ P384 AES128GCMN Issue * 10000 : 847ms
55
+ P384 AES128GCMN Parse * 10000 : 1841ms
56
+ P384 AES256GCMN Issue * 10000 : 789ms
57
+ P384 AES256GCMN Parse * 10000 : 1829ms
58
+ P521 AES128GCMN Issue * 10000 : 684ms
59
+ P521 AES128GCMN Parse * 10000 : 1355ms
60
+ P521 AES256GCMN Issue * 10000 : 683ms
61
+ P521 AES256GCMN Parse * 10000 : 1343ms
62
+
63
+ Single-Thread
64
+ P256 AES128GCMN Issue * 10000 : 223ms
65
+ P256 AES128GCMN Parse * 10000 : 447ms
66
+ P256 AES256GCMN Issue * 10000 : 213ms
67
+ P256 AES256GCMN Parse * 10000 : 450ms
68
+ P384 AES128GCMN Issue * 10000 : 4915ms
69
+ P384 AES128GCMN Parse * 10000 : 11750ms
70
+ P384 AES256GCMN Issue * 10000 : 4915ms
71
+ P384 AES256GCMN Parse * 10000 : 11705ms
72
+ P521 AES128GCMN Issue * 10000 : 3576ms
73
+ P521 AES128GCMN Parse * 10000 : 7219ms
74
+ P521 AES256GCMN Issue * 10000 : 3623ms
75
+ P521 AES256GCMN Parse * 10000 : 7246ms
76
+ ```
@@ -0,0 +1,60 @@
1
+ # DAT - Distributed Access Token
2
+
3
+ ## Document
4
+
5
+ ### [DAT Run Online](https://dat.saro.me)
6
+
7
+ ### [What is DAT](https://dat.saro.me/--/intro)
8
+
9
+ ### [Example](https://dat.saro.me/--/libs/pypi-saro-dat)
10
+
11
+ ## support signature algorithm
12
+ | name | algorithm |
13
+ |--------|------------|
14
+ | P256 | secp256r1 |
15
+ | P384 | secp384r1 |
16
+ | P521 | secp521r1 |
17
+
18
+ ## support crypto algorithm
19
+ | name | algorithm |
20
+ |------------|-----------------------------|
21
+ | AES128GCMN | aes-128-gcm n(nonce + body) |
22
+ | AES256GCMN | aes-256-cbc n(nonce + body) |
23
+
24
+
25
+ # Performance
26
+ - random plain and secure test
27
+ - mac mini m4 2024 basic (10 core)
28
+ - [test_bench.py](tests/test_bench.py)
29
+ ```
30
+ Plain: A9wAt86DfDVQCXzfijnfB7j5GWk9TOO4lapQS7AzYenPoRAELwaiqKa8IikDmT8FAfNZJFocR66Rvqcae3JcSf3OVIppE0lYDg4G
31
+ Secure: R2KJfHAFClJdS0je4TkU5JoTDfRMGjp5y5zn5sG2iCwsL6IVhjzCOXaVcQPAJwqiEJGDmd3Rdl6tCI0KwAcsjD4ZrqL3IEL5Jr2Q
32
+
33
+ Multi-Thread
34
+ P256 AES128GCMN Issue * 10000 : 192ms
35
+ P256 AES128GCMN Parse * 10000 : 171ms
36
+ P256 AES256GCMN Issue * 10000 : 191ms
37
+ P256 AES256GCMN Parse * 10000 : 168ms
38
+ P384 AES128GCMN Issue * 10000 : 847ms
39
+ P384 AES128GCMN Parse * 10000 : 1841ms
40
+ P384 AES256GCMN Issue * 10000 : 789ms
41
+ P384 AES256GCMN Parse * 10000 : 1829ms
42
+ P521 AES128GCMN Issue * 10000 : 684ms
43
+ P521 AES128GCMN Parse * 10000 : 1355ms
44
+ P521 AES256GCMN Issue * 10000 : 683ms
45
+ P521 AES256GCMN Parse * 10000 : 1343ms
46
+
47
+ Single-Thread
48
+ P256 AES128GCMN Issue * 10000 : 223ms
49
+ P256 AES128GCMN Parse * 10000 : 447ms
50
+ P256 AES256GCMN Issue * 10000 : 213ms
51
+ P256 AES256GCMN Parse * 10000 : 450ms
52
+ P384 AES128GCMN Issue * 10000 : 4915ms
53
+ P384 AES128GCMN Parse * 10000 : 11750ms
54
+ P384 AES256GCMN Issue * 10000 : 4915ms
55
+ P384 AES256GCMN Parse * 10000 : 11705ms
56
+ P521 AES128GCMN Issue * 10000 : 3576ms
57
+ P521 AES128GCMN Parse * 10000 : 7219ms
58
+ P521 AES256GCMN Issue * 10000 : 3623ms
59
+ P521 AES256GCMN Parse * 10000 : 7246ms
60
+ ```
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "saro-dat"
3
+ version = "1.0.0"
4
+ description = "Distributed Access Token"
5
+ keywords = ["dat", "distributed", "access", "token", "jwt", "security", "authentication"]
6
+ readme = "README.md"
7
+ authors = [{name = "Marker Seoul", email = "j@saro.me"}]
8
+ requires-python = ">=3.9"
9
+ dependencies = [
10
+ "cryptography>=47.0.0",
11
+ "readerwriterlock>=1.0.9",
12
+ ]
13
+
14
+ [project.urls]
15
+ "Homepage" = "https://dat.saro.me"
16
+ "Repository" = "https://github.com/saro-lab/dat-pypi"
17
+ "Documentation" = "https://dat.saro.me/ko/libs/pypi-saro-dat"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "pytest>=8.4.2",
22
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ from .crypto import DatCryptoAlgorithm, DatCryptoKey
2
+ from .dat import Dat, DatPayload
3
+ from .dat_certificate import DatCertificate
4
+ from .dat_manager import DatManager
5
+ from .signature import DatSignatureAlgorithm, DatSignatureKey, DatSignatureKeyOutOption
6
+
7
+ __all__ = [
8
+ "DatManager",
9
+ "DatCertificate",
10
+ "Dat",
11
+ "DatPayload",
12
+ "DatCryptoKey",
13
+ "DatCryptoAlgorithm",
14
+ "DatSignatureKey",
15
+ "DatSignatureAlgorithm",
16
+ "DatSignatureKeyOutOption",
17
+ ]
@@ -0,0 +1,79 @@
1
+ import os
2
+ from enum import Enum
3
+ from typing import Union, Dict, Optional
4
+
5
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
6
+
7
+ from .util import decode_base64_url
8
+
9
+
10
+ class DatCryptoAlgorithm(str, Enum):
11
+ AES128GCMN = "AES128GCMN"
12
+ AES256GCMN = "AES256GCMN"
13
+
14
+ CRYPTO_CONFIG: Dict[str, dict] = {
15
+ "AES128GCMN": {"name": "AES-GCM", "length": 16},
16
+ "AES256GCMN": {"name": "AES-GCM", "length": 32},
17
+ }
18
+
19
+ def get_crypto_config(algorithm: str) -> dict:
20
+ config = CRYPTO_CONFIG.get(algorithm)
21
+ if config:
22
+ return config
23
+ raise ValueError(f"Unsupported DAT Crypto Algorithm: {algorithm}")
24
+
25
+ class DatCryptoKey:
26
+ def __init__(self, algorithm: DatCryptoAlgorithm, key_bytes: bytes, config: Optional[Dict[str, dict]] = None):
27
+ if config is None:
28
+ config = get_crypto_config(algorithm)
29
+ self.algorithm = algorithm
30
+ self._config = config
31
+ self._key_bytes = key_bytes
32
+ self._cipher = AESGCM(key_bytes)
33
+
34
+ @classmethod
35
+ def generate(cls, algorithm: DatCryptoAlgorithm) -> DatCryptoKey:
36
+ config = get_crypto_config(algorithm)
37
+ key_bytes = AESGCM.generate_key(bit_length=config['length'] * 8)
38
+ return cls(algorithm, key_bytes, config)
39
+
40
+ @classmethod
41
+ def imports(cls, algorithm: str, raw: bytes) -> DatCryptoKey:
42
+ return cls(DatCryptoAlgorithm(algorithm), raw)
43
+
44
+ def exports(self) -> bytes:
45
+ return self._key_bytes
46
+
47
+ def encrypt(self, data: Union[bytes, str, None]) -> bytes:
48
+ if isinstance(data, str):
49
+ data = data.encode('utf-8')
50
+
51
+ if not data:
52
+ return b""
53
+
54
+ #if self._config["name"] == "AES-GCM":
55
+
56
+ nonce = os.urandom(12)
57
+ ciphertext = self._cipher.encrypt(nonce, data, None)
58
+ return nonce + ciphertext
59
+
60
+ #raise ValueError(f"Unsupported DAT Crypto Algorithm: {self.algorithm}")
61
+
62
+ def decrypt(self, data: Union[bytes, str, None]) -> bytes:
63
+ if isinstance(data, str):
64
+ data = decode_base64_url(data)
65
+
66
+ if not data:
67
+ return b""
68
+
69
+ #if self._config["name"] == "AES-GCM":
70
+
71
+ if len(data) <= 12:
72
+ raise ValueError("Invalid data length")
73
+
74
+ nonce = data[:12]
75
+ ciphertext_with_tag = data[12:]
76
+
77
+ return self._cipher.decrypt(nonce, ciphertext_with_tag, None)
78
+
79
+ #raise ValueError(f"Unsupported DAT Crypto Algorithm: {self.algorithm}")
@@ -0,0 +1,60 @@
1
+ import time
2
+ from typing import Optional, Union
3
+
4
+ from .util import decode_base64_url
5
+
6
+
7
+ class Dat:
8
+ def __init__(self, dat_str: Optional[str]):
9
+ self.dat = dat_str or ''
10
+ self._format = False
11
+ self._expire = 0
12
+ self._cid = 0
13
+ self._plain = b''
14
+ self._secure = b''
15
+ self._signature = b''
16
+
17
+ if self.dat:
18
+ parts = self.dat.split('.')
19
+ if len(parts) == 5:
20
+ try:
21
+ # parts: [expire, cid(hex), plain(b64), secure(b64), signature(b64)]
22
+ self._expire = int(parts[0])
23
+ self._cid = int(parts[1], 16)
24
+ self._plain = decode_base64_url(parts[2])
25
+ self._secure = decode_base64_url(parts[3])
26
+ self._signature = decode_base64_url(parts[4])
27
+ self._format = (len(self._signature) > 0 and self._expire >= 0)
28
+ except (ValueError, RuntimeError):
29
+ self._format = False
30
+
31
+ @classmethod
32
+ def from_value(cls, value: Union[Dat, str, None]) -> Dat:
33
+ if isinstance(value, Dat):
34
+ return value
35
+ return cls(value)
36
+
37
+ def expired(self) -> bool:
38
+ if not self._format:
39
+ return True
40
+ return int(time.time()) > self._expire
41
+
42
+ def body_string(self) -> str:
43
+ """서명 검증을 위한 서명 제외 나머지 본문 반환"""
44
+ if '.' not in self.dat:
45
+ return ""
46
+ return self.dat.rsplit('.', 1)[0]
47
+
48
+ class DatPayload:
49
+ def __init__(self, expire: int, plain: bytes, secure: bytes):
50
+ self.expire = expire
51
+ self.plain_bytes = plain
52
+ self.secure_bytes = secure
53
+
54
+ @property
55
+ def plain(self) -> str:
56
+ return self.plain_bytes.decode('utf-8')
57
+
58
+ @property
59
+ def secure(self) -> str:
60
+ return self.secure_bytes.decode('utf-8')
@@ -0,0 +1,57 @@
1
+ import time
2
+
3
+ from .crypto import DatCryptoKey
4
+ from .signature import DatSignatureKey, DatSignatureKeyOutOption
5
+ from .util import encode_base64_url_str, decode_base64_url
6
+
7
+
8
+ class DatCertificate:
9
+ def __init__(
10
+ self,
11
+ cid: int,
12
+ signature_key: DatSignatureKey,
13
+ crypto_key: DatCryptoKey,
14
+ dat_issue_begin: int,
15
+ dat_issue_end: int,
16
+ dat_ttl: int
17
+ ):
18
+ self.cid = cid
19
+ self._signature_key = signature_key
20
+ self._crypto_key = crypto_key
21
+ self._dat_issue_begin = dat_issue_begin
22
+ self._dat_issue_end = dat_issue_end
23
+ self._dat_ttl = dat_ttl
24
+
25
+ def exports(self, option: DatSignatureKeyOutOption) -> str:
26
+ cid_hex = hex(self.cid)[2:]
27
+ sig_alg = self._signature_key.algorithm.value
28
+ sig_key = self._signature_key.exports(option)
29
+ cry_alg = self._crypto_key.algorithm.value
30
+ cry_key = encode_base64_url_str(self._crypto_key.exports())
31
+
32
+ return f"{cid_hex}.{sig_alg}.{sig_key}.{cry_alg}.{cry_key}.{self._dat_issue_begin}.{self._dat_issue_end}.{self._dat_ttl}"
33
+
34
+ @classmethod
35
+ def imports(cls, format_str: str) -> DatCertificate:
36
+ split = format_str.split(".")
37
+ if len(split) != 8:
38
+ raise ValueError("Invalid Certificate format")
39
+
40
+ cid = int(split[0], 16)
41
+ sig_key = DatSignatureKey.imports(split[1], split[2])
42
+ cry_key = DatCryptoKey.imports(split[3], decode_base64_url(split[4]))
43
+
44
+ return cls(
45
+ cid, sig_key, cry_key,
46
+ int(split[5]), int(split[6]), int(split[7])
47
+ )
48
+
49
+ def issuable(self) -> bool:
50
+ now = int(time.time())
51
+ return self.has_signing_key() and self._dat_issue_begin <= now <= self._dat_issue_end
52
+
53
+ def expired(self) -> bool:
54
+ return int(time.time()) > (self._dat_issue_end + self._dat_ttl)
55
+
56
+ def has_signing_key(self) -> bool:
57
+ return self._signature_key.has_signing_key()
@@ -0,0 +1,115 @@
1
+ import time
2
+ from typing import List, Optional, Union
3
+
4
+ from readerwriterlock import rwlock
5
+
6
+ from . import DatCertificate, Dat, DatPayload
7
+ from .signature import DatSignatureKeyOutOption
8
+ from .util import encode_base64_url_str
9
+
10
+
11
+ class DatManager:
12
+ def __init__(self):
13
+ self._issuer = None
14
+ self._certificates = []
15
+ self._lock = rwlock.RWLockFairD()
16
+
17
+
18
+ def import_certificates(self, input_certs: List[DatCertificate], clear: bool = False):
19
+ certificates = []
20
+
21
+ if not clear:
22
+ with self._lock.gen_rlock():
23
+ certificates.extend(self._certificates)
24
+
25
+ before_cids = set(map(lambda x: x.cid,certificates))
26
+ seen_cids = set()
27
+
28
+ for cert in input_certs:
29
+ if cert.cid in seen_cids:
30
+ raise ValueError(f"Duplicate CID: {cert.cid}")
31
+ seen_cids.add(cert.cid)
32
+ if cert.expired():
33
+ continue
34
+ if cert.cid in before_cids:
35
+ continue
36
+ certificates.append(cert)
37
+
38
+ certificates.sort(key=lambda x: x._dat_issue_end)
39
+ issuer = next((c for c in reversed(certificates) if c.issuable()), None)
40
+
41
+ with self._lock.gen_wlock():
42
+ self._issuer = issuer
43
+ self._certificates = certificates
44
+
45
+
46
+ def imports(self, format_str: str, clear: bool = False):
47
+ certs = []
48
+ for line in format_str.strip().split('\n'):
49
+ if line.strip():
50
+ certs.append(DatCertificate.imports(line.strip()))
51
+ self.import_certificates(certs, clear)
52
+
53
+ def exports(self, option: DatSignatureKeyOutOption) -> str:
54
+ lines = []
55
+
56
+ with self._lock.gen_rlock():
57
+ for cert in self._certificates:
58
+ lines.append(cert.exports(option))
59
+
60
+ return '\n'.join(lines)
61
+
62
+ def _find_unsafe(self, cid: int) -> Optional[DatCertificate]:
63
+ return next((c for c in self._certificates if c.cid == cid), None)
64
+
65
+ def issue(self, plain: Union[bytes, str, None], secure: Union[bytes, str, None]) -> str:
66
+ with self._lock.gen_rlock():
67
+ if self._issuer:
68
+ return self._issue(self._issuer, plain, secure)
69
+ raise RuntimeError("Invalid DAT: Signing Key Does Not Exist")
70
+
71
+ def parse(self, dat_input: Union[Dat, str, None]) -> DatPayload:
72
+ dat = Dat.from_value(dat_input)
73
+ if not dat._format:
74
+ raise ValueError("Invalid DAT: Format")
75
+
76
+ with self._lock.gen_rlock():
77
+ certificate = self._find_unsafe(dat._cid)
78
+ if certificate is not None:
79
+ return self._parse(certificate, dat)
80
+ raise ValueError("Invalid DAT: CID(Certificate ID) Not Found")
81
+
82
+ @staticmethod
83
+ def _issue(cert: DatCertificate, plain: Union[bytes, str, None], secure: Union[bytes, str, None]) -> str:
84
+ now = int(time.time())
85
+ expire = now + cert._dat_ttl
86
+ cid_hex = hex(cert.cid)[2:]
87
+
88
+ # Plain 데이터 처리 (문자열인 경우 utf-8 바이트로)
89
+ plain_bytes = plain.encode() if isinstance(plain, str) else (plain or b'')
90
+ plain_b64 = encode_base64_url_str(plain_bytes)
91
+
92
+ # Secure 데이터 암호화
93
+ encrypted_secure = cert._crypto_key.encrypt(secure)
94
+ secure_b64 = encode_base64_url_str(encrypted_secure)
95
+
96
+ body = f"{expire}.{cid_hex}.{plain_b64}.{secure_b64}"
97
+ signature = encode_base64_url_str(cert._signature_key.sign(body))
98
+
99
+ return f"{body}.{signature}"
100
+
101
+ @staticmethod
102
+ def _parse(cert: DatCertificate, dat_input: Union[Dat, str, None]) -> DatPayload:
103
+ dat = Dat.from_value(dat_input)
104
+ if not dat._format:
105
+ raise RuntimeError("Invalid DAT: Format")
106
+ if dat.expired():
107
+ raise RuntimeError("Invalid DAT: Expired")
108
+
109
+ # 서명 검증
110
+ if not cert._signature_key.verify(dat.body_string(), dat._signature):
111
+ raise RuntimeError("Invalid DAT: Signature")
112
+
113
+ # 데이터 복호화
114
+ decrypted_secure = cert._crypto_key.decrypt(dat._secure)
115
+ return DatPayload(dat._expire, dat._plain, decrypted_secure)
@@ -0,0 +1,174 @@
1
+ from enum import Enum
2
+ from typing import Union, Optional, TypeAlias
3
+
4
+ from cryptography.hazmat.primitives import hashes
5
+ from cryptography.hazmat.primitives import serialization
6
+ from cryptography.hazmat.primitives.asymmetric import ec
7
+ from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
8
+ from cryptography.hazmat.primitives.hashes import HashAlgorithm
9
+
10
+ from .util import decode_base64_url, encode_base64_url_str
11
+
12
+
13
+ class DatSignatureAlgorithm(str, Enum):
14
+ P256 = "P256"
15
+ P384 = "P384"
16
+ P521 = "P521"
17
+
18
+ class DatSignatureKeyOutOption(str, Enum):
19
+ FULL = "FULL"
20
+ SIGNING = "SIGNING"
21
+ VERIFYING = "VERIFYING"
22
+
23
+ CurveType: TypeAlias = Union[EllipticCurve]
24
+ HashType: TypeAlias = Union[HashAlgorithm]
25
+ ConfigValue: TypeAlias = dict[str, Union[CurveType, HashAlgorithm]]
26
+
27
+ SIGNATURE_CONFIG: dict[str, ConfigValue] = {
28
+ "P256": {"curve": ec.SECP256R1(), "hash": hashes.SHA256()},
29
+ "P384": {"curve": ec.SECP384R1(), "hash": hashes.SHA384()},
30
+ "P521": {"curve": ec.SECP521R1(), "hash": hashes.SHA512()},
31
+ }
32
+
33
+ def get_signature_config(algorithm: str) -> dict:
34
+ config = SIGNATURE_CONFIG.get(algorithm)
35
+ if config:
36
+ return config
37
+ raise ValueError(f"Unsupported DAT Crypto Algorithm: {algorithm}")
38
+
39
+ class DatSignatureKey:
40
+ def __init__(
41
+ self,
42
+ algorithm: DatSignatureAlgorithm,
43
+ signing_key: Optional[ec.EllipticCurvePrivateKey],
44
+ verifying_key: ec.EllipticCurvePublicKey,
45
+ config: ConfigValue
46
+ ):
47
+ if config is None:
48
+ config = get_signature_config(algorithm)
49
+ self.algorithm = algorithm
50
+ self.signing_key = signing_key
51
+ self.verifying_key = verifying_key
52
+ self._config = config
53
+
54
+ @staticmethod
55
+ def generate(algorithm: Union[DatSignatureAlgorithm, str]) -> DatSignatureKey:
56
+ config = get_signature_config(algorithm)
57
+ if isinstance(algorithm, str):
58
+ algorithm = DatSignatureAlgorithm(algorithm)
59
+ private_key = ec.generate_private_key(config["curve"])
60
+ public_key = private_key.public_key()
61
+ return DatSignatureKey(algorithm, private_key, public_key, config)
62
+
63
+ @staticmethod
64
+ def imports(algorithm: Union[DatSignatureAlgorithm, str], format_str: str) -> DatSignatureKey:
65
+ config = get_signature_config(algorithm)
66
+ if isinstance(algorithm, str):
67
+ algorithm = DatSignatureAlgorithm(algorithm)
68
+
69
+ parts = format_str.split("~")
70
+ parts_len = len(parts)
71
+
72
+ if not (parts_len in range(1, 3)):
73
+ raise ValueError("Invalid DAT Signature Key Format: No keys found")
74
+
75
+ signing_key = None
76
+ verifying_key = None
77
+
78
+ if parts[0]: # 첫 번째 파트가 비어있지 않으면 개인키 존재
79
+ d_value = int.from_bytes(decode_base64_url(parts[0]), 'big')
80
+ signing_key = ec.derive_private_key(d_value, config["curve"])
81
+
82
+ # 2. Public Key (Verifying Key) 처리
83
+ if parts_len == 2 and parts[1]:
84
+ public_bytes = decode_base64_url(parts[1])
85
+ verifying_key = ec.EllipticCurvePublicKey.from_encoded_point(
86
+ config["curve"], public_bytes
87
+ )
88
+ elif signing_key:
89
+ verifying_key = signing_key.public_key()
90
+ else:
91
+ raise ValueError("Invalid DAT Signature Key Format: No keys found")
92
+
93
+ return DatSignatureKey(algorithm, signing_key, verifying_key, config)
94
+
95
+ def exports(self, option: DatSignatureKeyOutOption) -> str:
96
+ rv_parts = ["", ""] # [signing, verifying]
97
+
98
+ if option in ["FULL", "SIGNING"]:
99
+ if self.signing_key:
100
+ # Private Key 'd' 값을 고정된 길이의 바이트로 추출
101
+ private_numbers = self.signing_key.private_numbers()
102
+ curve_size = (self.signing_key.curve.key_size + 7) // 8
103
+ d_bytes = private_numbers.private_value.to_bytes(curve_size, 'big')
104
+ rv_parts[0] = encode_base64_url_str(d_bytes)
105
+ elif option == "SIGNING":
106
+ raise ValueError("Signature key is not supported - verifying only key")
107
+
108
+ if option in ["FULL", "VERIFYING"]:
109
+ # Public Key를 Raw Bytes(Uncompressed)로 추출
110
+ public_bytes = self.verifying_key.public_bytes(
111
+ encoding=serialization.Encoding.X962,
112
+ format=serialization.PublicFormat.UncompressedPoint
113
+ )
114
+ rv_parts[1] = encode_base64_url_str(public_bytes)
115
+
116
+ if option == "SIGNING":
117
+ return rv_parts[0]
118
+ if option == "VERIFYING":
119
+ return "~" + rv_parts[1]
120
+ return f"{rv_parts[0]}~{rv_parts[1]}"
121
+
122
+ def sign(self, body: Union[bytes, str]) -> bytes:
123
+ if not self.signing_key:
124
+ raise ValueError("Signature key is not supported - verifying only key")
125
+
126
+ if isinstance(body, str):
127
+ body = body.encode()
128
+
129
+ if not body:
130
+ raise ValueError("Sign Error - body is empty")
131
+
132
+ signature = self.signing_key.sign(
133
+ body,
134
+ ec.ECDSA(self._config["hash"])
135
+ )
136
+
137
+ return self._der_to_raw_signature(signature)
138
+
139
+ def verify(self, body: Union[bytes, str], signature: Union[bytes, str]) -> bool:
140
+ if isinstance(body, str):
141
+ body = body.encode('utf-8')
142
+ if not body:
143
+ return False
144
+
145
+ sig_bytes = decode_base64_url(signature) if isinstance(signature, str) else signature
146
+
147
+ try:
148
+ # Raw (R|S) -> DER 변환 후 검증
149
+ der_sig = self._raw_to_der_signature(sig_bytes)
150
+ self.verifying_key.verify(
151
+ der_sig,
152
+ body,
153
+ ec.ECDSA(self._config["hash"])
154
+ )
155
+ return True
156
+ except Exception:
157
+ return False
158
+
159
+ def has_signing_key(self) -> bool:
160
+ return self.signing_key is not None
161
+
162
+ # --- 유틸리티: Web Crypto (Raw R|S) <-> OpenSSL (DER) 변환 ---
163
+ def _der_to_raw_signature(self, signature: bytes) -> bytes:
164
+ from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
165
+ r, s = decode_dss_signature(signature)
166
+ size = (self.verifying_key.curve.key_size + 7) // 8
167
+ return r.to_bytes(size, 'big') + s.to_bytes(size, 'big')
168
+
169
+ def _raw_to_der_signature(self, signature: bytes) -> bytes:
170
+ from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
171
+ size = len(signature) // 2
172
+ r = int.from_bytes(signature[:size], 'big')
173
+ s = int.from_bytes(signature[size:], 'big')
174
+ return encode_dss_signature(r, s)
@@ -0,0 +1,30 @@
1
+ import base64
2
+ from typing import Union
3
+
4
+
5
+ def encode_base64_url(s: Union[bytes, str, None]) -> bytes:
6
+ if isinstance(s, str):
7
+ if s == "":
8
+ return b""
9
+ s = s.encode('utf-8')
10
+ if s is None:
11
+ return b""
12
+ return base64.urlsafe_b64encode(s).rstrip(b'=')
13
+
14
+ def encode_base64_url_str(s: Union[bytes, str, None]) -> str:
15
+ return encode_base64_url(s).decode('ascii')
16
+
17
+ def decode_base64_url(s: Union[bytes, str, None]) -> bytes:
18
+ if isinstance(s, str):
19
+ if s == "":
20
+ return b""
21
+ s = s.encode('utf-8')
22
+ if s is None:
23
+ return b""
24
+ rem = len(s) % 4
25
+ if rem > 0:
26
+ s += b'=' * (4 - rem)
27
+ return base64.urlsafe_b64decode(s)
28
+
29
+ def decode_base64_url_str(s: Union[bytes, str, None]) -> str:
30
+ return decode_base64_url(s).decode('utf-8')
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: saro-dat
3
+ Version: 1.0.0
4
+ Summary: Distributed Access Token
5
+ Author-email: Marker Seoul <j@saro.me>
6
+ Project-URL: Homepage, https://dat.saro.me
7
+ Project-URL: Repository, https://github.com/saro-lab/dat-pypi
8
+ Project-URL: Documentation, https://dat.saro.me/ko/libs/pypi-saro-dat
9
+ Keywords: dat,distributed,access,token,jwt,security,authentication
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: cryptography>=47.0.0
14
+ Requires-Dist: readerwriterlock>=1.0.9
15
+ Dynamic: license-file
16
+
17
+ # DAT - Distributed Access Token
18
+
19
+ ## Document
20
+
21
+ ### [DAT Run Online](https://dat.saro.me)
22
+
23
+ ### [What is DAT](https://dat.saro.me/--/intro)
24
+
25
+ ### [Example](https://dat.saro.me/--/libs/pypi-saro-dat)
26
+
27
+ ## support signature algorithm
28
+ | name | algorithm |
29
+ |--------|------------|
30
+ | P256 | secp256r1 |
31
+ | P384 | secp384r1 |
32
+ | P521 | secp521r1 |
33
+
34
+ ## support crypto algorithm
35
+ | name | algorithm |
36
+ |------------|-----------------------------|
37
+ | AES128GCMN | aes-128-gcm n(nonce + body) |
38
+ | AES256GCMN | aes-256-cbc n(nonce + body) |
39
+
40
+
41
+ # Performance
42
+ - random plain and secure test
43
+ - mac mini m4 2024 basic (10 core)
44
+ - [test_bench.py](tests/test_bench.py)
45
+ ```
46
+ Plain: A9wAt86DfDVQCXzfijnfB7j5GWk9TOO4lapQS7AzYenPoRAELwaiqKa8IikDmT8FAfNZJFocR66Rvqcae3JcSf3OVIppE0lYDg4G
47
+ Secure: R2KJfHAFClJdS0je4TkU5JoTDfRMGjp5y5zn5sG2iCwsL6IVhjzCOXaVcQPAJwqiEJGDmd3Rdl6tCI0KwAcsjD4ZrqL3IEL5Jr2Q
48
+
49
+ Multi-Thread
50
+ P256 AES128GCMN Issue * 10000 : 192ms
51
+ P256 AES128GCMN Parse * 10000 : 171ms
52
+ P256 AES256GCMN Issue * 10000 : 191ms
53
+ P256 AES256GCMN Parse * 10000 : 168ms
54
+ P384 AES128GCMN Issue * 10000 : 847ms
55
+ P384 AES128GCMN Parse * 10000 : 1841ms
56
+ P384 AES256GCMN Issue * 10000 : 789ms
57
+ P384 AES256GCMN Parse * 10000 : 1829ms
58
+ P521 AES128GCMN Issue * 10000 : 684ms
59
+ P521 AES128GCMN Parse * 10000 : 1355ms
60
+ P521 AES256GCMN Issue * 10000 : 683ms
61
+ P521 AES256GCMN Parse * 10000 : 1343ms
62
+
63
+ Single-Thread
64
+ P256 AES128GCMN Issue * 10000 : 223ms
65
+ P256 AES128GCMN Parse * 10000 : 447ms
66
+ P256 AES256GCMN Issue * 10000 : 213ms
67
+ P256 AES256GCMN Parse * 10000 : 450ms
68
+ P384 AES128GCMN Issue * 10000 : 4915ms
69
+ P384 AES128GCMN Parse * 10000 : 11750ms
70
+ P384 AES256GCMN Issue * 10000 : 4915ms
71
+ P384 AES256GCMN Parse * 10000 : 11705ms
72
+ P521 AES128GCMN Issue * 10000 : 3576ms
73
+ P521 AES128GCMN Parse * 10000 : 7219ms
74
+ P521 AES256GCMN Issue * 10000 : 3623ms
75
+ P521 AES256GCMN Parse * 10000 : 7246ms
76
+ ```
@@ -0,0 +1,22 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/saro_dat/__init__.py
5
+ src/saro_dat/crypto.py
6
+ src/saro_dat/dat.py
7
+ src/saro_dat/dat_certificate.py
8
+ src/saro_dat/dat_manager.py
9
+ src/saro_dat/signature.py
10
+ src/saro_dat/util.py
11
+ src/saro_dat.egg-info/PKG-INFO
12
+ src/saro_dat.egg-info/SOURCES.txt
13
+ src/saro_dat.egg-info/dependency_links.txt
14
+ src/saro_dat.egg-info/requires.txt
15
+ src/saro_dat.egg-info/top_level.txt
16
+ tests/test_bench.py
17
+ tests/test_certificate.py
18
+ tests/test_crypto.py
19
+ tests/test_example.py
20
+ tests/test_manager.py
21
+ tests/test_signature.py
22
+ tests/test_util.py
@@ -0,0 +1,2 @@
1
+ cryptography>=47.0.0
2
+ readerwriterlock>=1.0.9
@@ -0,0 +1 @@
1
+ saro_dat
@@ -0,0 +1,94 @@
1
+ import asyncio
2
+ import secrets
3
+ import string
4
+ import time
5
+ import unittest
6
+ from concurrent.futures import ThreadPoolExecutor
7
+
8
+ from saro_dat import DatSignatureAlgorithm, DatCryptoAlgorithm, DatCertificate, DatManager, DatSignatureKey, DatCryptoKey
9
+
10
+
11
+ def generate_base62(length: int) -> str:
12
+ characters = string.ascii_letters + string.digits
13
+ return ''.join(secrets.choice(characters) for _ in range(length))
14
+
15
+
16
+ def run_issue(cert, plain, secure):
17
+ return DatManager._issue(cert, plain, secure)
18
+
19
+
20
+ def run_parse(cert, dat_str):
21
+ return DatManager._parse(cert, dat_str)
22
+
23
+
24
+ async def loops(multi_thread: bool, loop_size: int, certificates: list[DatCertificate], plain: str, secure: str):
25
+ mode_name = "Multi-Thread" if multi_thread else "Single-Thread"
26
+ print(f"\n--- {mode_name} ---")
27
+
28
+ for cert in certificates:
29
+ pre = f"{cert._signature_key.algorithm.name} {cert._crypto_key.algorithm.name}"
30
+
31
+ start = time.perf_counter()
32
+ last_dat = ""
33
+
34
+ if multi_thread:
35
+ # 파이썬의 GIL 우회를 위해 ProcessPoolExecutor 사용 (실제 멀티코어 활용)
36
+ with ThreadPoolExecutor() as executor:
37
+ futures = [executor.submit(run_issue, cert, plain, secure) for _ in range(loop_size)]
38
+ for fut in futures:
39
+ last_dat = fut.result()
40
+ else:
41
+ for _ in range(loop_size):
42
+ last_dat = DatManager._issue(cert, plain, secure)
43
+
44
+ duration_ms = (time.perf_counter() - start) * 1000
45
+ print(f"{pre} Issue * {loop_size} : {duration_ms:.2f}ms")
46
+
47
+ # 2. Parse Benchmark
48
+ start = time.perf_counter()
49
+ last_payload = None
50
+
51
+ if multi_thread:
52
+ with ThreadPoolExecutor() as executor:
53
+ futures = [executor.submit(run_parse, cert, last_dat) for _ in range(loop_size)]
54
+ for fut in futures:
55
+ last_payload = fut.result()
56
+ else:
57
+ for _ in range(loop_size):
58
+ last_payload = DatManager._parse(cert, last_dat)
59
+
60
+ duration_ms = (time.perf_counter() - start) * 1000
61
+ print(f"{pre} Parse * {loop_size} : {duration_ms:.2f}ms")
62
+
63
+ # 검증
64
+ assert last_payload.plain == plain
65
+ assert last_payload.secure == secure
66
+
67
+
68
+ async def benchmark(loop_size: int):
69
+ plain = generate_base62(100)
70
+ secure = generate_base62(100)
71
+
72
+ print("Performance Test (Plain, Secure)")
73
+ print(f"Plain: {plain}")
74
+ print(f"Secure: {secure}")
75
+
76
+ certificates = []
77
+ now = int(time.time())
78
+
79
+ for sa in DatSignatureAlgorithm:
80
+ for ca in DatCryptoAlgorithm:
81
+ certificates.append(DatCertificate(0, DatSignatureKey.generate(sa), DatCryptoKey.generate(ca), now - 10, now + 600, 60))
82
+
83
+ await loops(True, loop_size, certificates, plain, secure)
84
+ await loops(False, loop_size, certificates, plain, secure)
85
+
86
+
87
+ class TestBench(unittest.TestCase):
88
+ def test(self):
89
+ loop_size = 100
90
+ asyncio.run(benchmark(loop_size))
91
+
92
+
93
+ if __name__ == "__main__":
94
+ unittest.main()
@@ -0,0 +1,74 @@
1
+ import secrets
2
+ import string
3
+ import time
4
+ import unittest
5
+
6
+ from saro_dat import DatSignatureAlgorithm, DatCryptoAlgorithm, DatCertificate, DatSignatureKey, DatCryptoKey, \
7
+ DatSignatureKeyOutOption, DatManager
8
+
9
+
10
+ def generate_base62(length: int) -> str:
11
+ characters = string.ascii_letters + string.digits
12
+ return ''.join(secrets.choice(characters) for _ in range(length))
13
+
14
+ def generate_certificate(cid: int, sa: DatSignatureAlgorithm, ca: DatCryptoAlgorithm) -> DatCertificate:
15
+ now = int(time.time())
16
+ return DatCertificate(cid, DatSignatureKey.generate(sa), DatCryptoKey.generate(ca), now - 10, now + 100, 1800)
17
+
18
+ def cert_test(test: TestCertificate, fail_cert: DatCertificate, cid: int, sa: DatSignatureAlgorithm, ca: DatCryptoAlgorithm):
19
+ tag = f"CERT {sa.value} {ca.value}"
20
+ original_plain = generate_base62(100)
21
+ original_secure = generate_base62(100)
22
+
23
+ new_cert = generate_certificate(cid, sa, ca)
24
+ export_full_cert: str = new_cert.exports(DatSignatureKeyOutOption.FULL)
25
+ export_signing_cert = new_cert.exports(DatSignatureKeyOutOption.SIGNING)
26
+ export_verifying_cert = new_cert.exports(DatSignatureKeyOutOption.VERIFYING)
27
+
28
+ reimport_full_cert = DatCertificate.imports(export_full_cert)
29
+ reimport_signing_cert = DatCertificate.imports(export_signing_cert)
30
+ reimport_verifying_cert = DatCertificate.imports(export_verifying_cert)
31
+
32
+ print(f"{tag} Generated-Imported cert: {export_full_cert}")
33
+
34
+ dat_1 = DatManager._issue(new_cert, original_plain, original_secure)
35
+ dat_2 = DatManager._issue(reimport_full_cert, original_plain, original_secure)
36
+ dat_3 = DatManager._issue(reimport_signing_cert, original_plain, original_secure)
37
+ dat_empty = DatManager._issue(new_cert, "", "")
38
+
39
+ print(f"{tag} Issue DAT: {dat_1}")
40
+ print(f"{tag} Issue DAT: {dat_2}")
41
+ print(f"{tag} Issue DAT: {dat_3}")
42
+
43
+ payload_1 = DatManager._parse(reimport_verifying_cert, dat_1)
44
+ payload_2 = DatManager._parse(reimport_signing_cert, dat_2)
45
+ payload_3 = DatManager._parse(reimport_full_cert, dat_3)
46
+ payload_empty = DatManager._parse(reimport_verifying_cert, dat_empty)
47
+
48
+ assert payload_1.plain == original_plain
49
+ assert payload_2.plain == original_plain
50
+ assert payload_3.plain == original_plain
51
+ assert payload_empty.plain == ""
52
+ assert payload_1.secure == original_secure
53
+ assert payload_2.secure == original_secure
54
+ assert payload_3.secure == original_secure
55
+ assert payload_empty.secure == ""
56
+ print(f"{tag} Verify DAT")
57
+
58
+ with test.assertRaises(RuntimeError):
59
+ DatManager._parse(fail_cert, dat_1)
60
+
61
+
62
+ class TestCertificate(unittest.TestCase):
63
+
64
+
65
+ def test(self):
66
+ fail_cert = generate_certificate(3424342, DatSignatureAlgorithm.P256, DatCryptoAlgorithm.AES128GCMN)
67
+ for sa in DatSignatureAlgorithm:
68
+ for ca in DatCryptoAlgorithm:
69
+ for i in range(30):
70
+ cert_test(self, fail_cert, i, sa, ca)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ unittest.main()
@@ -0,0 +1,48 @@
1
+ import secrets
2
+ import string
3
+ import unittest
4
+
5
+ from saro_dat import util
6
+ from saro_dat.crypto import DatCryptoKey, DatCryptoAlgorithm
7
+
8
+
9
+ def generate_base62(length: int) -> str:
10
+ characters = string.ascii_letters + string.digits
11
+ return ''.join(secrets.choice(characters) for _ in range(length))
12
+
13
+ def algorithm_test(algorithm: DatCryptoAlgorithm):
14
+ tag = algorithm.value
15
+ gen_key = DatCryptoKey.generate(algorithm)
16
+ out_key_bytes = gen_key.exports()
17
+ out_key_base64 = util.encode_base64_url_str(out_key_bytes)
18
+ copy_key = DatCryptoKey.imports(algorithm, util.decode_base64_url(out_key_base64))
19
+ print(f"{tag} Generated-Imported key [{len(out_key_bytes)}] {out_key_base64}")
20
+
21
+ original_text = ">!#2 유니코드" + generate_base62(80)
22
+ encrypted = util.encode_base64_url_str(gen_key.encrypt(original_text))
23
+ print(f"{tag} Encrypted: {encrypted}")
24
+
25
+ decrypted = copy_key.decrypt(encrypted).decode('utf-8')
26
+ print(f"{tag} Decrypted: {decrypted}")
27
+
28
+ # empty
29
+ original_text = ""
30
+ encrypted = util.encode_base64_url_str(gen_key.encrypt(original_text))
31
+ print(f"{tag} Encrypted: {encrypted}")
32
+
33
+ decrypted = copy_key.decrypt(encrypted).decode('utf-8')
34
+ print(f"{tag} Decrypted: {decrypted}")
35
+
36
+ assert(original_text == decrypted)
37
+
38
+
39
+ class TestDatCrypto(unittest.TestCase):
40
+
41
+ def test(self):
42
+ for algorithm in DatCryptoAlgorithm:
43
+ for i in range(30):
44
+ algorithm_test(algorithm)
45
+
46
+
47
+ if __name__ == "__main__":
48
+ unittest.main()
@@ -0,0 +1,66 @@
1
+ import time
2
+ import unittest
3
+
4
+ from saro_dat import DatManager, DatCertificate, DatSignatureKey, DatSignatureAlgorithm, DatCryptoKey, DatCryptoAlgorithm
5
+
6
+
7
+ class TestDatManager(unittest.TestCase):
8
+
9
+
10
+ def test_issue_and_parse(self):
11
+ dat_manager = DatManager()
12
+
13
+ # create certificate
14
+ now = int(time.time())
15
+ cert = DatCertificate(0, DatSignatureKey.generate(DatSignatureAlgorithm.P256), DatCryptoKey.generate(DatCryptoAlgorithm.AES128GCMN), now - 10, now + 10, 1800)
16
+
17
+ # import certificate
18
+ dat_manager.import_certificates([cert])
19
+
20
+ example_plain = "plain text = 평문"
21
+ example_secure = "secure = 암호문"
22
+
23
+ dat = dat_manager.issue(example_plain, example_secure)
24
+ payload = dat_manager.parse(dat)
25
+
26
+ assert payload.plain == example_plain
27
+ assert payload.secure == example_secure
28
+ print(f"PARSE DAT: {dat}")
29
+ print(f"plain: {payload.plain}")
30
+ print(f"secure: {payload.secure}")
31
+
32
+
33
+
34
+ def test_hard(self):
35
+ example_certs = """0.P256.JH0wsVa7I1P2nyMKNi-REWeIyJ_ja30c8N7JmQpE2Ns.AES128GCMN.29dmalXRXXQT-YNV95HFvg.1.17786821880.1800
36
+ 1.P256.Gh9fb16c8XYdzKobpUwytD446_cMy3gWyncvViqeIZo.AES128GCMN.zX8sV8zO73-lDSfT-x8I1Q.1.17786821880.1800
37
+ 2.P256.fsufFa3ByNffCTG0fSQhEZqGF2z8pWxC3aLbZR6F0f8.AES128GCMN.e4h8InqvA5VvE8OtpjxRMQ.1.17786821880.1800
38
+ 3.P256.koYkmCZYmvLISg4-tPbksoZ71ay8hEVEWB6aBbexliA.AES128GCMN.7nQ7R5yuFQ3S5Vb07Brawg.1.17786821880.1800
39
+ 4.P256.Cda0iq0axjTeKhpnqfRFapk4innUjwbd2HEXAzYipAM.AES128GCMN.ttE0ydNaePVMVsGhRmyq8Q.1.17786821880.1800
40
+ 5.P256.Tf4ZFbD9ZmY_9kyQRlSXKpgy18uyMGAImh48_z4e9XE.AES128GCMN.ifsTUtiUzZY6-CUielQWkw.1.17786821880.1800
41
+ 6.P256.fRz08K1e3mtebqWwHADKl3G4nZR1V2v6knLU6-r7jG4.AES128GCMN.cEPBfYQVKKSd_adfRkrTJQ.1.17786821880.1800
42
+ 7.P256.HPDwUUi44EWESaenixl1uOkafSc5Yshi9Q5ht5JV-7o.AES128GCMN._WwtaqkFgvqkxv8T5-WHsg.1.17786821880.1800
43
+ 8.P256.2TSn2rl4Ucc7_tcgLFoba81zMGpdN4zInbgjD_IA0dM.AES128GCMN.zgj82h8PpS0nGxlhJBbUQQ.1.17786821880.1800
44
+ 9.P256.uKfLtmwS9-4o5_H5fflSoaiCULwHRQXdHan2JSK6-o0.AES128GCMN.w0WXojP3LeCCl68C_KTBMQ.1.1708680188.1800"""
45
+
46
+ example_plain = "plain text = 평문"
47
+ example_secure = "secure = 암호문"
48
+ example_dat = "19565503600.8.cGxhaW4gdGV4dCA9IO2PieusuA.1YswrtsIMWMz9dJjR2M1sRAZWxzcfTu7CQo-GXaCDMh6nF6g72HQn_F9HnK-kg.kgY9pxi5nITg-8Wk_vRFaUDDldyoSrrOP0-BBxVrwbP0-ZXlpuCmKQ1YiBZd4qv3KrO9vvwZdghpsiYHITbybA"
49
+
50
+
51
+ manager = DatManager()
52
+
53
+ manager.imports(example_certs, True)
54
+
55
+ payload = manager.parse(example_dat)
56
+
57
+ assert payload.plain == example_plain
58
+ assert payload.secure == example_secure
59
+ print(f"PARSE DAT: {example_dat}")
60
+ print(f"plain: {payload.plain}")
61
+ print(f"secure: {payload.secure}")
62
+
63
+ if __name__ == "__main__":
64
+ unittest.main()
65
+
66
+
@@ -0,0 +1,75 @@
1
+ import secrets
2
+ import string
3
+ import time
4
+ import unittest
5
+
6
+ from saro_dat import DatSignatureAlgorithm, DatCryptoAlgorithm, DatCertificate, DatSignatureKey, DatCryptoKey, \
7
+ DatSignatureKeyOutOption, DatManager
8
+
9
+
10
+ def generate_base62(length: int) -> str:
11
+ characters = string.ascii_letters + string.digits
12
+ return ''.join(secrets.choice(characters) for _ in range(length))
13
+
14
+ def generate_certificate(cid: int, sa: DatSignatureAlgorithm, ca: DatCryptoAlgorithm) -> DatCertificate:
15
+ now = int(time.time())
16
+ return DatCertificate(cid, DatSignatureKey.generate(sa), DatCryptoKey.generate(ca), now - 10, now + 100, 1800)
17
+
18
+ class TestDatManager(unittest.TestCase):
19
+
20
+ def test(self):
21
+ original_plain = generate_base62(100)
22
+ original_secure = generate_base62(100)
23
+ dat_list: list[str] = []
24
+ manager = DatManager()
25
+
26
+ i = 0
27
+ for sa in DatSignatureAlgorithm:
28
+ for ca in DatCryptoAlgorithm:
29
+ for _ in range(30):
30
+ i += 1
31
+ cert = generate_certificate(i, sa, ca)
32
+ manager.import_certificates([cert], False)
33
+ dat = DatManager._issue(cert, original_plain, original_secure)
34
+ dat_list.append(dat)
35
+
36
+ print(f"DAT Manager Import : {len(dat_list)} Certificates")
37
+
38
+ manager_full_export = manager.exports(DatSignatureKeyOutOption.FULL)
39
+ manager_signing_export = manager.exports(DatSignatureKeyOutOption.SIGNING)
40
+ manager_verifying_export = manager.exports(DatSignatureKeyOutOption.VERIFYING)
41
+
42
+
43
+ reimport_full_manager = DatManager()
44
+ reimport_full_manager.imports(manager_full_export)
45
+ reimport_signing_manager = DatManager()
46
+ reimport_signing_manager.imports(manager_signing_export)
47
+ reimport_verifying_manager = DatManager()
48
+ reimport_verifying_manager.imports(manager_verifying_export)
49
+
50
+ dat_list.append(reimport_full_manager.issue(original_plain, original_secure))
51
+ dat_list.append(reimport_signing_manager.issue(original_plain, original_secure))
52
+
53
+ with self.assertRaises(RuntimeError):
54
+ dat_list.append(reimport_verifying_manager.issue(original_plain, original_secure))
55
+
56
+ print(f"DAT Manager Re-Import")
57
+ print(f"ISSUE {len(dat_list)} DAT")
58
+
59
+ for dat in dat_list:
60
+ dat1 = manager.parse(dat)
61
+ dat2 = reimport_verifying_manager.parse(dat)
62
+ dat3 = reimport_signing_manager.parse(dat)
63
+ dat4 = reimport_full_manager.parse(dat)
64
+ assert dat1.plain == original_plain
65
+ assert dat1.secure == original_secure
66
+ assert dat2.plain == original_plain
67
+ assert dat2.secure == original_secure
68
+ assert dat3.plain == original_plain
69
+ assert dat3.secure == original_secure
70
+ assert dat4.plain == original_plain
71
+ assert dat4.secure == original_secure
72
+ print(f"PARSE DAT: {dat}")
73
+
74
+ if __name__ == "__main__":
75
+ unittest.main()
@@ -0,0 +1,48 @@
1
+ import secrets
2
+ import string
3
+ import unittest
4
+
5
+ from saro_dat import DatSignatureAlgorithm, DatSignatureKey, DatSignatureKeyOutOption
6
+
7
+
8
+ def generate_base62(length: int) -> str:
9
+ characters = string.ascii_letters + string.digits
10
+ return ''.join(secrets.choice(characters) for _ in range(length))
11
+
12
+ def algorithm_test(algorithm: DatSignatureAlgorithm):
13
+ tag = algorithm.value
14
+ gen_key = DatSignatureKey.generate(algorithm)
15
+
16
+ out_key_full = gen_key.exports(DatSignatureKeyOutOption.FULL)
17
+ out_key_signing = gen_key.exports(DatSignatureKeyOutOption.SIGNING)
18
+ out_key_verifying = gen_key.exports(DatSignatureKeyOutOption.VERIFYING)
19
+
20
+ copy_out_key_full = DatSignatureKey.imports(algorithm, out_key_full)
21
+ copy_out_key_signing = DatSignatureKey.imports(algorithm, out_key_signing)
22
+ copy_out_key_verifying = DatSignatureKey.imports(algorithm, out_key_verifying)
23
+
24
+ print(f"{tag} Generated-Imported key: {out_key_full}")
25
+
26
+ original_text = ">!#2 유니코드" + generate_base62(80)
27
+ sign1 = gen_key.sign(original_text)
28
+ sign2 = copy_out_key_full.sign(original_text)
29
+ sign3 = copy_out_key_signing.sign(original_text)
30
+
31
+ assert gen_key.verify(original_text, sign3)
32
+ assert copy_out_key_full.verify(original_text, sign2)
33
+ assert copy_out_key_signing.verify(original_text, sign1)
34
+ assert copy_out_key_verifying.verify(original_text, sign1)
35
+ assert not copy_out_key_verifying.verify(b"", sign1)
36
+
37
+ print(f"{tag} Signing-Verify key")
38
+
39
+
40
+ class TestSignature(unittest.TestCase):
41
+
42
+ def test(self):
43
+ for algorithm in DatSignatureAlgorithm:
44
+ for i in range(30):
45
+ algorithm_test(algorithm)
46
+
47
+ if __name__ == "__main__":
48
+ unittest.main()
@@ -0,0 +1,26 @@
1
+ import unittest
2
+
3
+ from saro_dat.util import encode_base64_url_str, decode_base64_url_str
4
+
5
+
6
+ class TestBase64(unittest.TestCase):
7
+ def test_base64(self):
8
+ text = "$$><'2 ABC 유니코드"
9
+ b64 = "JCQ-PCcyICAgIEFCQyAg7Jyg64uI7L2U65Oc"
10
+
11
+ b64_1 = encode_base64_url_str(text)
12
+ self.assertEqual(b64, b64_1)
13
+ print(b64_1)
14
+
15
+ de_b64_1 = decode_base64_url_str(b64_1)
16
+ self.assertEqual(text, de_b64_1)
17
+ print(de_b64_1)
18
+
19
+ print("Test passed successfully")
20
+
21
+
22
+
23
+
24
+ if __name__ == "__main__":
25
+ unittest.main()
26
+