chipi-stack 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ """Encryption utilities for private key protection."""
2
+
3
+ import base64
4
+ import hashlib
5
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
6
+ from cryptography.hazmat.backends import default_backend
7
+ from cryptography.hazmat.primitives import hashes
8
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
9
+
10
+
11
+ def encrypt_private_key(private_key: str, password: str) -> str:
12
+ """
13
+ Encrypt private key using AES-256-CBC.
14
+
15
+ This implementation is compatible with crypto-es library used in TypeScript SDK.
16
+ Uses OpenSSL-compatible format: "Salted__" + salt + ciphertext
17
+
18
+ Args:
19
+ private_key: Private key to encrypt
20
+ password: Password for encryption
21
+
22
+ Returns:
23
+ Base64 encoded encrypted string
24
+ """
25
+ # Generate random salt (8 bytes)
26
+ import os
27
+ salt = os.urandom(8)
28
+
29
+ # Derive key and IV from password using OpenSSL's EVP_BytesToKey equivalent
30
+ # This matches crypto-es behavior
31
+ key_iv = _derive_key_and_iv(password.encode('utf-8'), salt, 32, 16)
32
+ key = key_iv[:32]
33
+ iv = key_iv[32:48]
34
+
35
+ # Pad the private key (PKCS7 padding)
36
+ padded_key = _pkcs7_pad(private_key.encode('utf-8'), 16)
37
+
38
+ # Encrypt
39
+ cipher = Cipher(
40
+ algorithms.AES(key),
41
+ modes.CBC(iv),
42
+ backend=default_backend()
43
+ )
44
+ encryptor = cipher.encryptor()
45
+ ciphertext = encryptor.update(padded_key) + encryptor.finalize()
46
+
47
+ # Format as OpenSSL compatible: "Salted__" + salt + ciphertext
48
+ encrypted_data = b'Salted__' + salt + ciphertext
49
+
50
+ # Base64 encode
51
+ return base64.b64encode(encrypted_data).decode('utf-8')
52
+
53
+
54
+ def decrypt_private_key(encrypted_private_key: str, password: str) -> str:
55
+ """
56
+ Decrypt private key using AES-256-CBC.
57
+
58
+ This implementation is compatible with crypto-es library used in TypeScript SDK.
59
+
60
+ Args:
61
+ encrypted_private_key: Base64 encoded encrypted private key
62
+ password: Password for decryption
63
+
64
+ Returns:
65
+ Decrypted private key
66
+
67
+ Raises:
68
+ ValueError: If decryption fails
69
+ """
70
+ try:
71
+ # Decode base64
72
+ encrypted_data = base64.b64decode(encrypted_private_key)
73
+
74
+ # Check for "Salted__" prefix
75
+ if not encrypted_data.startswith(b'Salted__'):
76
+ raise ValueError("Invalid encrypted data format")
77
+
78
+ # Extract salt and ciphertext
79
+ salt = encrypted_data[8:16]
80
+ ciphertext = encrypted_data[16:]
81
+
82
+ # Derive key and IV from password
83
+ key_iv = _derive_key_and_iv(password.encode('utf-8'), salt, 32, 16)
84
+ key = key_iv[:32]
85
+ iv = key_iv[32:48]
86
+
87
+ # Decrypt
88
+ cipher = Cipher(
89
+ algorithms.AES(key),
90
+ modes.CBC(iv),
91
+ backend=default_backend()
92
+ )
93
+ decryptor = cipher.decryptor()
94
+ padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
95
+
96
+ # Remove PKCS7 padding
97
+ plaintext = _pkcs7_unpad(padded_plaintext)
98
+
99
+ # Convert to string
100
+ decrypted = plaintext.decode('utf-8')
101
+
102
+ if not decrypted:
103
+ raise ValueError("Decryption resulted in empty string")
104
+
105
+ return decrypted
106
+ except Exception as e:
107
+ raise ValueError(f"Decryption failed: {str(e)}")
108
+
109
+
110
+ def _derive_key_and_iv(password: bytes, salt: bytes, key_len: int, iv_len: int) -> bytes:
111
+ """
112
+ Derive key and IV using OpenSSL's EVP_BytesToKey algorithm.
113
+
114
+ This matches the behavior of crypto-es to ensure compatibility.
115
+ Uses MD5 hashing in multiple rounds.
116
+
117
+ Args:
118
+ password: Password bytes
119
+ salt: Salt bytes
120
+ key_len: Desired key length
121
+ iv_len: Desired IV length
122
+
123
+ Returns:
124
+ Concatenated key + IV bytes
125
+ """
126
+ target_len = key_len + iv_len
127
+ derived = b''
128
+ last_hash = b''
129
+
130
+ while len(derived) < target_len:
131
+ last_hash = hashlib.md5(last_hash + password + salt).digest()
132
+ derived += last_hash
133
+
134
+ return derived[:target_len]
135
+
136
+
137
+ def _pkcs7_pad(data: bytes, block_size: int) -> bytes:
138
+ """
139
+ Apply PKCS7 padding to data.
140
+
141
+ Args:
142
+ data: Data to pad
143
+ block_size: Block size in bytes
144
+
145
+ Returns:
146
+ Padded data
147
+ """
148
+ padding_len = block_size - (len(data) % block_size)
149
+ padding = bytes([padding_len] * padding_len)
150
+ return data + padding
151
+
152
+
153
+ def _pkcs7_unpad(data: bytes) -> bytes:
154
+ """
155
+ Remove PKCS7 padding from data.
156
+
157
+ Args:
158
+ data: Padded data
159
+
160
+ Returns:
161
+ Unpadded data
162
+
163
+ Raises:
164
+ ValueError: If padding is invalid
165
+ """
166
+ if not data:
167
+ raise ValueError("Cannot unpad empty data")
168
+
169
+ padding_len = data[-1]
170
+
171
+ if padding_len > len(data) or padding_len == 0:
172
+ raise ValueError("Invalid padding")
173
+
174
+ # Verify all padding bytes are correct
175
+ for i in range(padding_len):
176
+ if data[-(i + 1)] != padding_len:
177
+ raise ValueError("Invalid padding")
178
+
179
+ return data[:-padding_len]
chipi_sdk/errors.py ADDED
@@ -0,0 +1,130 @@
1
+ """Error classes and utilities for Chipi SDK."""
2
+
3
+ from typing import Any, Optional
4
+
5
+
6
+ class ChipiError(Exception):
7
+ """Base exception for Chipi SDK."""
8
+
9
+ def __init__(
10
+ self, message: str, code: str = "UNKNOWN_ERROR", status: Optional[int] = None
11
+ ):
12
+ super().__init__(message)
13
+ self.message = message
14
+ self.code = code
15
+ self.status = status
16
+
17
+
18
+ class ChipiApiError(ChipiError):
19
+ """API request errors."""
20
+
21
+ def __init__(self, message: str, code: str, status: int):
22
+ super().__init__(message, code, status)
23
+
24
+
25
+ class ChipiWalletError(ChipiError):
26
+ """Wallet operation errors."""
27
+
28
+ def __init__(self, message: str, code: str = "WALLET_ERROR"):
29
+ super().__init__(message, code)
30
+
31
+
32
+ class ChipiTransactionError(ChipiError):
33
+ """Transaction execution errors."""
34
+
35
+ def __init__(self, message: str, code: str = "TRANSACTION_ERROR"):
36
+ super().__init__(message, code)
37
+
38
+
39
+ class ChipiSessionError(ChipiError):
40
+ """Session management errors."""
41
+
42
+ def __init__(self, message: str, code: str = "SESSION_ERROR"):
43
+ super().__init__(message, code)
44
+
45
+
46
+ class ChipiSkuError(ChipiError):
47
+ """SKU-related errors."""
48
+
49
+ def __init__(self, message: str, code: str = "SKU_ERROR"):
50
+ super().__init__(message, code)
51
+
52
+
53
+ class ChipiValidationError(ChipiError):
54
+ """Validation errors."""
55
+
56
+ def __init__(self, message: str, code: str = "VALIDATION_ERROR"):
57
+ super().__init__(message, code, 400)
58
+
59
+
60
+ class ChipiAuthError(ChipiError):
61
+ """Authentication errors."""
62
+
63
+ def __init__(self, message: str, code: str = "AUTH_ERROR"):
64
+ super().__init__(message, code, 401)
65
+
66
+
67
+ class WalletNotCompatibleError(ChipiWalletError):
68
+ """Thrown when attempting an operation the wallet type doesn't support."""
69
+
70
+ def __init__(self, message: str):
71
+ super().__init__(message, "WALLET_NOT_COMPATIBLE")
72
+
73
+
74
+ class WalletClassHashNotFoundError(ChipiWalletError):
75
+ """Thrown when a custom wallet type's class hash isn't registered."""
76
+
77
+ def __init__(self, wallet_type: str):
78
+ super().__init__(
79
+ f"No class hash registered for wallet type '{wallet_type}'. "
80
+ "Register it via custom_wallet_types in SDK config.",
81
+ "WALLET_CLASS_HASH_NOT_FOUND",
82
+ )
83
+
84
+
85
+ class PaymasterIncompatibleError(ChipiError):
86
+ """Thrown when wallet account doesn't support SNIP-9 outside execution."""
87
+
88
+ def __init__(self, class_hash: str):
89
+ super().__init__(
90
+ f"Wallet class hash {class_hash} is not compatible with Chipi paymaster. "
91
+ "Account must implement SNIP-9 (outside_execution_v2).",
92
+ "PAYMASTER_INCOMPATIBLE",
93
+ 400,
94
+ )
95
+
96
+
97
+ def is_chipi_error(error: Any) -> bool:
98
+ """Check if an error is a ChipiError instance."""
99
+ return isinstance(error, ChipiError)
100
+
101
+
102
+ def handle_api_error(error: Any) -> ChipiError:
103
+ """
104
+ Convert various error types to ChipiError.
105
+
106
+ Args:
107
+ error: Any exception or error object
108
+
109
+ Returns:
110
+ ChipiError instance
111
+ """
112
+ if is_chipi_error(error):
113
+ return error
114
+
115
+ # Handle httpx or requests errors
116
+ if hasattr(error, "response"):
117
+ status = getattr(error.response, "status_code", None)
118
+ if status:
119
+ try:
120
+ data = error.response.json()
121
+ message = data.get("message", str(error))
122
+ code = data.get("code", f"HTTP_{status}")
123
+ except Exception:
124
+ message = str(error)
125
+ code = f"HTTP_{status}"
126
+ return ChipiApiError(message, code, status)
127
+
128
+ # Default case
129
+ message = str(error) if error else "An unknown error occurred"
130
+ return ChipiError(message, "UNKNOWN_ERROR")