CCencrypt 0.3.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.
- ccencrypt-0.3.0/CCencrypt.egg-info/PKG-INFO +214 -0
- ccencrypt-0.3.0/CCencrypt.egg-info/SOURCES.txt +19 -0
- ccencrypt-0.3.0/CCencrypt.egg-info/dependency_links.txt +1 -0
- ccencrypt-0.3.0/CCencrypt.egg-info/requires.txt +2 -0
- ccencrypt-0.3.0/CCencrypt.egg-info/top_level.txt +1 -0
- ccencrypt-0.3.0/PKG-INFO +214 -0
- ccencrypt-0.3.0/README.md +201 -0
- ccencrypt-0.3.0/ccencrypt/__init__.py +25 -0
- ccencrypt-0.3.0/ccencrypt/adyen.py +154 -0
- ccencrypt-0.3.0/ccencrypt/adyen_cse.py +45 -0
- ccencrypt-0.3.0/ccencrypt/braintree.py +66 -0
- ccencrypt-0.3.0/ccencrypt/cybersource.py +150 -0
- ccencrypt-0.3.0/ccencrypt/eway.py +35 -0
- ccencrypt-0.3.0/ccencrypt/helpers/__init__.py +0 -0
- ccencrypt-0.3.0/ccencrypt/helpers/card.py +21 -0
- ccencrypt-0.3.0/ccencrypt/helpers/encoding.py +34 -0
- ccencrypt-0.3.0/ccencrypt/paydock.py +41 -0
- ccencrypt-0.3.0/ccencrypt/shopify.py +40 -0
- ccencrypt-0.3.0/ccencrypt/stripe.py +47 -0
- ccencrypt-0.3.0/pyproject.toml +24 -0
- ccencrypt-0.3.0/setup.cfg +4 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CCencrypt
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Card encryption and tokenisation for Adyen, CyberSource, eWay, Stripe, Braintree, Shopify and PayDock
|
|
5
|
+
Author: itsregza
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/itsregza/CCencrypt
|
|
8
|
+
Project-URL: Repository, https://github.com/itsregza/CCencrypt
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: cryptography>=42.0.0
|
|
12
|
+
Requires-Dist: requests>=2.28.0
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="https://raw.githubusercontent.com/itsregza/CCencrypt/main/assets/banner.svg" alt="CCencrypt" width="520">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">Card encryption and tokenisation for Python.</p>
|
|
19
|
+
|
|
20
|
+
| Processor | Function | Type | Returns |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| Adyen | `ccadyen` | Encrypt | dict of 4 encrypted fields |
|
|
23
|
+
| CyberSource Flex | `cccybersource` / `cccybersource_token` | Encrypt / Tokenise | JWE / transient token |
|
|
24
|
+
| eWay | `cceway` / `cceway_card` | Encrypt | `eCrypted:...` strings |
|
|
25
|
+
| Stripe | `ccstripe` | Tokenise | `pm_...` payment method id |
|
|
26
|
+
| Braintree | `ccbraintree` | Tokenise | `tokencc_...` nonce |
|
|
27
|
+
| Shopify | `ccshopify` | Tokenise | card session id |
|
|
28
|
+
| PayDock | `ccpaydock` | Tokenise | one-time card token |
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ccencrypt
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Card details
|
|
37
|
+
|
|
38
|
+
Every function cleans the card details before using them, so any of these work:
|
|
39
|
+
|
|
40
|
+
| Field | Accepted |
|
|
41
|
+
|---|---|
|
|
42
|
+
| card | `"4242424242424242"`, `"4242 4242 4242 4242"`, `"4242-4242-4242-4242"` |
|
|
43
|
+
| month | `1`, `"1"`, `"01"` |
|
|
44
|
+
| year | `29`, `"29"`, `2029`, `"2029"` |
|
|
45
|
+
|
|
46
|
+
Month and year are always separate arguments. If you have `"01/29"`, split it first:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
month, year = "01/29".split("/")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Proxies
|
|
53
|
+
|
|
54
|
+
You can pass a proxy for the process. Some processors like paydock will ratelimit so its important to use proxies:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
token = ccencrypt.ccpaydock(public_key, card, month, year, cvv, name, proxy="http://user:pass@host:port")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Errors
|
|
61
|
+
|
|
62
|
+
Tokenise functions raise `RuntimeError` with the status code and response body when the processor rejects the request.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Encryption
|
|
67
|
+
|
|
68
|
+
### Adyen
|
|
69
|
+
|
|
70
|
+
`encrypt_key` is the site's Adyen public key (`10001|A532...`).
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import ccencrypt
|
|
74
|
+
|
|
75
|
+
result = ccencrypt.ccadyen(
|
|
76
|
+
encrypt_key="10001|A532...",
|
|
77
|
+
card="4242424242424242",
|
|
78
|
+
month="12",
|
|
79
|
+
year="2029",
|
|
80
|
+
cvc="123",
|
|
81
|
+
client_key="live_...",
|
|
82
|
+
domain="https://example.com",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
result["encryptedCardNumber"]
|
|
86
|
+
result["encryptedExpiryMonth"]
|
|
87
|
+
result["encryptedExpiryYear"]
|
|
88
|
+
result["encryptedSecurityCode"]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`client_key` and `domain` are optional, but pass the site's own values when you have them.
|
|
92
|
+
|
|
93
|
+
`version` defaults to `"4.5.0"` (Secured Fields). For sites on legacy encryption (`adyenjs_0_1_XX$...`), pass `"0_1_18"`, `"0_1_22"`, `"0_1_23"`, `"0_1_24"` or `"0_1_25"`. Legacy versions ignore `client_key` and `domain`.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
result = ccencrypt.ccadyen(encrypt_key, card, month, year, cvc, version="0_1_25")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To encrypt a custom legacy payload such as `holderName`:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from ccencrypt.adyen import parse_public_key
|
|
103
|
+
|
|
104
|
+
blob = ccencrypt.cse_encrypt(
|
|
105
|
+
{"holderName": "John Smith", "generationtime": "2029-01-01T00:00:00.000Z"},
|
|
106
|
+
parse_public_key("10001|A532..."),
|
|
107
|
+
version="0_1_25",
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### CyberSource Flex
|
|
112
|
+
|
|
113
|
+
`context` is the capture context JWT from the site's CyberSource init endpoint.
|
|
114
|
+
|
|
115
|
+
Encrypt and exchange for a transient token in one call:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
token = ccencrypt.cccybersource_token(
|
|
119
|
+
card="4242424242424242",
|
|
120
|
+
month="12",
|
|
121
|
+
year="2029",
|
|
122
|
+
cvv="123",
|
|
123
|
+
context=context,
|
|
124
|
+
origin="https://www.example.com",
|
|
125
|
+
user_agent="Mozilla/5.0 ...",
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For Microform sites, pass `microform=True` instead of `origin`.
|
|
130
|
+
|
|
131
|
+
To encrypt only, use `cccybersource` with the same card arguments and `context`. It returns a JWE that you POST yourself to `https://flex.cybersource.com/flex/v2/tokens` as `application/jwt`.
|
|
132
|
+
|
|
133
|
+
### eWay
|
|
134
|
+
|
|
135
|
+
`encrypt_key` is the `data-eway-encrypt-key` value from the site's payment form.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
fields = ccencrypt.cceway_card(encrypt_key, card="4242424242424242", cvv="123")
|
|
139
|
+
|
|
140
|
+
fields["EWAY_CARDNUMBER"]
|
|
141
|
+
fields["EWAY_CARDCVN"]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
To encrypt a single value, use `ccencrypt.cceway(encrypt_key, value)`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Tokenisation
|
|
149
|
+
|
|
150
|
+
### Stripe
|
|
151
|
+
|
|
152
|
+
`key` is the site's publishable key (`pk_live_...`), sent as `key=` in the site's requests to `api.stripe.com`.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
pm = ccencrypt.ccstripe(
|
|
156
|
+
key="pk_live_...",
|
|
157
|
+
card="4242424242424242",
|
|
158
|
+
month="12",
|
|
159
|
+
year="2029",
|
|
160
|
+
cvc="123",
|
|
161
|
+
name="John Smith",
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
If the site's requests also send `_stripe_account=acct_...`, pass it as `account`.
|
|
166
|
+
|
|
167
|
+
### Braintree
|
|
168
|
+
|
|
169
|
+
`client_token` is the base64 client token the site hands to Braintree. A raw `authorizationFingerprint` also works.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
nonce = ccencrypt.ccbraintree(
|
|
173
|
+
client_token="eyJ2ZXJzaW9uIjoy...",
|
|
174
|
+
card="4242424242424242",
|
|
175
|
+
month="12",
|
|
176
|
+
year="2029",
|
|
177
|
+
cvv="123",
|
|
178
|
+
name="John Smith",
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Shopify
|
|
183
|
+
|
|
184
|
+
`domain` is the store's domain, without `https://` or `www.`.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
session_id = ccencrypt.ccshopify(
|
|
188
|
+
domain="example.com.au",
|
|
189
|
+
card="4242424242424242",
|
|
190
|
+
month="12",
|
|
191
|
+
year="2029",
|
|
192
|
+
cvv="123",
|
|
193
|
+
name="John Smith",
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The returned id goes into the checkout's payment as the card `sessionId`.
|
|
198
|
+
|
|
199
|
+
### PayDock
|
|
200
|
+
|
|
201
|
+
`public_key` is the site's PayDock public key, sent as `x-user-public-key` in the site's requests to `api.paydock.com`.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
token = ccencrypt.ccpaydock(
|
|
205
|
+
public_key="5b12b8af...",
|
|
206
|
+
card="4242424242424242",
|
|
207
|
+
month="12",
|
|
208
|
+
year="2029",
|
|
209
|
+
cvv="123",
|
|
210
|
+
name="John Smith",
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
If the site's token request includes a `gateway_id`, pass it as `gateway_id`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
CCencrypt.egg-info/PKG-INFO
|
|
4
|
+
CCencrypt.egg-info/SOURCES.txt
|
|
5
|
+
CCencrypt.egg-info/dependency_links.txt
|
|
6
|
+
CCencrypt.egg-info/requires.txt
|
|
7
|
+
CCencrypt.egg-info/top_level.txt
|
|
8
|
+
ccencrypt/__init__.py
|
|
9
|
+
ccencrypt/adyen.py
|
|
10
|
+
ccencrypt/adyen_cse.py
|
|
11
|
+
ccencrypt/braintree.py
|
|
12
|
+
ccencrypt/cybersource.py
|
|
13
|
+
ccencrypt/eway.py
|
|
14
|
+
ccencrypt/paydock.py
|
|
15
|
+
ccencrypt/shopify.py
|
|
16
|
+
ccencrypt/stripe.py
|
|
17
|
+
ccencrypt/helpers/__init__.py
|
|
18
|
+
ccencrypt/helpers/card.py
|
|
19
|
+
ccencrypt/helpers/encoding.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ccencrypt
|
ccencrypt-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: CCencrypt
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Card encryption and tokenisation for Adyen, CyberSource, eWay, Stripe, Braintree, Shopify and PayDock
|
|
5
|
+
Author: itsregza
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/itsregza/CCencrypt
|
|
8
|
+
Project-URL: Repository, https://github.com/itsregza/CCencrypt
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: cryptography>=42.0.0
|
|
12
|
+
Requires-Dist: requests>=2.28.0
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="https://raw.githubusercontent.com/itsregza/CCencrypt/main/assets/banner.svg" alt="CCencrypt" width="520">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">Card encryption and tokenisation for Python.</p>
|
|
19
|
+
|
|
20
|
+
| Processor | Function | Type | Returns |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| Adyen | `ccadyen` | Encrypt | dict of 4 encrypted fields |
|
|
23
|
+
| CyberSource Flex | `cccybersource` / `cccybersource_token` | Encrypt / Tokenise | JWE / transient token |
|
|
24
|
+
| eWay | `cceway` / `cceway_card` | Encrypt | `eCrypted:...` strings |
|
|
25
|
+
| Stripe | `ccstripe` | Tokenise | `pm_...` payment method id |
|
|
26
|
+
| Braintree | `ccbraintree` | Tokenise | `tokencc_...` nonce |
|
|
27
|
+
| Shopify | `ccshopify` | Tokenise | card session id |
|
|
28
|
+
| PayDock | `ccpaydock` | Tokenise | one-time card token |
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ccencrypt
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Card details
|
|
37
|
+
|
|
38
|
+
Every function cleans the card details before using them, so any of these work:
|
|
39
|
+
|
|
40
|
+
| Field | Accepted |
|
|
41
|
+
|---|---|
|
|
42
|
+
| card | `"4242424242424242"`, `"4242 4242 4242 4242"`, `"4242-4242-4242-4242"` |
|
|
43
|
+
| month | `1`, `"1"`, `"01"` |
|
|
44
|
+
| year | `29`, `"29"`, `2029`, `"2029"` |
|
|
45
|
+
|
|
46
|
+
Month and year are always separate arguments. If you have `"01/29"`, split it first:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
month, year = "01/29".split("/")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Proxies
|
|
53
|
+
|
|
54
|
+
You can pass a proxy for the process. Some processors like paydock will ratelimit so its important to use proxies:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
token = ccencrypt.ccpaydock(public_key, card, month, year, cvv, name, proxy="http://user:pass@host:port")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Errors
|
|
61
|
+
|
|
62
|
+
Tokenise functions raise `RuntimeError` with the status code and response body when the processor rejects the request.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Encryption
|
|
67
|
+
|
|
68
|
+
### Adyen
|
|
69
|
+
|
|
70
|
+
`encrypt_key` is the site's Adyen public key (`10001|A532...`).
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import ccencrypt
|
|
74
|
+
|
|
75
|
+
result = ccencrypt.ccadyen(
|
|
76
|
+
encrypt_key="10001|A532...",
|
|
77
|
+
card="4242424242424242",
|
|
78
|
+
month="12",
|
|
79
|
+
year="2029",
|
|
80
|
+
cvc="123",
|
|
81
|
+
client_key="live_...",
|
|
82
|
+
domain="https://example.com",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
result["encryptedCardNumber"]
|
|
86
|
+
result["encryptedExpiryMonth"]
|
|
87
|
+
result["encryptedExpiryYear"]
|
|
88
|
+
result["encryptedSecurityCode"]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`client_key` and `domain` are optional, but pass the site's own values when you have them.
|
|
92
|
+
|
|
93
|
+
`version` defaults to `"4.5.0"` (Secured Fields). For sites on legacy encryption (`adyenjs_0_1_XX$...`), pass `"0_1_18"`, `"0_1_22"`, `"0_1_23"`, `"0_1_24"` or `"0_1_25"`. Legacy versions ignore `client_key` and `domain`.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
result = ccencrypt.ccadyen(encrypt_key, card, month, year, cvc, version="0_1_25")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To encrypt a custom legacy payload such as `holderName`:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from ccencrypt.adyen import parse_public_key
|
|
103
|
+
|
|
104
|
+
blob = ccencrypt.cse_encrypt(
|
|
105
|
+
{"holderName": "John Smith", "generationtime": "2029-01-01T00:00:00.000Z"},
|
|
106
|
+
parse_public_key("10001|A532..."),
|
|
107
|
+
version="0_1_25",
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### CyberSource Flex
|
|
112
|
+
|
|
113
|
+
`context` is the capture context JWT from the site's CyberSource init endpoint.
|
|
114
|
+
|
|
115
|
+
Encrypt and exchange for a transient token in one call:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
token = ccencrypt.cccybersource_token(
|
|
119
|
+
card="4242424242424242",
|
|
120
|
+
month="12",
|
|
121
|
+
year="2029",
|
|
122
|
+
cvv="123",
|
|
123
|
+
context=context,
|
|
124
|
+
origin="https://www.example.com",
|
|
125
|
+
user_agent="Mozilla/5.0 ...",
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
For Microform sites, pass `microform=True` instead of `origin`.
|
|
130
|
+
|
|
131
|
+
To encrypt only, use `cccybersource` with the same card arguments and `context`. It returns a JWE that you POST yourself to `https://flex.cybersource.com/flex/v2/tokens` as `application/jwt`.
|
|
132
|
+
|
|
133
|
+
### eWay
|
|
134
|
+
|
|
135
|
+
`encrypt_key` is the `data-eway-encrypt-key` value from the site's payment form.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
fields = ccencrypt.cceway_card(encrypt_key, card="4242424242424242", cvv="123")
|
|
139
|
+
|
|
140
|
+
fields["EWAY_CARDNUMBER"]
|
|
141
|
+
fields["EWAY_CARDCVN"]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
To encrypt a single value, use `ccencrypt.cceway(encrypt_key, value)`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Tokenisation
|
|
149
|
+
|
|
150
|
+
### Stripe
|
|
151
|
+
|
|
152
|
+
`key` is the site's publishable key (`pk_live_...`), sent as `key=` in the site's requests to `api.stripe.com`.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
pm = ccencrypt.ccstripe(
|
|
156
|
+
key="pk_live_...",
|
|
157
|
+
card="4242424242424242",
|
|
158
|
+
month="12",
|
|
159
|
+
year="2029",
|
|
160
|
+
cvc="123",
|
|
161
|
+
name="John Smith",
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
If the site's requests also send `_stripe_account=acct_...`, pass it as `account`.
|
|
166
|
+
|
|
167
|
+
### Braintree
|
|
168
|
+
|
|
169
|
+
`client_token` is the base64 client token the site hands to Braintree. A raw `authorizationFingerprint` also works.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
nonce = ccencrypt.ccbraintree(
|
|
173
|
+
client_token="eyJ2ZXJzaW9uIjoy...",
|
|
174
|
+
card="4242424242424242",
|
|
175
|
+
month="12",
|
|
176
|
+
year="2029",
|
|
177
|
+
cvv="123",
|
|
178
|
+
name="John Smith",
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Shopify
|
|
183
|
+
|
|
184
|
+
`domain` is the store's domain, without `https://` or `www.`.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
session_id = ccencrypt.ccshopify(
|
|
188
|
+
domain="example.com.au",
|
|
189
|
+
card="4242424242424242",
|
|
190
|
+
month="12",
|
|
191
|
+
year="2029",
|
|
192
|
+
cvv="123",
|
|
193
|
+
name="John Smith",
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The returned id goes into the checkout's payment as the card `sessionId`.
|
|
198
|
+
|
|
199
|
+
### PayDock
|
|
200
|
+
|
|
201
|
+
`public_key` is the site's PayDock public key, sent as `x-user-public-key` in the site's requests to `api.paydock.com`.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
token = ccencrypt.ccpaydock(
|
|
205
|
+
public_key="5b12b8af...",
|
|
206
|
+
card="4242424242424242",
|
|
207
|
+
month="12",
|
|
208
|
+
year="2029",
|
|
209
|
+
cvv="123",
|
|
210
|
+
name="John Smith",
|
|
211
|
+
)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
If the site's token request includes a `gateway_id`, pass it as `gateway_id`.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/itsregza/CCencrypt/main/assets/banner.svg" alt="CCencrypt" width="520">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">Card encryption and tokenisation for Python.</p>
|
|
6
|
+
|
|
7
|
+
| Processor | Function | Type | Returns |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| Adyen | `ccadyen` | Encrypt | dict of 4 encrypted fields |
|
|
10
|
+
| CyberSource Flex | `cccybersource` / `cccybersource_token` | Encrypt / Tokenise | JWE / transient token |
|
|
11
|
+
| eWay | `cceway` / `cceway_card` | Encrypt | `eCrypted:...` strings |
|
|
12
|
+
| Stripe | `ccstripe` | Tokenise | `pm_...` payment method id |
|
|
13
|
+
| Braintree | `ccbraintree` | Tokenise | `tokencc_...` nonce |
|
|
14
|
+
| Shopify | `ccshopify` | Tokenise | card session id |
|
|
15
|
+
| PayDock | `ccpaydock` | Tokenise | one-time card token |
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install ccencrypt
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Card details
|
|
24
|
+
|
|
25
|
+
Every function cleans the card details before using them, so any of these work:
|
|
26
|
+
|
|
27
|
+
| Field | Accepted |
|
|
28
|
+
|---|---|
|
|
29
|
+
| card | `"4242424242424242"`, `"4242 4242 4242 4242"`, `"4242-4242-4242-4242"` |
|
|
30
|
+
| month | `1`, `"1"`, `"01"` |
|
|
31
|
+
| year | `29`, `"29"`, `2029`, `"2029"` |
|
|
32
|
+
|
|
33
|
+
Month and year are always separate arguments. If you have `"01/29"`, split it first:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
month, year = "01/29".split("/")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Proxies
|
|
40
|
+
|
|
41
|
+
You can pass a proxy for the process. Some processors like paydock will ratelimit so its important to use proxies:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
token = ccencrypt.ccpaydock(public_key, card, month, year, cvv, name, proxy="http://user:pass@host:port")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Errors
|
|
48
|
+
|
|
49
|
+
Tokenise functions raise `RuntimeError` with the status code and response body when the processor rejects the request.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Encryption
|
|
54
|
+
|
|
55
|
+
### Adyen
|
|
56
|
+
|
|
57
|
+
`encrypt_key` is the site's Adyen public key (`10001|A532...`).
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import ccencrypt
|
|
61
|
+
|
|
62
|
+
result = ccencrypt.ccadyen(
|
|
63
|
+
encrypt_key="10001|A532...",
|
|
64
|
+
card="4242424242424242",
|
|
65
|
+
month="12",
|
|
66
|
+
year="2029",
|
|
67
|
+
cvc="123",
|
|
68
|
+
client_key="live_...",
|
|
69
|
+
domain="https://example.com",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
result["encryptedCardNumber"]
|
|
73
|
+
result["encryptedExpiryMonth"]
|
|
74
|
+
result["encryptedExpiryYear"]
|
|
75
|
+
result["encryptedSecurityCode"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`client_key` and `domain` are optional, but pass the site's own values when you have them.
|
|
79
|
+
|
|
80
|
+
`version` defaults to `"4.5.0"` (Secured Fields). For sites on legacy encryption (`adyenjs_0_1_XX$...`), pass `"0_1_18"`, `"0_1_22"`, `"0_1_23"`, `"0_1_24"` or `"0_1_25"`. Legacy versions ignore `client_key` and `domain`.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
result = ccencrypt.ccadyen(encrypt_key, card, month, year, cvc, version="0_1_25")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To encrypt a custom legacy payload such as `holderName`:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from ccencrypt.adyen import parse_public_key
|
|
90
|
+
|
|
91
|
+
blob = ccencrypt.cse_encrypt(
|
|
92
|
+
{"holderName": "John Smith", "generationtime": "2029-01-01T00:00:00.000Z"},
|
|
93
|
+
parse_public_key("10001|A532..."),
|
|
94
|
+
version="0_1_25",
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### CyberSource Flex
|
|
99
|
+
|
|
100
|
+
`context` is the capture context JWT from the site's CyberSource init endpoint.
|
|
101
|
+
|
|
102
|
+
Encrypt and exchange for a transient token in one call:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
token = ccencrypt.cccybersource_token(
|
|
106
|
+
card="4242424242424242",
|
|
107
|
+
month="12",
|
|
108
|
+
year="2029",
|
|
109
|
+
cvv="123",
|
|
110
|
+
context=context,
|
|
111
|
+
origin="https://www.example.com",
|
|
112
|
+
user_agent="Mozilla/5.0 ...",
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For Microform sites, pass `microform=True` instead of `origin`.
|
|
117
|
+
|
|
118
|
+
To encrypt only, use `cccybersource` with the same card arguments and `context`. It returns a JWE that you POST yourself to `https://flex.cybersource.com/flex/v2/tokens` as `application/jwt`.
|
|
119
|
+
|
|
120
|
+
### eWay
|
|
121
|
+
|
|
122
|
+
`encrypt_key` is the `data-eway-encrypt-key` value from the site's payment form.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
fields = ccencrypt.cceway_card(encrypt_key, card="4242424242424242", cvv="123")
|
|
126
|
+
|
|
127
|
+
fields["EWAY_CARDNUMBER"]
|
|
128
|
+
fields["EWAY_CARDCVN"]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To encrypt a single value, use `ccencrypt.cceway(encrypt_key, value)`.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Tokenisation
|
|
136
|
+
|
|
137
|
+
### Stripe
|
|
138
|
+
|
|
139
|
+
`key` is the site's publishable key (`pk_live_...`), sent as `key=` in the site's requests to `api.stripe.com`.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
pm = ccencrypt.ccstripe(
|
|
143
|
+
key="pk_live_...",
|
|
144
|
+
card="4242424242424242",
|
|
145
|
+
month="12",
|
|
146
|
+
year="2029",
|
|
147
|
+
cvc="123",
|
|
148
|
+
name="John Smith",
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If the site's requests also send `_stripe_account=acct_...`, pass it as `account`.
|
|
153
|
+
|
|
154
|
+
### Braintree
|
|
155
|
+
|
|
156
|
+
`client_token` is the base64 client token the site hands to Braintree. A raw `authorizationFingerprint` also works.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
nonce = ccencrypt.ccbraintree(
|
|
160
|
+
client_token="eyJ2ZXJzaW9uIjoy...",
|
|
161
|
+
card="4242424242424242",
|
|
162
|
+
month="12",
|
|
163
|
+
year="2029",
|
|
164
|
+
cvv="123",
|
|
165
|
+
name="John Smith",
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Shopify
|
|
170
|
+
|
|
171
|
+
`domain` is the store's domain, without `https://` or `www.`.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
session_id = ccencrypt.ccshopify(
|
|
175
|
+
domain="example.com.au",
|
|
176
|
+
card="4242424242424242",
|
|
177
|
+
month="12",
|
|
178
|
+
year="2029",
|
|
179
|
+
cvv="123",
|
|
180
|
+
name="John Smith",
|
|
181
|
+
)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The returned id goes into the checkout's payment as the card `sessionId`.
|
|
185
|
+
|
|
186
|
+
### PayDock
|
|
187
|
+
|
|
188
|
+
`public_key` is the site's PayDock public key, sent as `x-user-public-key` in the site's requests to `api.paydock.com`.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
token = ccencrypt.ccpaydock(
|
|
192
|
+
public_key="5b12b8af...",
|
|
193
|
+
card="4242424242424242",
|
|
194
|
+
month="12",
|
|
195
|
+
year="2029",
|
|
196
|
+
cvv="123",
|
|
197
|
+
name="John Smith",
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
If the site's token request includes a `gateway_id`, pass it as `gateway_id`.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from ccencrypt.adyen import ccadyen, ccadyen_cse
|
|
2
|
+
from ccencrypt.adyen_cse import CSE_VERSIONS, cse_encrypt
|
|
3
|
+
from ccencrypt.braintree import ccbraintree
|
|
4
|
+
from ccencrypt.cybersource import CardTypes, card_type, cccybersource, cccybersource_token
|
|
5
|
+
from ccencrypt.eway import cceway, cceway_card
|
|
6
|
+
from ccencrypt.paydock import ccpaydock
|
|
7
|
+
from ccencrypt.shopify import ccshopify
|
|
8
|
+
from ccencrypt.stripe import ccstripe
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ccadyen",
|
|
12
|
+
"ccadyen_cse",
|
|
13
|
+
"cse_encrypt",
|
|
14
|
+
"CSE_VERSIONS",
|
|
15
|
+
"cccybersource",
|
|
16
|
+
"cccybersource_token",
|
|
17
|
+
"cceway",
|
|
18
|
+
"cceway_card",
|
|
19
|
+
"ccstripe",
|
|
20
|
+
"ccbraintree",
|
|
21
|
+
"ccshopify",
|
|
22
|
+
"ccpaydock",
|
|
23
|
+
"CardTypes",
|
|
24
|
+
"card_type",
|
|
25
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from cryptography.hazmat.backends import default_backend
|
|
7
|
+
from cryptography.hazmat.primitives import hashes, hmac
|
|
8
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
9
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
10
|
+
|
|
11
|
+
from ccencrypt.adyen_cse import cse_encrypt
|
|
12
|
+
from ccencrypt.helpers.card import clean_card, expiry
|
|
13
|
+
from ccencrypt.helpers.encoding import b64url, hex_to_bytes, pack_bit_length
|
|
14
|
+
|
|
15
|
+
OAEP = padding.OAEP(
|
|
16
|
+
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
|
17
|
+
algorithm=hashes.SHA1(),
|
|
18
|
+
label=None,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def utc_stamp():
|
|
23
|
+
dt = datetime.now(timezone.utc)
|
|
24
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(dt.microsecond / 1000):03d}Z"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_public_key(encrypt_key):
|
|
28
|
+
parts = encrypt_key.split("|", 1)
|
|
29
|
+
if len(parts) != 2:
|
|
30
|
+
raise ValueError("malformed encrypt key")
|
|
31
|
+
e = int.from_bytes(hex_to_bytes(parts[0]), "big")
|
|
32
|
+
n = int.from_bytes(hex_to_bytes(parts[1]), "big")
|
|
33
|
+
return rsa.RSAPublicNumbers(e, n).public_key(default_backend())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def jwe_encrypt(payload, public_key):
|
|
37
|
+
dek = os.urandom(64)
|
|
38
|
+
wrapped_key = public_key.encrypt(dek, OAEP)
|
|
39
|
+
|
|
40
|
+
header_json = json.dumps(
|
|
41
|
+
{"alg": "RSA-OAEP", "enc": "A256CBC-HS512", "version": "1"},
|
|
42
|
+
separators=(",", ":"),
|
|
43
|
+
)
|
|
44
|
+
header_b64 = b64url(header_json.encode("utf-8"))
|
|
45
|
+
header_raw = header_b64.encode("utf-8")
|
|
46
|
+
|
|
47
|
+
iv = os.urandom(16)
|
|
48
|
+
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
49
|
+
pad = 16 - (len(body) % 16)
|
|
50
|
+
body = body + bytes([pad]) * pad
|
|
51
|
+
|
|
52
|
+
enc = Cipher(algorithms.AES(dek[32:]), modes.CBC(iv), backend=default_backend()).encryptor()
|
|
53
|
+
ciphertext = enc.update(body) + enc.finalize()
|
|
54
|
+
|
|
55
|
+
mac = hmac.HMAC(dek[:32], hashes.SHA512(), backend=default_backend())
|
|
56
|
+
mac.update(header_raw + iv + ciphertext + pack_bit_length(len(header_raw)))
|
|
57
|
+
auth_tag = mac.finalize()[:32]
|
|
58
|
+
|
|
59
|
+
return ".".join([header_b64, b64url(wrapped_key), b64url(iv), b64url(ciphertext), b64url(auth_tag)])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def secured_fields_referrer(client_key, domain):
|
|
63
|
+
d = base64.b64encode(domain.encode("utf-8")).decode("ascii")
|
|
64
|
+
return (
|
|
65
|
+
"https://checkoutshopper-live.adyen.com/checkoutshopper/securedfields/"
|
|
66
|
+
f"{client_key}/4.5.0/securedFields.html?type=card&d={d}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def format_pan(card):
|
|
71
|
+
digits = clean_card(card)
|
|
72
|
+
return " ".join(digits[i : i + 4] for i in range(0, len(digits), 4))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_card_payloads(card, month, year, cvc, referrer, ts):
|
|
76
|
+
pan = format_pan(card)
|
|
77
|
+
month, year = expiry(month, year)
|
|
78
|
+
cvc = str(cvc)
|
|
79
|
+
return {
|
|
80
|
+
"encryptedCardNumber": {
|
|
81
|
+
"number": pan,
|
|
82
|
+
"generationtime": ts,
|
|
83
|
+
"numberBind": "1",
|
|
84
|
+
"activate": "3",
|
|
85
|
+
"referrer": referrer,
|
|
86
|
+
"numberFieldFocusCount": "3",
|
|
87
|
+
"numberFieldLog": "fo@44070,cl@44071,KN@44082,fo@44324,cl@44325,cl@44333,KN@44346,KN@44347,KN@44348,KN@44350,KN@44351,KN@44353,KN@44354,KN@44355,KN@44356,KN@44358,fo@44431,cl@44432,KN@44434,KN@44436,KN@44438,KN@44440,KN@44440",
|
|
88
|
+
"numberFieldClickCount": "4",
|
|
89
|
+
"numberFieldKeyCount": "16",
|
|
90
|
+
},
|
|
91
|
+
"encryptedExpiryMonth": {"expiryMonth": month, "generationtime": ts},
|
|
92
|
+
"encryptedExpiryYear": {"expiryYear": year, "generationtime": ts},
|
|
93
|
+
"encryptedSecurityCode": {
|
|
94
|
+
"cvc": cvc,
|
|
95
|
+
"generationtime": ts,
|
|
96
|
+
"cvcBind": "1",
|
|
97
|
+
"activate": "4",
|
|
98
|
+
"referrer": referrer,
|
|
99
|
+
"cvcFieldFocusCount": "4",
|
|
100
|
+
"cvcFieldLog": "fo@122,cl@123,KN@136,KN@138,KN@140,fo@11204,cl@11205,ch@11221,bl@11221,fo@33384,bl@33384,fo@50318,cl@50319,cl@50321,KN@50334,KN@50336,KN@50336",
|
|
101
|
+
"cvcFieldClickCount": "4",
|
|
102
|
+
"cvcFieldKeyCount": "6",
|
|
103
|
+
"cvcFieldChangeCount": "1",
|
|
104
|
+
"cvcFieldBlurCount": "2",
|
|
105
|
+
"deactivate": "2",
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def ccadyen(
|
|
111
|
+
encrypt_key,
|
|
112
|
+
card,
|
|
113
|
+
month,
|
|
114
|
+
year,
|
|
115
|
+
cvc,
|
|
116
|
+
client_key=None,
|
|
117
|
+
domain=None,
|
|
118
|
+
version="4.5.0",
|
|
119
|
+
):
|
|
120
|
+
if not card or not month or not year or not cvc:
|
|
121
|
+
raise ValueError("missing card details")
|
|
122
|
+
if not encrypt_key:
|
|
123
|
+
raise ValueError("missing encrypt key")
|
|
124
|
+
if str(version).strip() != "4.5.0":
|
|
125
|
+
return ccadyen_cse(encrypt_key, card, month, year, cvc, version=version)
|
|
126
|
+
if not client_key:
|
|
127
|
+
client_key = "live_SQV7J3WVMNFK3HU33BTUNF5AH4E2L2ZN"
|
|
128
|
+
if not domain:
|
|
129
|
+
domain = "https://minisoshop.co.uk"
|
|
130
|
+
|
|
131
|
+
rsa_key = parse_public_key(encrypt_key)
|
|
132
|
+
referrer = secured_fields_referrer(client_key, domain)
|
|
133
|
+
ts = utc_stamp()
|
|
134
|
+
fields = build_card_payloads(card, month, year, cvc, referrer, ts)
|
|
135
|
+
|
|
136
|
+
return {name: jwe_encrypt(data, rsa_key) for name, data in fields.items()}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def ccadyen_cse(encrypt_key, card, month, year, cvc, version="0_1_25"):
|
|
140
|
+
if not card or not month or not year or not cvc:
|
|
141
|
+
raise ValueError("missing card details")
|
|
142
|
+
if not encrypt_key:
|
|
143
|
+
raise ValueError("missing encrypt key")
|
|
144
|
+
|
|
145
|
+
rsa_key = parse_public_key(encrypt_key)
|
|
146
|
+
ts = utc_stamp()
|
|
147
|
+
month, year = expiry(month, year)
|
|
148
|
+
fields = {
|
|
149
|
+
"encryptedCardNumber": {"number": format_pan(card), "generationtime": ts},
|
|
150
|
+
"encryptedExpiryMonth": {"expiryMonth": month, "generationtime": ts},
|
|
151
|
+
"encryptedExpiryYear": {"expiryYear": year, "generationtime": ts},
|
|
152
|
+
"encryptedSecurityCode": {"cvc": str(cvc), "generationtime": ts},
|
|
153
|
+
}
|
|
154
|
+
return {name: cse_encrypt(data, rsa_key, version) for name, data in fields.items()}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
6
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESCCM
|
|
7
|
+
|
|
8
|
+
CSE_VERSIONS = ("0_1_18", "0_1_22", "0_1_23", "0_1_24", "0_1_25")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def normalize_cse_version(version):
|
|
12
|
+
v = str(version).strip().lower().replace("adyenjs_", "").replace(".", "_")
|
|
13
|
+
if v.isdigit():
|
|
14
|
+
v = f"0_1_{v}"
|
|
15
|
+
if v not in CSE_VERSIONS:
|
|
16
|
+
raise ValueError(f"unsupported adyen cse version: {version!r}")
|
|
17
|
+
return v
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cse_extra_fields(version):
|
|
21
|
+
# Fields the adyen-encrypt JS lib appends to every payload for this version.
|
|
22
|
+
if version == "0_1_18":
|
|
23
|
+
return {"initializeCount": "1", "dfValue": ""}
|
|
24
|
+
if version == "0_1_25":
|
|
25
|
+
return {"initializeCount": "1", "sjclStrength": "10"}
|
|
26
|
+
return {"initializeCount": "1", "sjclStrength": "10", "dfValue": ""}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cse_encrypt(data, public_key, version="0_1_25"):
|
|
30
|
+
version = normalize_cse_version(version)
|
|
31
|
+
payload = dict(data)
|
|
32
|
+
payload.update(cse_extra_fields(version))
|
|
33
|
+
plaintext = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
34
|
+
|
|
35
|
+
aes_key = os.urandom(32)
|
|
36
|
+
iv = os.urandom(12)
|
|
37
|
+
cipher = AESCCM(aes_key, tag_length=8).encrypt(iv, plaintext, None)
|
|
38
|
+
wrapped_key = public_key.encrypt(aes_key, padding.PKCS1v15())
|
|
39
|
+
|
|
40
|
+
return "".join([
|
|
41
|
+
f"adyenjs_{version}$",
|
|
42
|
+
base64.b64encode(wrapped_key).decode("ascii"),
|
|
43
|
+
"$",
|
|
44
|
+
base64.b64encode(iv + cipher).decode("ascii"),
|
|
45
|
+
])
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from ccencrypt.helpers.card import clean_card, expiry, proxies
|
|
8
|
+
|
|
9
|
+
BRAINTREE_GQL_URL = "https://payments.braintree-api.com/graphql"
|
|
10
|
+
|
|
11
|
+
TOKENIZE_QUERY = (
|
|
12
|
+
"mutation TokenizeCreditCard($input: TokenizeCreditCardInput!) { "
|
|
13
|
+
"tokenizeCreditCard(input: $input) { token creditCard { bin brandCode last4 } } }"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fingerprint(client_token):
|
|
18
|
+
try:
|
|
19
|
+
data = json.loads(base64.b64decode(client_token))
|
|
20
|
+
except Exception:
|
|
21
|
+
return client_token
|
|
22
|
+
return data["authorizationFingerprint"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ccbraintree(client_token, card, month, year, cvv, name="", proxy=None, timeout=30):
|
|
26
|
+
month, year = expiry(month, year)
|
|
27
|
+
payload = {
|
|
28
|
+
"clientSdkMetadata": {
|
|
29
|
+
"source": "client",
|
|
30
|
+
"integration": "custom",
|
|
31
|
+
"sessionId": str(uuid.uuid4()),
|
|
32
|
+
},
|
|
33
|
+
"query": TOKENIZE_QUERY,
|
|
34
|
+
"variables": {
|
|
35
|
+
"input": {
|
|
36
|
+
"creditCard": {
|
|
37
|
+
"number": clean_card(card),
|
|
38
|
+
"expirationMonth": month,
|
|
39
|
+
"expirationYear": year,
|
|
40
|
+
"cvv": str(cvv).strip(),
|
|
41
|
+
"cardholderName": name,
|
|
42
|
+
},
|
|
43
|
+
"options": {"validate": False},
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"operationName": "TokenizeCreditCard",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resp = requests.post(
|
|
50
|
+
BRAINTREE_GQL_URL,
|
|
51
|
+
headers={
|
|
52
|
+
"authorization": f"Bearer {fingerprint(client_token)}",
|
|
53
|
+
"braintree-version": "2018-05-10",
|
|
54
|
+
"content-type": "application/json",
|
|
55
|
+
"origin": "https://assets.braintreegateway.com",
|
|
56
|
+
"referer": "https://assets.braintreegateway.com/",
|
|
57
|
+
},
|
|
58
|
+
json=payload,
|
|
59
|
+
proxies=proxies(proxy),
|
|
60
|
+
timeout=timeout,
|
|
61
|
+
)
|
|
62
|
+
body = resp.json() if resp.text else {}
|
|
63
|
+
token = ((body.get("data") or {}).get("tokenizeCreditCard") or {}).get("token")
|
|
64
|
+
if resp.status_code != 200 or not token:
|
|
65
|
+
raise RuntimeError(f"braintree failed: {resp.status_code} {resp.text[:300]}")
|
|
66
|
+
return token
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import urllib.parse
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from cryptography.hazmat.backends import default_backend
|
|
8
|
+
from cryptography.hazmat.primitives import hashes
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
10
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
11
|
+
|
|
12
|
+
from ccencrypt.helpers.card import clean_card, expiry, proxies
|
|
13
|
+
from ccencrypt.helpers.encoding import b64url, b64url_decode, jwt_payload
|
|
14
|
+
|
|
15
|
+
FLEX_TOKENS_URL = "https://flex.cybersource.com/flex/v2/tokens"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CardTypes = {
|
|
19
|
+
"Visa": "001",
|
|
20
|
+
"MasterCard": "002",
|
|
21
|
+
"AmericanExpress": "003",
|
|
22
|
+
"Discover": "004",
|
|
23
|
+
"Diners": "005",
|
|
24
|
+
"JCB": "007",
|
|
25
|
+
"Maestro": "042",
|
|
26
|
+
"ChinaUnionPay": "062",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def card_type(number):
|
|
31
|
+
n = clean_card(number)
|
|
32
|
+
if n.startswith("5"):
|
|
33
|
+
return CardTypes["MasterCard"]
|
|
34
|
+
if n.startswith("34") or n.startswith("37"):
|
|
35
|
+
return CardTypes["AmericanExpress"]
|
|
36
|
+
return CardTypes["Visa"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def jwk_rsa_key(jwk):
|
|
40
|
+
e = int.from_bytes(b64url_decode(jwk["e"]), "big")
|
|
41
|
+
n = int.from_bytes(b64url_decode(jwk["n"]), "big")
|
|
42
|
+
return rsa.RSAPublicNumbers(e, n).public_key(default_backend())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cccybersource(card, month, year, cvv, context, index=0):
|
|
46
|
+
key_id = jwt_payload(context)
|
|
47
|
+
jwk = key_id["flx"]["jwk"]
|
|
48
|
+
header = {
|
|
49
|
+
"kid": jwk["kid"],
|
|
50
|
+
"alg": "RSA-OAEP",
|
|
51
|
+
"enc": "A256GCM",
|
|
52
|
+
}
|
|
53
|
+
month, year = expiry(month, year)
|
|
54
|
+
payload = {
|
|
55
|
+
"data": {
|
|
56
|
+
"number": clean_card(card),
|
|
57
|
+
"securityCode": str(cvv),
|
|
58
|
+
"expirationMonth": month,
|
|
59
|
+
"expirationYear": year,
|
|
60
|
+
"type": card_type(card),
|
|
61
|
+
},
|
|
62
|
+
"context": context,
|
|
63
|
+
"index": index,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
iv = os.urandom(12)
|
|
67
|
+
aes_key = AESGCM.generate_key(bit_length=256)
|
|
68
|
+
header_b64 = b64url(json.dumps(header, separators=(",", ":")))
|
|
69
|
+
aad = header_b64.encode("utf-8")
|
|
70
|
+
plaintext = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
71
|
+
encrypted = AESGCM(aes_key).encrypt(iv, plaintext, aad)
|
|
72
|
+
|
|
73
|
+
tag_len = 16
|
|
74
|
+
body = encrypted[:-tag_len]
|
|
75
|
+
tag = encrypted[-tag_len:]
|
|
76
|
+
|
|
77
|
+
wrapped = jwk_rsa_key(jwk).encrypt(
|
|
78
|
+
aes_key,
|
|
79
|
+
padding.OAEP(
|
|
80
|
+
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
|
81
|
+
algorithm=hashes.SHA1(),
|
|
82
|
+
label=None,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return ".".join(
|
|
87
|
+
[
|
|
88
|
+
header_b64,
|
|
89
|
+
b64url(wrapped),
|
|
90
|
+
b64url(iv),
|
|
91
|
+
b64url(body),
|
|
92
|
+
b64url(tag),
|
|
93
|
+
]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def microform_referer(context):
|
|
98
|
+
kid = jwt_payload(context)["flx"]["jwk"]["kid"]
|
|
99
|
+
q = urllib.parse.quote(kid, safe="")
|
|
100
|
+
return f"https://flex.cybersource.com/microform/bundle/v2.9.0/iframe.html?keyId={q}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def cccybersource_token(
|
|
104
|
+
card,
|
|
105
|
+
month,
|
|
106
|
+
year,
|
|
107
|
+
cvv,
|
|
108
|
+
context,
|
|
109
|
+
origin=None,
|
|
110
|
+
referer=None,
|
|
111
|
+
user_agent=None,
|
|
112
|
+
microform=False,
|
|
113
|
+
index=0,
|
|
114
|
+
proxy=None,
|
|
115
|
+
timeout=30,
|
|
116
|
+
):
|
|
117
|
+
encrypted = cccybersource(card, month, year, cvv, context, index=index)
|
|
118
|
+
|
|
119
|
+
if microform:
|
|
120
|
+
headers = {
|
|
121
|
+
"content-type": "application/jwt; charset=UTF-8",
|
|
122
|
+
"origin": "https://flex.cybersource.com",
|
|
123
|
+
"referer": referer or microform_referer(context),
|
|
124
|
+
}
|
|
125
|
+
else:
|
|
126
|
+
if not origin:
|
|
127
|
+
raise ValueError("origin required unless microform=True")
|
|
128
|
+
site = origin.rstrip("/")
|
|
129
|
+
headers = {
|
|
130
|
+
"content-type": "application/jwt",
|
|
131
|
+
"origin": site,
|
|
132
|
+
"referer": referer or f"{site}/",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if user_agent:
|
|
136
|
+
headers["user-agent"] = user_agent
|
|
137
|
+
|
|
138
|
+
resp = requests.post(
|
|
139
|
+
FLEX_TOKENS_URL,
|
|
140
|
+
headers=headers,
|
|
141
|
+
data=str(encrypted).strip(),
|
|
142
|
+
proxies=proxies(proxy),
|
|
143
|
+
timeout=timeout,
|
|
144
|
+
)
|
|
145
|
+
if resp.status_code != 201:
|
|
146
|
+
raise RuntimeError(f"flex /tokens failed: {resp.status_code} {(resp.text or '')[:300]}")
|
|
147
|
+
token = (resp.text or "").strip()
|
|
148
|
+
if not token:
|
|
149
|
+
raise RuntimeError("flex /tokens returned empty body")
|
|
150
|
+
return token
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
3
|
+
from cryptography.hazmat.backends import default_backend
|
|
4
|
+
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
|
5
|
+
|
|
6
|
+
from ccencrypt.helpers.card import clean_card
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def modulus_from_key_blob(key_blob):
|
|
10
|
+
try:
|
|
11
|
+
mod_bytes = base64.b64decode(key_blob)
|
|
12
|
+
except Exception as exc:
|
|
13
|
+
raise ValueError("invalid encrypt key") from exc
|
|
14
|
+
n = int.from_bytes(mod_bytes, "big")
|
|
15
|
+
return rsa.RSAPublicNumbers(65537, n).public_key(default_backend())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def cceway(encrypt_key, value):
|
|
19
|
+
if not encrypt_key:
|
|
20
|
+
raise ValueError("missing encrypt key")
|
|
21
|
+
if value is None or str(value).strip() == "":
|
|
22
|
+
raise ValueError("missing value")
|
|
23
|
+
|
|
24
|
+
pub = modulus_from_key_blob(str(encrypt_key).strip())
|
|
25
|
+
ciphertext = pub.encrypt(str(value).encode("utf-8"), padding.PKCS1v15())
|
|
26
|
+
return "eCrypted:" + base64.b64encode(ciphertext).decode("ascii")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def cceway_card(encrypt_key, card, cvv):
|
|
30
|
+
pan = clean_card(card)
|
|
31
|
+
code = str(cvv).strip()
|
|
32
|
+
return {
|
|
33
|
+
"EWAY_CARDNUMBER": cceway(encrypt_key, pan),
|
|
34
|
+
"EWAY_CARDCVN": cceway(encrypt_key, code),
|
|
35
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
def clean_card(card):
|
|
2
|
+
return str(card).strip().replace(" ", "").replace("-", "")
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def spaced_card(card):
|
|
6
|
+
n = clean_card(card)
|
|
7
|
+
if len(n) == 15:
|
|
8
|
+
return f"{n[:4]} {n[4:10]} {n[10:]}"
|
|
9
|
+
return " ".join(n[i : i + 4] for i in range(0, len(n), 4))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def proxies(proxy):
|
|
13
|
+
return {"http": proxy, "https": proxy} if proxy else None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def expiry(month, year):
|
|
17
|
+
m = "".join(c for c in str(month) if c.isdigit()).zfill(2)[-2:]
|
|
18
|
+
y = "".join(c for c in str(year) if c.isdigit())
|
|
19
|
+
if len(y) == 2:
|
|
20
|
+
y = "20" + y
|
|
21
|
+
return m, y
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def b64url(data):
|
|
6
|
+
if isinstance(data, str):
|
|
7
|
+
data = data.encode("utf-8")
|
|
8
|
+
return base64.b64encode(data).decode("ascii").rstrip("=").replace("+", "-").replace("/", "_")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def hex_to_bytes(hex_str):
|
|
12
|
+
if not hex_str:
|
|
13
|
+
return b""
|
|
14
|
+
if len(hex_str) % 2 == 1:
|
|
15
|
+
hex_str = "0" + hex_str
|
|
16
|
+
return bytes.fromhex(hex_str)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def b64url_decode(s):
|
|
20
|
+
s = s.replace("-", "+").replace("_", "/")
|
|
21
|
+
pad = "=" * ((4 - len(s) % 4) % 4)
|
|
22
|
+
return base64.b64decode(s + pad)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def jwt_payload(token):
|
|
26
|
+
part = token.split(".")[1]
|
|
27
|
+
return json.loads(b64url_decode(part))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def pack_bit_length(length_bytes):
|
|
31
|
+
bits = length_bytes * 8
|
|
32
|
+
hi = bits // (2**32)
|
|
33
|
+
lo = bits % (2**32)
|
|
34
|
+
return hi.to_bytes(4, "big") + lo.to_bytes(4, "big")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from ccencrypt.helpers.card import clean_card, expiry, proxies
|
|
4
|
+
|
|
5
|
+
PAYDOCK_TOKENS_URL = "https://api.paydock.com/v1/payment_sources/tokens"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ccpaydock(public_key, card, month, year, cvv, name, gateway_id="", proxy=None, timeout=30):
|
|
9
|
+
month, year = expiry(month, year)
|
|
10
|
+
payload = {
|
|
11
|
+
"type": "card",
|
|
12
|
+
"card_name": name,
|
|
13
|
+
"card_number": clean_card(card),
|
|
14
|
+
"expire_month": month,
|
|
15
|
+
"expire_year": year[-2:],
|
|
16
|
+
"card_ccv": str(cvv).strip(),
|
|
17
|
+
"gateway_id": gateway_id,
|
|
18
|
+
"store_ccv": True,
|
|
19
|
+
"meta": {},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
resp = requests.post(
|
|
23
|
+
PAYDOCK_TOKENS_URL,
|
|
24
|
+
headers={
|
|
25
|
+
"accept": "application/json, text/plain, */*",
|
|
26
|
+
"content-type": "application/json",
|
|
27
|
+
"origin": "https://widget.paydock.com",
|
|
28
|
+
"referer": "https://widget.paydock.com/",
|
|
29
|
+
"x-sdk-type": "client",
|
|
30
|
+
"x-sdk-version": "v1.131.0",
|
|
31
|
+
"x-user-public-key": public_key,
|
|
32
|
+
},
|
|
33
|
+
json=payload,
|
|
34
|
+
proxies=proxies(proxy),
|
|
35
|
+
timeout=timeout,
|
|
36
|
+
)
|
|
37
|
+
body = resp.json() if resp.text else {}
|
|
38
|
+
token = (body.get("resource") or {}).get("data")
|
|
39
|
+
if resp.status_code != 201 or not token:
|
|
40
|
+
raise RuntimeError(f"paydock failed: {resp.status_code} {resp.text[:300]}")
|
|
41
|
+
return token
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from ccencrypt.helpers.card import expiry, proxies, spaced_card
|
|
4
|
+
|
|
5
|
+
SHOPIFY_PCI_URL = "https://checkout.pci.shopifyinc.com/sessions"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ccshopify(domain, card, month, year, cvv, name, proxy=None, timeout=30):
|
|
9
|
+
month, year = expiry(month, year)
|
|
10
|
+
payload = {
|
|
11
|
+
"credit_card": {
|
|
12
|
+
"number": spaced_card(card),
|
|
13
|
+
"month": int(month),
|
|
14
|
+
"year": int(year),
|
|
15
|
+
"verification_value": str(cvv).strip(),
|
|
16
|
+
"start_month": None,
|
|
17
|
+
"start_year": None,
|
|
18
|
+
"issue_number": "",
|
|
19
|
+
"name": name,
|
|
20
|
+
},
|
|
21
|
+
"payment_session_scope": domain.replace("https://", "").replace("www.", "").strip("/"),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
resp = requests.post(
|
|
25
|
+
SHOPIFY_PCI_URL,
|
|
26
|
+
headers={
|
|
27
|
+
"accept": "application/json",
|
|
28
|
+
"content-type": "application/json",
|
|
29
|
+
"origin": "https://checkout.pci.shopifyinc.com",
|
|
30
|
+
"referer": "https://checkout.pci.shopifyinc.com/",
|
|
31
|
+
},
|
|
32
|
+
json=payload,
|
|
33
|
+
proxies=proxies(proxy),
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
)
|
|
36
|
+
body = resp.json() if resp.text else {}
|
|
37
|
+
session_id = body.get("id")
|
|
38
|
+
if resp.status_code != 200 or not session_id:
|
|
39
|
+
raise RuntimeError(f"shopify failed: {resp.status_code} {resp.text[:300]}")
|
|
40
|
+
return session_id
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from ccencrypt.helpers.card import clean_card, expiry, proxies
|
|
6
|
+
|
|
7
|
+
STRIPE_PM_URL = "https://api.stripe.com/v1/payment_methods"
|
|
8
|
+
STRIPE_JS = "stripe.js/e2a5fc9c3e; stripe-js-v3/e2a5fc9c3e; card-element"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ccstripe(key, card, month, year, cvc, name="", account=None, proxy=None, timeout=30):
|
|
12
|
+
month, year = expiry(month, year)
|
|
13
|
+
data = {
|
|
14
|
+
"type": "card",
|
|
15
|
+
"card[number]": clean_card(card),
|
|
16
|
+
"card[cvc]": str(cvc).strip(),
|
|
17
|
+
"card[exp_month]": month,
|
|
18
|
+
"card[exp_year]": year[-2:],
|
|
19
|
+
"payment_user_agent": STRIPE_JS,
|
|
20
|
+
"guid": str(uuid.uuid4()),
|
|
21
|
+
"muid": str(uuid.uuid4()),
|
|
22
|
+
"sid": str(uuid.uuid4()),
|
|
23
|
+
"key": key,
|
|
24
|
+
}
|
|
25
|
+
if name:
|
|
26
|
+
data["billing_details[name]"] = name
|
|
27
|
+
if account:
|
|
28
|
+
data["_stripe_account"] = account
|
|
29
|
+
|
|
30
|
+
resp = requests.post(
|
|
31
|
+
STRIPE_PM_URL,
|
|
32
|
+
headers={
|
|
33
|
+
"accept": "application/json",
|
|
34
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
35
|
+
"origin": "https://js.stripe.com",
|
|
36
|
+
"referer": "https://js.stripe.com/",
|
|
37
|
+
},
|
|
38
|
+
data=data,
|
|
39
|
+
proxies=proxies(proxy),
|
|
40
|
+
timeout=timeout,
|
|
41
|
+
)
|
|
42
|
+
body = resp.json() if resp.text else {}
|
|
43
|
+
pm = body.get("id", "")
|
|
44
|
+
if resp.status_code != 200 or not pm.startswith("pm_"):
|
|
45
|
+
err = body.get("error") or {}
|
|
46
|
+
raise RuntimeError(f"stripe failed: {resp.status_code} {err.get('message') or resp.text[:300]}")
|
|
47
|
+
return pm
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "CCencrypt"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Card encryption and tokenisation for Adyen, CyberSource, eWay, Stripe, Braintree, Shopify and PayDock"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "itsregza" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"cryptography>=42.0.0",
|
|
15
|
+
"requests>=2.28.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/itsregza/CCencrypt"
|
|
20
|
+
Repository = "https://github.com/itsregza/CCencrypt"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["."]
|
|
24
|
+
include = ["ccencrypt*"]
|