rwn64 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.
- rwn64-1.0.0/PKG-INFO +63 -0
- rwn64-1.0.0/README.md +53 -0
- rwn64-1.0.0/pyproject.toml +20 -0
- rwn64-1.0.0/rwn64/__init__.py +25 -0
- rwn64-1.0.0/rwn64/_crypto.py +90 -0
- rwn64-1.0.0/rwn64/_util.py +27 -0
- rwn64-1.0.0/rwn64.egg-info/PKG-INFO +63 -0
- rwn64-1.0.0/rwn64.egg-info/SOURCES.txt +10 -0
- rwn64-1.0.0/rwn64.egg-info/dependency_links.txt +1 -0
- rwn64-1.0.0/rwn64.egg-info/top_level.txt +1 -0
- rwn64-1.0.0/setup.cfg +4 -0
- rwn64-1.0.0/tests/test_rwn64.py +91 -0
rwn64-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rwn64
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AES-256-GCM token encryption — interoperable with Node.js rwn64
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/hen76d/rwn64
|
|
7
|
+
Keywords: encryption,aes-256-gcm,crypto,token
|
|
8
|
+
Requires-Python: >=3.6
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# rwn64
|
|
12
|
+
|
|
13
|
+
AES-256-GCM token encryption for Python. Zero dependencies. Tokens are fully interoperable with the [Node.js rwn64 package](https://www.npmjs.com/package/rwn64).
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install rwn64
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import rwn64
|
|
25
|
+
|
|
26
|
+
token = rwn64.encrypt("my secret", "mypassword")
|
|
27
|
+
text = rwn64.decrypt(token, "mypassword")
|
|
28
|
+
ok = rwn64.verify(token, "mypassword")
|
|
29
|
+
fp = rwn64.fingerprint("hello", "secret")
|
|
30
|
+
pw = rwn64.generate(32)
|
|
31
|
+
meta = rwn64.inspect(token)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Expiry
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="1h")
|
|
38
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="30m")
|
|
39
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="7d")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Node.js interoperability
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# decrypt a token generated by Node.js rwn64 (v1 tokens)
|
|
46
|
+
text = rwn64.decrypt("rwn64:v1:...", "mypassword")
|
|
47
|
+
|
|
48
|
+
# generate in Python, decrypt in Node.js
|
|
49
|
+
token = rwn64.encrypt("hello", "mypassword")
|
|
50
|
+
# → rwn64 denc 'token' -sw mypassword
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Security
|
|
54
|
+
|
|
55
|
+
- AES-256-GCM authenticated encryption
|
|
56
|
+
- scrypt key derivation (N=16384, r=8, p=1)
|
|
57
|
+
- Random salt + nonce per token
|
|
58
|
+
- Auth tag detects tampering before decryption
|
|
59
|
+
- Zero external dependencies
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
rwn64-1.0.0/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# rwn64
|
|
2
|
+
|
|
3
|
+
AES-256-GCM token encryption for Python. Zero dependencies. Tokens are fully interoperable with the [Node.js rwn64 package](https://www.npmjs.com/package/rwn64).
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install rwn64
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import rwn64
|
|
15
|
+
|
|
16
|
+
token = rwn64.encrypt("my secret", "mypassword")
|
|
17
|
+
text = rwn64.decrypt(token, "mypassword")
|
|
18
|
+
ok = rwn64.verify(token, "mypassword")
|
|
19
|
+
fp = rwn64.fingerprint("hello", "secret")
|
|
20
|
+
pw = rwn64.generate(32)
|
|
21
|
+
meta = rwn64.inspect(token)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Expiry
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="1h")
|
|
28
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="30m")
|
|
29
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="7d")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Node.js interoperability
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# decrypt a token generated by Node.js rwn64 (v1 tokens)
|
|
36
|
+
text = rwn64.decrypt("rwn64:v1:...", "mypassword")
|
|
37
|
+
|
|
38
|
+
# generate in Python, decrypt in Node.js
|
|
39
|
+
token = rwn64.encrypt("hello", "mypassword")
|
|
40
|
+
# → rwn64 denc 'token' -sw mypassword
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Security
|
|
44
|
+
|
|
45
|
+
- AES-256-GCM authenticated encryption
|
|
46
|
+
- scrypt key derivation (N=16384, r=8, p=1)
|
|
47
|
+
- Random salt + nonce per token
|
|
48
|
+
- Auth tag detects tampering before decryption
|
|
49
|
+
- Zero external dependencies
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68","wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rwn64"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "AES-256-GCM token encryption — interoperable with Node.js rwn64"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text="MIT"}
|
|
11
|
+
requires-python = ">=3.6"
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = ["encryption","aes-256-gcm","crypto","token"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/hen76d/rwn64"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["."]
|
|
20
|
+
include = ["rwn64*"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from ._crypto import encrypt as _enc,decrypt as _dec
|
|
3
|
+
from ._util import fingerprint as _fp,generate as _gen,parse_expiry,inspect as _ins
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
__all__ = ["encrypt","decrypt","verify","fingerprint","generate","inspect","parse_expiry"]
|
|
7
|
+
TOKEN_PREFIX = "rwn64:v1:"
|
|
8
|
+
|
|
9
|
+
def encrypt(text:str,password:str,*,expires:str|None=None,expires_ms:int|None=None)->str:
|
|
10
|
+
if not password: raise TypeError("password must be non-empty")
|
|
11
|
+
exp=parse_expiry(expires) if expires else expires_ms
|
|
12
|
+
return _enc(text,password,expires_ms=exp)
|
|
13
|
+
|
|
14
|
+
def decrypt(token:str,password:str)->str:
|
|
15
|
+
if not token: raise TypeError("token must be non-empty")
|
|
16
|
+
if not password: raise TypeError("password must be non-empty")
|
|
17
|
+
return _dec(token,password)[0]
|
|
18
|
+
|
|
19
|
+
def verify(token:str,password:str)->bool:
|
|
20
|
+
try: _dec(token,password); return True
|
|
21
|
+
except: return False
|
|
22
|
+
|
|
23
|
+
def fingerprint(text:str,secret:str)->str: return _fp(text,secret)
|
|
24
|
+
def generate(nbytes:int=24)->str: return _gen(nbytes)
|
|
25
|
+
def inspect(token:str)->dict: return _ins(token)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ctypes as _c, hashlib as _h, os as _o, struct as _s, time as _t, base64 as _b64
|
|
3
|
+
|
|
4
|
+
_lib = _c.CDLL("libcrypto.so.3")
|
|
5
|
+
_ci = _c.c_int
|
|
6
|
+
_cv = _c.c_void_p
|
|
7
|
+
_cc = _c.c_char_p
|
|
8
|
+
|
|
9
|
+
_lib.EVP_CIPHER_CTX_new.restype = _cv
|
|
10
|
+
_lib.EVP_aes_256_gcm.restype = _cv
|
|
11
|
+
_lib.EVP_EncryptInit_ex.argtypes = [_cv,_cv,_cv,_cc,_cc]; _lib.EVP_EncryptInit_ex.restype = _ci
|
|
12
|
+
_lib.EVP_DecryptInit_ex.argtypes = [_cv,_cv,_cv,_cc,_cc]; _lib.EVP_DecryptInit_ex.restype = _ci
|
|
13
|
+
_lib.EVP_CIPHER_CTX_ctrl.argtypes = [_cv,_ci,_ci,_cv]; _lib.EVP_CIPHER_CTX_ctrl.restype = _ci
|
|
14
|
+
_lib.EVP_EncryptUpdate.argtypes = [_cv,_cc,_c.POINTER(_ci),_cc,_ci]; _lib.EVP_EncryptUpdate.restype = _ci
|
|
15
|
+
_lib.EVP_DecryptUpdate.argtypes = [_cv,_cc,_c.POINTER(_ci),_cc,_ci]; _lib.EVP_DecryptUpdate.restype = _ci
|
|
16
|
+
_lib.EVP_EncryptFinal_ex.argtypes = [_cv,_cc,_c.POINTER(_ci)]; _lib.EVP_EncryptFinal_ex.restype = _ci
|
|
17
|
+
_lib.EVP_DecryptFinal_ex.argtypes = [_cv,_cc,_c.POINTER(_ci)]; _lib.EVP_DecryptFinal_ex.restype = _ci
|
|
18
|
+
_lib.EVP_CIPHER_CTX_free.argtypes = [_cv]
|
|
19
|
+
|
|
20
|
+
_GCM_SIVLEN = 0x9
|
|
21
|
+
_GCM_GTAG = 0x10
|
|
22
|
+
_GCM_STAG = 0x11
|
|
23
|
+
_SL,_NL,_TL,_KL = 16,12,16,32
|
|
24
|
+
_N,_R,_P = 16384,8,1
|
|
25
|
+
_PX1 = b"rwn64:v1:"
|
|
26
|
+
|
|
27
|
+
def _kdf(pw:bytes,salt:bytes)->bytes:
|
|
28
|
+
return _h.scrypt(pw,salt=salt,n=_N,r=_R,p=_P,dklen=_KL)
|
|
29
|
+
|
|
30
|
+
def _enc(key:bytes,nonce:bytes,pt:bytes)->tuple[bytes,bytes]:
|
|
31
|
+
x=_lib.EVP_CIPHER_CTX_new()
|
|
32
|
+
_lib.EVP_EncryptInit_ex(x,_lib.EVP_aes_256_gcm(),None,None,None)
|
|
33
|
+
_lib.EVP_CIPHER_CTX_ctrl(x,_GCM_SIVLEN,len(nonce),None)
|
|
34
|
+
_lib.EVP_EncryptInit_ex(x,None,None,key,nonce)
|
|
35
|
+
buf=_c.create_string_buffer(len(pt)+16); l=_ci(0)
|
|
36
|
+
_lib.EVP_EncryptUpdate(x,buf,_c.byref(l),pt,len(pt))
|
|
37
|
+
ct=buf.raw[:l.value]
|
|
38
|
+
_lib.EVP_EncryptFinal_ex(x,buf,_c.byref(l))
|
|
39
|
+
tag=_c.create_string_buffer(16)
|
|
40
|
+
_lib.EVP_CIPHER_CTX_ctrl(x,_GCM_GTAG,16,tag)
|
|
41
|
+
_lib.EVP_CIPHER_CTX_free(x)
|
|
42
|
+
return ct,tag.raw
|
|
43
|
+
|
|
44
|
+
def _dec(key:bytes,nonce:bytes,ct:bytes,tag:bytes)->bytes:
|
|
45
|
+
x=_lib.EVP_CIPHER_CTX_new()
|
|
46
|
+
_lib.EVP_DecryptInit_ex(x,_lib.EVP_aes_256_gcm(),None,None,None)
|
|
47
|
+
_lib.EVP_CIPHER_CTX_ctrl(x,_GCM_SIVLEN,len(nonce),None)
|
|
48
|
+
_lib.EVP_DecryptInit_ex(x,None,None,key,nonce)
|
|
49
|
+
buf=_c.create_string_buffer(len(ct)+16); l=_ci(0)
|
|
50
|
+
_lib.EVP_DecryptUpdate(x,buf,_c.byref(l),ct,len(ct))
|
|
51
|
+
pt=buf.raw[:l.value]
|
|
52
|
+
_lib.EVP_CIPHER_CTX_ctrl(x,_GCM_STAG,16,_c.c_char_p(tag))
|
|
53
|
+
fin=_c.create_string_buffer(16)
|
|
54
|
+
ok=_lib.EVP_DecryptFinal_ex(x,fin,_c.byref(l))
|
|
55
|
+
_lib.EVP_CIPHER_CTX_free(x)
|
|
56
|
+
if ok<=0: raise ValueError("wrong password or corrupted token")
|
|
57
|
+
return pt
|
|
58
|
+
|
|
59
|
+
def _b64e(b:bytes)->bytes: return _b64.urlsafe_b64encode(b).rstrip(b"=")
|
|
60
|
+
def _b64d(b:bytes)->bytes:
|
|
61
|
+
p=4-len(b)%4
|
|
62
|
+
return _b64.urlsafe_b64decode(b+(b"="*p if p!=4 else b""))
|
|
63
|
+
|
|
64
|
+
def _eexp(ms:int)->bytes:
|
|
65
|
+
return _s.pack(">II",ms>>32,ms&0xFFFFFFFF)
|
|
66
|
+
|
|
67
|
+
def _dexp(b:bytes)->int:
|
|
68
|
+
hi,lo=_s.unpack(">II",b); return(hi<<32)|lo
|
|
69
|
+
|
|
70
|
+
def encrypt(plaintext:str,password:str,*,expires_ms:int|None=None)->str:
|
|
71
|
+
salt=_o.urandom(_SL); nonce=_o.urandom(_NL)
|
|
72
|
+
key=_kdf(password.encode(),salt)
|
|
73
|
+
hx=expires_ms is not None
|
|
74
|
+
inner=(b"\x01"+_eexp(expires_ms) if hx else b"\x00")+plaintext.encode()
|
|
75
|
+
ct,tag=_enc(key,nonce,inner)
|
|
76
|
+
return(_PX1+_b64e(salt+nonce+tag+ct)).decode()
|
|
77
|
+
|
|
78
|
+
def decrypt(token:str,password:str)->tuple[str,int|None]:
|
|
79
|
+
tb=token.encode()
|
|
80
|
+
if not tb.startswith(_PX1): raise ValueError("invalid token format")
|
|
81
|
+
raw=_b64d(tb[len(_PX1):])
|
|
82
|
+
if len(raw)<_SL+_NL+_TL+2: raise ValueError("token is too short or malformed")
|
|
83
|
+
salt=raw[:_SL]; nonce=raw[_SL:_SL+_NL]; tag=raw[_SL+_NL:_SL+_NL+_TL]; ct=raw[_SL+_NL+_TL:]
|
|
84
|
+
key=_kdf(password.encode(),salt)
|
|
85
|
+
inner=_dec(key,nonce,ct,tag)
|
|
86
|
+
if inner[0]==1:
|
|
87
|
+
exp=_dexp(inner[1:9])
|
|
88
|
+
if int(_t.time()*1000)>exp: raise ValueError(f"token expired at {exp}")
|
|
89
|
+
return inner[9:].decode(),exp
|
|
90
|
+
return inner[1:].decode(),None
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import base64 as _b,hashlib as _h,hmac as _m,os as _o,re as _r,time as _t
|
|
3
|
+
|
|
4
|
+
def _b64e(b:bytes)->bytes: return _b.urlsafe_b64encode(b).rstrip(b"=")
|
|
5
|
+
def _b64d(b:bytes)->bytes:
|
|
6
|
+
p=4-len(b)%4; return _b.urlsafe_b64decode(b+(b"="*p if p!=4 else b""))
|
|
7
|
+
|
|
8
|
+
def fingerprint(text:str,secret:str)->str:
|
|
9
|
+
return _m.new(secret.encode(),text.encode(),_h.sha256).hexdigest()[:16]
|
|
10
|
+
|
|
11
|
+
def generate(nbytes:int=24)->str:
|
|
12
|
+
return _b64e(_o.urandom(nbytes)).decode()
|
|
13
|
+
|
|
14
|
+
def parse_expiry(raw:str)->int:
|
|
15
|
+
m=_r.fullmatch(r"(\d+)(s|m|h|d)",raw.strip())
|
|
16
|
+
if not m: raise ValueError(f'invalid expiry: "{raw}" — use 30s 5m 2h 7d')
|
|
17
|
+
return int(_t.time()*1000)+int(m.group(1))*{"s":1000,"m":60000,"h":3600000,"d":86400000}[m.group(2)]
|
|
18
|
+
|
|
19
|
+
def inspect(token:str)->dict:
|
|
20
|
+
from ._crypto import _b64d,_SL,_NL,_PX1
|
|
21
|
+
tb=token.encode()
|
|
22
|
+
if tb.startswith(_PX1): pfx,ver,algo=_PX1,"v1","AES-256-GCM"
|
|
23
|
+
elif tb.startswith(b"rwn64:v2:"): pfx,ver,algo=b"rwn64:v2:","v2","ChaCha20-Poly1305"
|
|
24
|
+
else: raise ValueError("invalid token format")
|
|
25
|
+
raw=_b64d(tb[len(pfx):])
|
|
26
|
+
return {"prefix":pfx.decode(),"version":ver,"algo":algo,"kdf":"scrypt (N=16384,r=8,p=1)",
|
|
27
|
+
"salt_hex":raw[:16].hex(),"nonce_hex":raw[16:28].hex(),"payload_bytes":len(raw)}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rwn64
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AES-256-GCM token encryption — interoperable with Node.js rwn64
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/hen76d/rwn64
|
|
7
|
+
Keywords: encryption,aes-256-gcm,crypto,token
|
|
8
|
+
Requires-Python: >=3.6
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# rwn64
|
|
12
|
+
|
|
13
|
+
AES-256-GCM token encryption for Python. Zero dependencies. Tokens are fully interoperable with the [Node.js rwn64 package](https://www.npmjs.com/package/rwn64).
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install rwn64
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import rwn64
|
|
25
|
+
|
|
26
|
+
token = rwn64.encrypt("my secret", "mypassword")
|
|
27
|
+
text = rwn64.decrypt(token, "mypassword")
|
|
28
|
+
ok = rwn64.verify(token, "mypassword")
|
|
29
|
+
fp = rwn64.fingerprint("hello", "secret")
|
|
30
|
+
pw = rwn64.generate(32)
|
|
31
|
+
meta = rwn64.inspect(token)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Expiry
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="1h")
|
|
38
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="30m")
|
|
39
|
+
token = rwn64.encrypt("temporary", "mypassword", expires="7d")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Node.js interoperability
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# decrypt a token generated by Node.js rwn64 (v1 tokens)
|
|
46
|
+
text = rwn64.decrypt("rwn64:v1:...", "mypassword")
|
|
47
|
+
|
|
48
|
+
# generate in Python, decrypt in Node.js
|
|
49
|
+
token = rwn64.encrypt("hello", "mypassword")
|
|
50
|
+
# → rwn64 denc 'token' -sw mypassword
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Security
|
|
54
|
+
|
|
55
|
+
- AES-256-GCM authenticated encryption
|
|
56
|
+
- scrypt key derivation (N=16384, r=8, p=1)
|
|
57
|
+
- Random salt + nonce per token
|
|
58
|
+
- Auth tag detects tampering before decryption
|
|
59
|
+
- Zero external dependencies
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rwn64
|
rwn64-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import sys,os,time
|
|
2
|
+
sys.path.insert(0,os.path.join(os.path.dirname(__file__),".."))
|
|
3
|
+
import rwn64
|
|
4
|
+
|
|
5
|
+
_G="\x1b[32m✔\x1b[0m"; _R="\x1b[31m✘\x1b[0m"
|
|
6
|
+
_H="\x1b[1m\x1b[38;5;252m"; _D="\x1b[38;5;245m"; _E="\x1b[0m"
|
|
7
|
+
_p=_f=0
|
|
8
|
+
|
|
9
|
+
def _head(t): print(f"\n{_H}{t}{_E}")
|
|
10
|
+
def _assert(c,m="assertion failed"):
|
|
11
|
+
if not c: raise AssertionError(m)
|
|
12
|
+
|
|
13
|
+
def test(label,fn):
|
|
14
|
+
global _p,_f
|
|
15
|
+
try: fn(); print(f" {_G} {label}"); _p+=1
|
|
16
|
+
except Exception as e: print(f" {_R} {label}\n {_D}{e}{_E}"); _f+=1
|
|
17
|
+
|
|
18
|
+
_head("encrypt / decrypt")
|
|
19
|
+
test("returns rwn64:v1: token", lambda:_assert(rwn64.encrypt("hi","pw").startswith("rwn64:v1:")))
|
|
20
|
+
test("roundtrip plaintext", lambda:_assert(rwn64.decrypt(rwn64.encrypt("secret","pw"),"pw")=="secret"))
|
|
21
|
+
test("unique tokens per call", lambda:_assert(rwn64.encrypt("x","pw")!=rwn64.encrypt("x","pw")))
|
|
22
|
+
test("unicode roundtrip", lambda:_assert(rwn64.decrypt(rwn64.encrypt("日本語 ñ @#$","pw"),"pw")=="日本語 ñ @#$"))
|
|
23
|
+
test("10 KB roundtrip", lambda:_assert(rwn64.decrypt(rwn64.encrypt("x"*10240,"pw"),"pw")=="x"*10240))
|
|
24
|
+
|
|
25
|
+
_head("wrong password / tampered")
|
|
26
|
+
def _t6():
|
|
27
|
+
tok=rwn64.encrypt("secret","correct")
|
|
28
|
+
try: rwn64.decrypt(tok,"wrong"); raise AssertionError("should raise")
|
|
29
|
+
except ValueError: pass
|
|
30
|
+
test("wrong password raises",_t6)
|
|
31
|
+
|
|
32
|
+
def _t7():
|
|
33
|
+
tok=rwn64.encrypt("secret","pw")
|
|
34
|
+
try: rwn64.decrypt(tok[:-6]+"AAAAAA","pw"); raise AssertionError("should raise")
|
|
35
|
+
except Exception: pass
|
|
36
|
+
test("tampered token raises",_t7)
|
|
37
|
+
|
|
38
|
+
def _t8():
|
|
39
|
+
try: rwn64.decrypt("notatoken","pw"); raise AssertionError("should raise")
|
|
40
|
+
except Exception: pass
|
|
41
|
+
test("invalid format raises",_t8)
|
|
42
|
+
|
|
43
|
+
_head("verify")
|
|
44
|
+
test("valid token → True", lambda:_assert(rwn64.verify(rwn64.encrypt("x","pw"),"pw") is True))
|
|
45
|
+
test("wrong password → False",lambda:_assert(rwn64.verify(rwn64.encrypt("x","pw"),"bad") is False))
|
|
46
|
+
test("garbage → False", lambda:_assert(rwn64.verify("garbage","pw") is False))
|
|
47
|
+
|
|
48
|
+
_head("expiry")
|
|
49
|
+
test("no expiry decrypts", lambda:_assert(rwn64.decrypt(rwn64.encrypt("x","pw"),"pw")=="x"))
|
|
50
|
+
test("future expiry decrypts",lambda:_assert(rwn64.decrypt(rwn64.encrypt("x","pw",expires_ms=int(time.time()*1000)+60000),"pw")=="x"))
|
|
51
|
+
test("expires= string (1h)", lambda:_assert(rwn64.decrypt(rwn64.encrypt("x","pw",expires="1h"),"pw")=="x"))
|
|
52
|
+
|
|
53
|
+
def _t13():
|
|
54
|
+
tok=rwn64.encrypt("x","pw",expires_ms=int(time.time()*1000)+100)
|
|
55
|
+
time.sleep(0.2)
|
|
56
|
+
try: rwn64.decrypt(tok,"pw"); raise AssertionError("should raise")
|
|
57
|
+
except ValueError as e: _assert("expired" in str(e).lower())
|
|
58
|
+
test("expired token raises",_t13)
|
|
59
|
+
|
|
60
|
+
_head("fingerprint")
|
|
61
|
+
test("deterministic", lambda:_assert(rwn64.fingerprint("hi","s")==rwn64.fingerprint("hi","s")))
|
|
62
|
+
test("different input differs",lambda:_assert(rwn64.fingerprint("a","s")!=rwn64.fingerprint("b","s")))
|
|
63
|
+
test("16 hex chars", lambda:_assert(len(rwn64.fingerprint("x","s"))==16))
|
|
64
|
+
|
|
65
|
+
_head("generate")
|
|
66
|
+
test("non-empty string", lambda:_assert(isinstance(rwn64.generate(),str) and len(rwn64.generate())>0))
|
|
67
|
+
test("unique values", lambda:_assert(rwn64.generate()!=rwn64.generate()))
|
|
68
|
+
|
|
69
|
+
_head("inspect")
|
|
70
|
+
def _t_ins():
|
|
71
|
+
m=rwn64.inspect(rwn64.encrypt("x","pw"))
|
|
72
|
+
_assert(m["version"]=="v1")
|
|
73
|
+
_assert(m["algo"]=="AES-256-GCM")
|
|
74
|
+
_assert(m["prefix"]=="rwn64:v1:")
|
|
75
|
+
test("inspect metadata",_t_ins)
|
|
76
|
+
|
|
77
|
+
_head("node interoperability")
|
|
78
|
+
_TOK ="rwn64:v1:IaQVjP7tITZRERd-MWZBpxGMdo432gsidiGNlK9vZFSt-Vqhs2ioZt4dFykhmciisVa9a07VY0M"
|
|
79
|
+
_PASS="minhasenha"
|
|
80
|
+
_TXT ="old v1 text"
|
|
81
|
+
test("decrypt Node.js token", lambda:_assert(rwn64.decrypt(_TOK,_PASS)==_TXT))
|
|
82
|
+
test("Python token is valid", lambda:_assert(rwn64.decrypt(rwn64.encrypt(_TXT,_PASS),_PASS)==_TXT))
|
|
83
|
+
|
|
84
|
+
total=_p+_f
|
|
85
|
+
print(f"\n{_D}{'─'*48}{_E}\n")
|
|
86
|
+
print(f" total {total}")
|
|
87
|
+
print(f" \x1b[32mpassed {_p}\x1b[0m")
|
|
88
|
+
if _f: print(f" \x1b[31mfailed {_f}\x1b[0m")
|
|
89
|
+
print()
|
|
90
|
+
if not _f: print(f" \x1b[32m\x1b[1mall tests passed\x1b[0m\n")
|
|
91
|
+
else: print(f" \x1b[31m\x1b[1m{_f} test(s) failed\x1b[0m\n"); sys.exit(1)
|