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 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,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ rwn64/__init__.py
4
+ rwn64/_crypto.py
5
+ rwn64/_util.py
6
+ rwn64.egg-info/PKG-INFO
7
+ rwn64.egg-info/SOURCES.txt
8
+ rwn64.egg-info/dependency_links.txt
9
+ rwn64.egg-info/top_level.txt
10
+ tests/test_rwn64.py
@@ -0,0 +1 @@
1
+ rwn64
rwn64-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)