ciphera 0.1.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.
@@ -0,0 +1,170 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Ruff (linter)
153
+ .ruff_cache/
154
+
155
+ # Cython debug symbols
156
+ cython_debug/
157
+
158
+ # PyCharm
159
+ .idea/
160
+ !.idea/runConfigurations
161
+
162
+ # macOS
163
+ .DS_Store
164
+
165
+ # Received approval test files
166
+ *.received.*
167
+
168
+ # NPM
169
+ node_modules
170
+
ciphera-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aditya Pandey
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.
ciphera-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: ciphera
3
+ Version: 0.1.0
4
+ Summary: Zero-Knowledge KYC for Algorand — verify KYC status on-chain without exposing personal data
5
+ Project-URL: Homepage, https://github.com/Aditya060806/Ciphera
6
+ Project-URL: Repository, https://github.com/Aditya060806/Ciphera
7
+ Project-URL: Issues, https://github.com/Aditya060806/Ciphera/issues
8
+ Project-URL: Documentation, https://github.com/Aditya060806/Ciphera#readme
9
+ Author-email: Aditya Pandey <adityapandey060806@gmail.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Aditya Pandey
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: aadhaar,algorand,blockchain,groth16,kyc,privacy,snarkjs,zero-knowledge,zk-proof
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Security :: Cryptography
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Requires-Python: >=3.9
44
+ Requires-Dist: py-algorand-sdk>=2.6.0
45
+ Provides-Extra: algorand
46
+ Requires-Dist: py-algorand-sdk>=2.6.0; extra == 'algorand'
47
+ Provides-Extra: dev
48
+ Requires-Dist: build>=1.0; extra == 'dev'
49
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
50
+ Requires-Dist: pytest>=7.0; extra == 'dev'
51
+ Requires-Dist: twine>=5.0; extra == 'dev'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # ciphera
55
+
56
+ **Zero-Knowledge KYC for Algorand** — verify KYC status on-chain without exposing personal data.
57
+
58
+ [![PyPI version](https://img.shields.io/pypi/v/ciphera)](https://pypi.org/project/ciphera)
59
+ [![Python](https://img.shields.io/pypi/pyversions/ciphera)](https://pypi.org/project/ciphera)
60
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install ciphera
66
+ ```
67
+
68
+ ## What is Ciphera?
69
+
70
+ Ciphera is a privacy-preserving zero-knowledge KYC system on Algorand. Users prove they are KYC-verified Indian adults **without revealing any personal data**. ZK proofs are generated in the browser — private inputs never leave the device.
71
+
72
+ ## Quick Start
73
+
74
+ ### Check KYC Status (any dApp)
75
+
76
+ ```python
77
+ from ciphera import CipheraClient
78
+
79
+ client = CipheraClient() # connects to Algorand Testnet by default
80
+
81
+ # Check total registered credentials on-chain
82
+ status = client.verify_kyc()
83
+ print(status.is_verified) # True if any credentials registered
84
+ print(status.total_registered) # e.g. 42
85
+ print(status.app_id) # 756272073
86
+ ```
87
+
88
+ ### Check a Specific Nullifier
89
+
90
+ ```python
91
+ client = CipheraClient()
92
+
93
+ # Check if a specific nullifier is registered
94
+ is_verified = client.is_nullifier_registered("3b1f8a2c" + "0" * 56)
95
+ print(is_verified) # True / False
96
+ ```
97
+
98
+ ### Verify a ZK Proof (Issuer Backend)
99
+
100
+ ```python
101
+ import json
102
+ from ciphera import verify_proof
103
+
104
+ vk = json.load(open("verification_key.json"))
105
+
106
+ result = verify_proof(
107
+ verification_key=vk,
108
+ proof=proof_dict, # from browser SDK
109
+ public_signals=signals_list,
110
+ expected_app_id=756272073 # prevents cross-app attacks
111
+ )
112
+
113
+ if result.valid:
114
+ print(result.nullifier_hex) # 32-byte hex to register on-chain
115
+ print(result.public_signals.is_adult) # "1"
116
+ else:
117
+ print(result.error)
118
+ ```
119
+
120
+ ### Connect to Different Networks
121
+
122
+ ```python
123
+ from ciphera import CipheraClient
124
+
125
+ # Testnet (default)
126
+ client = CipheraClient()
127
+
128
+ # Mainnet
129
+ client = CipheraClient(network="mainnet")
130
+
131
+ # LocalNet (development)
132
+ client = CipheraClient(network="localnet")
133
+
134
+ # Custom endpoint
135
+ client = CipheraClient(
136
+ algod_url="https://my-algod.example.com",
137
+ algod_token="my-token"
138
+ )
139
+ ```
140
+
141
+ ---
142
+
143
+ ## API Reference
144
+
145
+ ### `CipheraClient`
146
+
147
+ ```python
148
+ CipheraClient(
149
+ network="testnet", # "testnet" | "mainnet" | "localnet"
150
+ algod_url=None, # Override endpoint
151
+ algod_token="", # Override token
152
+ contract_ids=None, # Override deployed contract IDs (dict)
153
+ )
154
+ ```
155
+
156
+ | Method | Returns | Description |
157
+ |---|---|---|
158
+ | `verify_kyc(wallet_address=None)` | `KYCStatus` | Check on-chain KYC registry |
159
+ | `is_nullifier_registered(hex)` | `bool` | Direct nullifier box check |
160
+ | `get_credential_asa_id()` | `int` | KYCRED ASA ID |
161
+ | `CipheraClient.explorer_tx_url(txid)` | `str` | Allo.info TX URL |
162
+ | `CipheraClient.explorer_app_url(app_id)` | `str` | Allo.info app URL |
163
+
164
+ ### `verify_proof(vk, proof, public_signals, expected_app_id=None)`
165
+
166
+ Verify a Groth16 ZK proof off-chain.
167
+
168
+ > **Requires Node.js** (`node` must be in PATH) as it delegates to snarkjs.
169
+
170
+ Returns `ProofResult` with `.valid`, `.nullifier_hex`, `.public_signals`, `.error`.
171
+
172
+ ---
173
+
174
+ ## Models
175
+
176
+ ```python
177
+ @dataclass
178
+ class KYCStatus:
179
+ is_verified: bool
180
+ app_id: int
181
+ total_registered: Optional[int]
182
+ nullifier: Optional[str]
183
+ error: Optional[str]
184
+
185
+ @dataclass
186
+ class ProofResult:
187
+ valid: bool
188
+ nullifier_hex: Optional[str]
189
+ public_signals: Optional[PublicSignals]
190
+ error: Optional[str]
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Deployed Contracts (Algorand Testnet)
196
+
197
+ | Contract | App ID |
198
+ |---|---|
199
+ | NullifierRegistry | 756272073 |
200
+ | SMTRegistry | 756272075 |
201
+ | KYCBoxStorage | 756272299 |
202
+ | CredentialManager | 756281076 |
203
+ | KYCRED ASA | 756281102 |
204
+
205
+ ---
206
+
207
+ ## Requirements
208
+
209
+ - Python 3.9+
210
+ - `py-algorand-sdk` (installed automatically)
211
+ - Node.js (only required for `verify_proof()`)
212
+
213
+ ---
214
+
215
+ ## License
216
+
217
+ MIT © [Aditya Pandey](https://github.com/Aditya060806)
@@ -0,0 +1,164 @@
1
+ # ciphera
2
+
3
+ **Zero-Knowledge KYC for Algorand** — verify KYC status on-chain without exposing personal data.
4
+
5
+ [![PyPI version](https://img.shields.io/pypi/v/ciphera)](https://pypi.org/project/ciphera)
6
+ [![Python](https://img.shields.io/pypi/pyversions/ciphera)](https://pypi.org/project/ciphera)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install ciphera
13
+ ```
14
+
15
+ ## What is Ciphera?
16
+
17
+ Ciphera is a privacy-preserving zero-knowledge KYC system on Algorand. Users prove they are KYC-verified Indian adults **without revealing any personal data**. ZK proofs are generated in the browser — private inputs never leave the device.
18
+
19
+ ## Quick Start
20
+
21
+ ### Check KYC Status (any dApp)
22
+
23
+ ```python
24
+ from ciphera import CipheraClient
25
+
26
+ client = CipheraClient() # connects to Algorand Testnet by default
27
+
28
+ # Check total registered credentials on-chain
29
+ status = client.verify_kyc()
30
+ print(status.is_verified) # True if any credentials registered
31
+ print(status.total_registered) # e.g. 42
32
+ print(status.app_id) # 756272073
33
+ ```
34
+
35
+ ### Check a Specific Nullifier
36
+
37
+ ```python
38
+ client = CipheraClient()
39
+
40
+ # Check if a specific nullifier is registered
41
+ is_verified = client.is_nullifier_registered("3b1f8a2c" + "0" * 56)
42
+ print(is_verified) # True / False
43
+ ```
44
+
45
+ ### Verify a ZK Proof (Issuer Backend)
46
+
47
+ ```python
48
+ import json
49
+ from ciphera import verify_proof
50
+
51
+ vk = json.load(open("verification_key.json"))
52
+
53
+ result = verify_proof(
54
+ verification_key=vk,
55
+ proof=proof_dict, # from browser SDK
56
+ public_signals=signals_list,
57
+ expected_app_id=756272073 # prevents cross-app attacks
58
+ )
59
+
60
+ if result.valid:
61
+ print(result.nullifier_hex) # 32-byte hex to register on-chain
62
+ print(result.public_signals.is_adult) # "1"
63
+ else:
64
+ print(result.error)
65
+ ```
66
+
67
+ ### Connect to Different Networks
68
+
69
+ ```python
70
+ from ciphera import CipheraClient
71
+
72
+ # Testnet (default)
73
+ client = CipheraClient()
74
+
75
+ # Mainnet
76
+ client = CipheraClient(network="mainnet")
77
+
78
+ # LocalNet (development)
79
+ client = CipheraClient(network="localnet")
80
+
81
+ # Custom endpoint
82
+ client = CipheraClient(
83
+ algod_url="https://my-algod.example.com",
84
+ algod_token="my-token"
85
+ )
86
+ ```
87
+
88
+ ---
89
+
90
+ ## API Reference
91
+
92
+ ### `CipheraClient`
93
+
94
+ ```python
95
+ CipheraClient(
96
+ network="testnet", # "testnet" | "mainnet" | "localnet"
97
+ algod_url=None, # Override endpoint
98
+ algod_token="", # Override token
99
+ contract_ids=None, # Override deployed contract IDs (dict)
100
+ )
101
+ ```
102
+
103
+ | Method | Returns | Description |
104
+ |---|---|---|
105
+ | `verify_kyc(wallet_address=None)` | `KYCStatus` | Check on-chain KYC registry |
106
+ | `is_nullifier_registered(hex)` | `bool` | Direct nullifier box check |
107
+ | `get_credential_asa_id()` | `int` | KYCRED ASA ID |
108
+ | `CipheraClient.explorer_tx_url(txid)` | `str` | Allo.info TX URL |
109
+ | `CipheraClient.explorer_app_url(app_id)` | `str` | Allo.info app URL |
110
+
111
+ ### `verify_proof(vk, proof, public_signals, expected_app_id=None)`
112
+
113
+ Verify a Groth16 ZK proof off-chain.
114
+
115
+ > **Requires Node.js** (`node` must be in PATH) as it delegates to snarkjs.
116
+
117
+ Returns `ProofResult` with `.valid`, `.nullifier_hex`, `.public_signals`, `.error`.
118
+
119
+ ---
120
+
121
+ ## Models
122
+
123
+ ```python
124
+ @dataclass
125
+ class KYCStatus:
126
+ is_verified: bool
127
+ app_id: int
128
+ total_registered: Optional[int]
129
+ nullifier: Optional[str]
130
+ error: Optional[str]
131
+
132
+ @dataclass
133
+ class ProofResult:
134
+ valid: bool
135
+ nullifier_hex: Optional[str]
136
+ public_signals: Optional[PublicSignals]
137
+ error: Optional[str]
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Deployed Contracts (Algorand Testnet)
143
+
144
+ | Contract | App ID |
145
+ |---|---|
146
+ | NullifierRegistry | 756272073 |
147
+ | SMTRegistry | 756272075 |
148
+ | KYCBoxStorage | 756272299 |
149
+ | CredentialManager | 756281076 |
150
+ | KYCRED ASA | 756281102 |
151
+
152
+ ---
153
+
154
+ ## Requirements
155
+
156
+ - Python 3.9+
157
+ - `py-algorand-sdk` (installed automatically)
158
+ - Node.js (only required for `verify_proof()`)
159
+
160
+ ---
161
+
162
+ ## License
163
+
164
+ MIT © [Aditya Pandey](https://github.com/Aditya060806)
@@ -0,0 +1,34 @@
1
+ """
2
+ ciphera — Zero-Knowledge KYC SDK for Algorand (Python)
3
+
4
+ Install:
5
+ pip install ciphera
6
+
7
+ Usage:
8
+ from ciphera import CipheraClient
9
+
10
+ client = CipheraClient()
11
+ status = client.verify_kyc("WALLET_ADDRESS")
12
+ print(status.is_verified)
13
+ """
14
+
15
+ from .client import CipheraClient
16
+ from .models import KYCStatus, ProofResult, PublicSignals
17
+ from .verifier import verify_proof
18
+ from .exceptions import CipheraError, ProofVerificationError, NetworkError
19
+
20
+ __version__ = "0.1.0"
21
+ __author__ = "Aditya Pandey"
22
+ __license__ = "MIT"
23
+
24
+ __all__ = [
25
+ "CipheraClient",
26
+ "KYCStatus",
27
+ "ProofResult",
28
+ "PublicSignals",
29
+ "verify_proof",
30
+ "CipheraError",
31
+ "ProofVerificationError",
32
+ "NetworkError",
33
+ "__version__",
34
+ ]
@@ -0,0 +1,182 @@
1
+ """
2
+ client.py — CipheraClient for Python
3
+
4
+ Connect to Algorand and query the Ciphera KYC contracts.
5
+
6
+ Usage:
7
+ from ciphera import CipheraClient
8
+
9
+ client = CipheraClient() # Testnet (default)
10
+ client = CipheraClient(network="mainnet") # Mainnet
11
+ client = CipheraClient(algod_url="http://localhost:4001", algod_token="aaaa...") # LocalNet
12
+
13
+ # Check KYC status
14
+ status = client.verify_kyc("WALLET_ADDRESS")
15
+ print(status.is_verified)
16
+
17
+ # Check a specific nullifier
18
+ registered = client.is_nullifier_registered("deadbeef...")
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import base64
24
+ from typing import Optional, Dict, Any
25
+
26
+ try:
27
+ import algosdk
28
+ from algosdk.v2client import algod as algod_client
29
+ _ALGOSDK_AVAILABLE = True
30
+ except ImportError:
31
+ _ALGOSDK_AVAILABLE = False
32
+
33
+ from .models import KYCStatus
34
+ from .exceptions import NetworkError, ContractError
35
+
36
+
37
+ # ── Default Algorand Testnet endpoints ───────────────────────────────────────
38
+ _TESTNET_ALGOD = "https://testnet-api.algonode.cloud"
39
+ _MAINNET_ALGOD = "https://mainnet-api.algonode.cloud"
40
+ _LOCALNET_ALGOD = "http://localhost:4001"
41
+ _LOCALNET_TOKEN = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
42
+
43
+
44
+ # ── Deployed contract IDs on Algorand Testnet ─────────────────────────────────
45
+ DEFAULT_CONTRACT_IDS = {
46
+ "nullifier_registry": 756272073,
47
+ "smt_registry": 756272075,
48
+ "kyc_box_storage": 756272299,
49
+ "credential_manager": 756281076,
50
+ "credential_asa_id": 756281102,
51
+ }
52
+
53
+
54
+ class CipheraClient:
55
+ """
56
+ Python client for the Ciphera ZK-KYC system on Algorand.
57
+
58
+ Args:
59
+ network: "testnet" (default) | "mainnet" | "localnet"
60
+ algod_url: Custom Algod endpoint URL (overrides network preset)
61
+ algod_token: Custom Algod API token (default: empty for AlgoNode)
62
+ contract_ids: Override deployed contract IDs (dict)
63
+
64
+ Example:
65
+ client = CipheraClient()
66
+ status = client.verify_kyc("AAAA...")
67
+ print(status.is_verified)
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ network: str = "testnet",
73
+ algod_url: Optional[str] = None,
74
+ algod_token: str = "",
75
+ contract_ids: Optional[Dict[str, int]] = None,
76
+ ) -> None:
77
+ if not _ALGOSDK_AVAILABLE:
78
+ raise ImportError(
79
+ "py-algorand-sdk is required: pip install ciphera[algorand]"
80
+ )
81
+
82
+ # Resolve endpoint
83
+ if algod_url:
84
+ url, token = algod_url, algod_token
85
+ elif network == "mainnet":
86
+ url, token = _MAINNET_ALGOD, ""
87
+ elif network == "localnet":
88
+ url, token = _LOCALNET_ALGOD, _LOCALNET_TOKEN
89
+ else: # testnet (default)
90
+ url, token = _TESTNET_ALGOD, ""
91
+
92
+ self._algod = algod_client.AlgodClient(token, url)
93
+
94
+ # Merge contract IDs
95
+ self._contracts = {**DEFAULT_CONTRACT_IDS, **(contract_ids or {})}
96
+
97
+ # ── Public Methods ─────────────────────────────────────────────────────
98
+
99
+ def verify_kyc(self, wallet_address: Optional[str] = None) -> KYCStatus:
100
+ """
101
+ Check KYC status via the on-chain NullifierRegistry.
102
+
103
+ Reads the `total_registered` global state variable.
104
+ A non-zero value means at least one credential has been registered.
105
+
106
+ For wallet-specific KYC, use is_nullifier_registered() with
107
+ the wallet's nullifier hex.
108
+
109
+ Args:
110
+ wallet_address: Not currently used (reserved for future wallet-level checks).
111
+
112
+ Returns:
113
+ KYCStatus with is_verified, total_registered, app_id
114
+
115
+ Example:
116
+ status = client.verify_kyc()
117
+ print(f"Registry has {status.total_registered} verified users")
118
+ """
119
+ app_id = self._contracts["nullifier_registry"]
120
+ try:
121
+ app_info = self._algod.application_info(app_id)
122
+ global_state = app_info.get("params", {}).get("global-state", [])
123
+
124
+ total_registered = 0
125
+ for kv in global_state:
126
+ key = base64.b64decode(kv["key"]).decode("utf-8", errors="ignore")
127
+ if key == "total_registered":
128
+ total_registered = kv["value"].get("uint", 0)
129
+
130
+ return KYCStatus(
131
+ is_verified=total_registered > 0,
132
+ app_id=app_id,
133
+ total_registered=total_registered,
134
+ )
135
+ except Exception as e:
136
+ raise NetworkError(f"Failed to query NullifierRegistry (app {app_id}): {e}") from e
137
+
138
+ def is_nullifier_registered(self, nullifier_hex: str) -> bool:
139
+ """
140
+ Check if a specific nullifier is registered in box storage.
141
+
142
+ More precise than verify_kyc() — directly confirms a specific
143
+ credential nullifier exists on-chain.
144
+
145
+ Args:
146
+ nullifier_hex: 32-byte nullifier as hex string (64 chars, no 0x prefix)
147
+
148
+ Returns:
149
+ True if the nullifier is registered, False otherwise
150
+
151
+ Example:
152
+ registered = client.is_nullifier_registered("deadbeef" * 8)
153
+ """
154
+ app_id = self._contracts["nullifier_registry"]
155
+ try:
156
+ # Box name = b"nul_" + 32-byte nullifier
157
+ prefix = b"nul_"
158
+ nullifier_bytes = bytes.fromhex(nullifier_hex.lstrip("0x").zfill(64))
159
+ box_name = base64.b64encode(prefix + nullifier_bytes).decode()
160
+
161
+ result = self._algod.application_box_by_name(app_id, box_name)
162
+ return bool(result.get("value"))
163
+ except algosdk.error.AlgodHTTPError as e:
164
+ if "box not found" in str(e).lower() or e.code == 404:
165
+ return False
166
+ raise NetworkError(f"Box lookup failed: {e}") from e
167
+ except Exception as e:
168
+ return False
169
+
170
+ def get_credential_asa_id(self) -> int:
171
+ """Return the KYCRED ASA (Algorand Standard Asset) ID."""
172
+ return self._contracts["credential_asa_id"]
173
+
174
+ @staticmethod
175
+ def explorer_tx_url(txid: str) -> str:
176
+ """Return Allo.info explorer URL for a transaction."""
177
+ return f"https://allo.info/tx/{txid}"
178
+
179
+ @staticmethod
180
+ def explorer_app_url(app_id: int) -> str:
181
+ """Return Allo.info explorer URL for an application."""
182
+ return f"https://allo.info/application/{app_id}"
@@ -0,0 +1,23 @@
1
+ """
2
+ exceptions.py — Ciphera SDK custom exceptions
3
+ """
4
+
5
+
6
+ class CipheraError(Exception):
7
+ """Base exception for all Ciphera SDK errors."""
8
+ pass
9
+
10
+
11
+ class ProofVerificationError(CipheraError):
12
+ """Raised when a ZK proof fails verification."""
13
+ pass
14
+
15
+
16
+ class NetworkError(CipheraError):
17
+ """Raised when Algorand network requests fail."""
18
+ pass
19
+
20
+
21
+ class ContractError(CipheraError):
22
+ """Raised when smart contract interactions fail."""
23
+ pass
@@ -0,0 +1,56 @@
1
+ """
2
+ models.py — Ciphera SDK Pydantic models
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from dataclasses import dataclass, field
7
+ from typing import Optional, List
8
+
9
+
10
+ @dataclass
11
+ class PublicSignals:
12
+ """Public signals output from the ZK circuit."""
13
+ nullifier: str # BN254 field element as decimal string
14
+ merkle_root: str # SMT root
15
+ app_id: str # Algorand NullifierRegistry app ID
16
+ is_indian: str # "1"
17
+ is_adult: str # "1"
18
+ is_kyc_verified: str # "1"
19
+
20
+ @classmethod
21
+ def from_list(cls, signals: List[str]) -> "PublicSignals":
22
+ """Parse from snarkjs publicSignals array."""
23
+ if len(signals) < 6:
24
+ raise ValueError(f"Expected >= 6 public signals, got {len(signals)}")
25
+ return cls(
26
+ nullifier=signals[0],
27
+ merkle_root=signals[1],
28
+ app_id=signals[2],
29
+ is_indian=signals[3],
30
+ is_adult=signals[4],
31
+ is_kyc_verified=signals[5],
32
+ )
33
+
34
+ @property
35
+ def nullifier_hex(self) -> str:
36
+ """Nullifier as 32-byte hex string (suitable for Algorand box keys)."""
37
+ return format(int(self.nullifier), '064x')
38
+
39
+
40
+ @dataclass
41
+ class ProofResult:
42
+ """Result of ZK proof verification."""
43
+ valid: bool
44
+ nullifier_hex: Optional[str] = None
45
+ public_signals: Optional[PublicSignals] = None
46
+ error: Optional[str] = None
47
+
48
+
49
+ @dataclass
50
+ class KYCStatus:
51
+ """KYC verification status for a wallet address."""
52
+ is_verified: bool
53
+ app_id: int
54
+ total_registered: Optional[int] = None
55
+ nullifier: Optional[str] = None
56
+ error: Optional[str] = None
@@ -0,0 +1,128 @@
1
+ """
2
+ verifier.py — ZK proof verification for the Ciphera Python SDK.
3
+
4
+ Verifies Groth16 proofs using snarkjs via Node.js subprocess.
5
+ The issuer backend calls verify_proof() before registering
6
+ a nullifier on-chain.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import subprocess
13
+ import shutil
14
+ import tempfile
15
+ from pathlib import Path
16
+ from typing import Union, List, Dict, Any
17
+
18
+ from .models import ProofResult, PublicSignals
19
+ from .exceptions import ProofVerificationError
20
+
21
+
22
+ def verify_proof(
23
+ verification_key: Union[Dict[str, Any], str, Path],
24
+ proof: Dict[str, Any],
25
+ public_signals: List[str],
26
+ expected_app_id: Optional[Union[int, str]] = None,
27
+ ) -> ProofResult:
28
+ """
29
+ Verify a Groth16 ZK proof using snarkjs (via Node.js subprocess).
30
+
31
+ Args:
32
+ verification_key: The Groth16 verification key (dict, JSON string, or file path).
33
+ proof: The Groth16 proof object from snarkjs.
34
+ public_signals: List of public signal strings from the circuit.
35
+ expected_app_id: Optional NullifierRegistry app ID to guard cross-app attacks.
36
+
37
+ Returns:
38
+ ProofResult with .valid, .nullifier_hex, .public_signals, .error
39
+
40
+ Example:
41
+ import json
42
+ vk = json.load(open("verification_key.json"))
43
+ result = verify_proof(vk, proof_dict, signal_list, expected_app_id=756272073)
44
+ if result.valid:
45
+ print(result.nullifier_hex)
46
+ """
47
+ # Normalise verification_key to dict
48
+ if isinstance(verification_key, (str, Path)):
49
+ p = Path(verification_key)
50
+ if p.exists():
51
+ with open(p) as f:
52
+ vk_dict = json.load(f)
53
+ else:
54
+ vk_dict = json.loads(str(verification_key))
55
+ else:
56
+ vk_dict = verification_key
57
+
58
+ # Validate public signals length
59
+ if len(public_signals) < 6:
60
+ return ProofResult(
61
+ valid=False,
62
+ error=f"Expected >= 6 public signals, got {len(public_signals)}"
63
+ )
64
+
65
+ # Quick sanity checks before expensive snarkjs call
66
+ is_indian, is_adult, is_kyc = public_signals[3], public_signals[4], public_signals[5]
67
+ if is_indian != "1":
68
+ return ProofResult(valid=False, error="isIndian must be 1")
69
+ if is_adult != "1":
70
+ return ProofResult(valid=False, error="isAdult must be 1")
71
+ if is_kyc != "1":
72
+ return ProofResult(valid=False, error="isKYCVerified must be 1")
73
+
74
+ app_id_signal = public_signals[2]
75
+ if expected_app_id is not None and app_id_signal != str(expected_app_id):
76
+ return ProofResult(
77
+ valid=False,
78
+ error=f"appId mismatch: expected {expected_app_id}, got {app_id_signal}"
79
+ )
80
+
81
+ # Run snarkjs verification via Node.js
82
+ if not shutil.which("node"):
83
+ raise ProofVerificationError(
84
+ "Node.js is required for ZK proof verification. "
85
+ "Install from https://nodejs.org"
86
+ )
87
+
88
+ js_script = """
89
+ const snarkjs = require('snarkjs');
90
+ const input = JSON.parse(process.argv[2]);
91
+ snarkjs.groth16.verify(input.vk, input.publicSignals, input.proof)
92
+ .then(valid => { process.stdout.write(JSON.stringify({valid})); })
93
+ .catch(e => { process.stdout.write(JSON.stringify({valid: false, error: e.message})); });
94
+ """
95
+ payload = json.dumps({
96
+ "vk": vk_dict,
97
+ "proof": proof,
98
+ "publicSignals": public_signals,
99
+ })
100
+
101
+ with tempfile.NamedTemporaryFile(suffix=".js", mode="w", delete=False) as f:
102
+ f.write(js_script)
103
+ script_path = f.name
104
+
105
+ try:
106
+ result = subprocess.run(
107
+ ["node", script_path, payload],
108
+ capture_output=True, text=True, timeout=60
109
+ )
110
+ output = json.loads(result.stdout)
111
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError) as e:
112
+ return ProofResult(valid=False, error=f"Verification failed: {e}")
113
+ finally:
114
+ Path(script_path).unlink(missing_ok=True)
115
+
116
+ if not output.get("valid"):
117
+ return ProofResult(valid=False, error=output.get("error", "Proof is invalid"))
118
+
119
+ signals = PublicSignals.from_list(public_signals)
120
+ return ProofResult(
121
+ valid=True,
122
+ nullifier_hex=signals.nullifier_hex,
123
+ public_signals=signals,
124
+ )
125
+
126
+
127
+ # Optional type hint (Python 3.9 compat)
128
+ from typing import Optional
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ciphera"
7
+ version = "0.1.0"
8
+ description = "Zero-Knowledge KYC for Algorand — verify KYC status on-chain without exposing personal data"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ authors = [
12
+ { name = "Aditya Pandey", email = "adityapandey060806@gmail.com" }
13
+ ]
14
+ keywords = [
15
+ "algorand", "zero-knowledge", "zk-proof", "kyc",
16
+ "aadhaar", "privacy", "blockchain", "snarkjs", "groth16"
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Developers",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Security :: Cryptography",
28
+ "Topic :: Software Development :: Libraries :: Python Modules",
29
+ ]
30
+ requires-python = ">=3.9"
31
+ dependencies = [
32
+ "py-algorand-sdk>=2.6.0",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ algorand = ["py-algorand-sdk>=2.6.0"]
37
+ dev = [
38
+ "pytest>=7.0",
39
+ "pytest-asyncio>=0.23",
40
+ "twine>=5.0",
41
+ "build>=1.0",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/Aditya060806/Ciphera"
46
+ Repository = "https://github.com/Aditya060806/Ciphera"
47
+ Issues = "https://github.com/Aditya060806/Ciphera/issues"
48
+ Documentation = "https://github.com/Aditya060806/Ciphera#readme"
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["ciphera"]
52
+
53
+ [tool.hatch.build.targets.sdist]
54
+ include = [
55
+ "ciphera/",
56
+ "README.md",
57
+ "LICENSE",
58
+ "pyproject.toml",
59
+ ]