-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 318efe8
Showing
172 changed files
with
49,448 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
.idea/ | ||
__pycache__/ | ||
venv/ | ||
*.pyc | ||
*~ | ||
service.log | ||
private.pem | ||
public.pem | ||
main.db |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# Cryto Ready Web Framework (CRWF) | ||
|
||
## Core features | ||
* Ready to accept crypto payments for users. | ||
* Minimal maintenance. | ||
* Easy scalable. | ||
* User ready with login, register, account activation by email, password forgot and password reset. | ||
* Recaptcha v2 protected forms. | ||
* Basic app abuse control at IP level for register, password forgot and maximum requests. | ||
|
||
The minimal maintenance is achieved by using as python core immutable features as possible. | ||
|
||
--- | ||
##### Requirements | ||
* Python3.7 or greater | ||
* Crypto IPN service https://github.com/Binarch00/crypto_gateway | ||
|
||
Abuse control by IP could end using proxy IP if proxies are used. | ||
|
||
--- | ||
|
||
##### Python libs setup | ||
Python 3.7 or greater required | ||
```shell script | ||
python3.7 -m venv venv | ||
. ./venv/bim/activate | ||
pip install -r requirements.txt | ||
``` | ||
|
||
##### For setup the initial database | ||
`PYTHONPATH=./ python database/models.py` | ||
|
||
##### For run unittests | ||
`./run_tests.sh` | ||
|
||
##### Run the web app | ||
|
||
`PYTHONPATH=./ python webapp/run.py` | ||
|
||
--- | ||
Internal Services | ||
--- | ||
|
||
#### Crypto Wallets Generator (Required Service) | ||
|
||
For security reasons, the users wallets are generated outside the users register process. | ||
This way the web app will never know how decode the wallets private keys. | ||
|
||
##### Before the webapp consume all unused address, call the address generator by | ||
`PYTHONPATH=./ python services/btc_address_generator.py` | ||
|
||
Security Details: | ||
* Run the service outside the webapp server | ||
* Setup secure remote database access to web app database | ||
* Remove all decryption keys at web app deploy. | ||
|
||
--- | ||
|
||
How To Deploy | ||
--- | ||
|
||
#### TODO ! | ||
|
||
. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import redis | ||
import json | ||
|
||
from settings import CACHE | ||
|
||
|
||
def get_redis(db=0): | ||
return redis.Redis(host=CACHE["host"], port=CACHE["port"], db=db) | ||
|
||
|
||
redis_con = get_redis() | ||
|
||
|
||
def get_object(key): | ||
global redis_con | ||
data = redis_con.get(key) | ||
return json.loads(data) if data else {} | ||
|
||
|
||
def set_object(key, data, ttl=600): | ||
global redis_con | ||
if type(data) is not dict: | ||
raise ValueError("Dict object required as data") | ||
return redis_con.set(key, json.dumps(data), ex=ttl) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import unittest | ||
import cache | ||
|
||
|
||
class TestCacheMethods(unittest.TestCase): | ||
|
||
def test_set_get_object(self): | ||
data = { | ||
"A": 1, | ||
"B": 2 | ||
} | ||
cache.set_object("test1", data) | ||
self.assertEqual(cache.get_object("test1"), data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
""" | ||
crypto gateway IPN client | ||
https://github.com/Binarch00/crypto_gateway | ||
""" | ||
|
||
import requests | ||
from settings import CRYPTO_GATEWAY_SERVER, logger, SERVER_NAME | ||
|
||
|
||
# TODO: move to https | ||
def subscribe(address, coin="btc"): | ||
try: | ||
url = "http://{}/btc_ipn".format(CRYPTO_GATEWAY_SERVER) | ||
data = { | ||
"address": address, | ||
"max_confirms": 1, | ||
"url": "http://{}/ipn".format(SERVER_NAME) | ||
} | ||
res = requests.post(url=url, data=data) | ||
if res.content.strip().count(b"success") >= 1: | ||
return True | ||
else: | ||
logger.error("Address {} subscription fail: {}".format(address, res.content[:30])) | ||
except Exception as ex: | ||
logger.exception(ex) | ||
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import random | ||
import string | ||
import hashlib | ||
|
||
|
||
def random_string(length): | ||
letters = string.ascii_letters | ||
return ''.join(random.choice(letters) for i in range(length)) | ||
|
||
|
||
def hash_maker(secret, salt=""): | ||
result = (secret + salt).encode('utf-8') | ||
return hashlib.sha1(result).hexdigest() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import cache | ||
import database | ||
from settings import logger | ||
from database.models import UserTransactions, CryptoAddress | ||
|
||
|
||
class CachedObject: | ||
db_table = "none" | ||
db_id = "id" | ||
TTL = 600 | ||
|
||
def __init__(self, id): | ||
self.id = id | ||
self._load_cache() | ||
if not self._data: | ||
self._load_database(refresh=True) | ||
|
||
def refresh_data(self): | ||
"""Update the cache with fresh database info""" | ||
self._load_database(refresh=True) | ||
|
||
def _load_database(self, refresh=False): | ||
try: | ||
session = database.Session() | ||
query = 'SELECT * FROM {} WHERE {} = :val LIMIT 1'.format(self.db_table, self.db_id) | ||
result = session.execute(query, {'val': self.id}) | ||
self._data = dict(result.first().items()) | ||
if refresh: | ||
if self._data: | ||
key = "cached/{}/{}".format(self.db_table, self.id) | ||
cache.set_object(key, self._data, ttl=self.TTL) | ||
else: | ||
raise ValueError("Invalid user id: {}".format(self.id)) | ||
|
||
except Exception as ex: | ||
logger.exception(ex) | ||
|
||
def _load_cache(self): | ||
key = "cached/{}/{}".format(self.db_table, self.id) | ||
cdata = cache.get_object(key) | ||
if cdata: | ||
self._data = cdata | ||
else: | ||
self._data = {} | ||
|
||
def __getattr__(self, item): | ||
return self._data.get(item) | ||
|
||
|
||
class UserCH(CachedObject): | ||
db_table = "users" | ||
|
||
def btc_balance(self): | ||
key = "user-netbalance/{}".format(self.id) | ||
net_balance = 0.0 | ||
try: | ||
net_balance = float(cache.redis_con.get(key)) | ||
except TypeError: | ||
net_balance = UserTransactions.get_user_netbalance(self.id) | ||
cache.redis_con.set(key, net_balance, 10) | ||
return net_balance | ||
|
||
def btc_address(self): | ||
return CryptoAddress.get_address_by_user(self.id) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import smtplib | ||
|
||
from email.mime.multipart import MIMEMultipart | ||
from email.mime.text import MIMEText | ||
import re | ||
from settings import logger, SMTP | ||
|
||
|
||
def send_email(from_, to_, subject, body_text, body_html): | ||
if not from_: | ||
from_ = SMTP["default_sender"] | ||
if not re.match(r"[^@]+@[^@]+\.[^@]+", from_) or not re.match(r"[^@]+@[^@]+\.[^@]+", to_): | ||
logger.error("Invalid email address from {} -- to {}".format(from_, to_)) | ||
return False | ||
|
||
# Create message container - the correct MIME type is multipart/alternative. | ||
msg = MIMEMultipart('alternative') | ||
msg['Subject'] = subject | ||
msg['From'] = from_ | ||
msg['To'] = to_ | ||
|
||
# Record the MIME types of both parts - text/plain and text/html. | ||
parts = [] | ||
if body_text: | ||
parts.append(MIMEText(body_text, 'plain')) | ||
if body_html: | ||
parts.append(MIMEText(body_html, 'html')) | ||
|
||
for part in parts: | ||
msg.attach(part) | ||
|
||
logger.warning("EMAIL: " + msg.as_string()) | ||
return True | ||
|
||
server = smtplib.SMTP_SSL(SMTP["host"], SMTP["port"]) | ||
server.ehlo() | ||
server.login(SMTP["user"], SMTP["password"]) | ||
server.sendmail(from_, to_, msg.as_string()) | ||
server.close() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import unittest | ||
from core.utils.cached_objects import UserCH | ||
import cache | ||
import database | ||
from database.models import User | ||
|
||
|
||
|
||
class TestCachedObjectsMethods(unittest.TestCase): | ||
|
||
def setUp(self) -> None: | ||
self.session = database.Session() | ||
self.session.query(User).filter_by(email='test@test.com').delete() | ||
self.session.commit() | ||
|
||
def test_user(self): | ||
User.add_user("test@test.com", "any") | ||
our_item = self.session.query(User).filter_by(email='test@test.com').first() | ||
usr = UserCH(our_item.id) | ||
self.assertEqual(usr.email, "test@test.com") | ||
self.assertEqual(cache.get_object("cached/{}/{}".format(usr.db_table, usr.id)), usr._data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
from Crypto.PublicKey import RSA | ||
from Crypto.Random import get_random_bytes | ||
from Crypto.Cipher import AES, PKCS1_OAEP | ||
|
||
|
||
class RSATools: | ||
|
||
@staticmethod | ||
def gen_keys(size=2048, passphrase="", prv_file="private.pem", pub_file="public.pem"): | ||
key = RSA.generate(size) | ||
private_key = key.export_key(passphrase=passphrase) | ||
file_out = open(prv_file, "wb") | ||
file_out.write(private_key) | ||
file_out.flush() | ||
file_out.close() | ||
|
||
public_key = key.publickey().export_key() | ||
file_out = open(pub_file, "wb") | ||
file_out.write(public_key) | ||
file_out.flush() | ||
file_out.close() | ||
|
||
def decrypt(self, data): | ||
data = bytearray(data) | ||
|
||
enc_session_key = data[:self.prv_key.size_in_bytes()] | ||
data = data[self.prv_key.size_in_bytes():] | ||
nonce = data[:16] | ||
data = data[16:] | ||
tag = data[:16] | ||
ciphertext = data[16:] | ||
|
||
# Decrypt the session key with the private RSA key | ||
cipher_rsa = PKCS1_OAEP.new(self.prv_key) | ||
session_key = cipher_rsa.decrypt(enc_session_key) | ||
|
||
# Decrypt the data with the AES session key | ||
cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce) | ||
data = cipher_aes.decrypt_and_verify(ciphertext, tag) | ||
return data.decode("utf-8") | ||
|
||
def encrypt(self, data): | ||
data = data.encode("utf-8") | ||
session_key = get_random_bytes(16) | ||
data_out = [] | ||
|
||
# Encrypt the session key with the public RSA key | ||
cipher_rsa = PKCS1_OAEP.new(self.pub_key) | ||
enc_session_key = cipher_rsa.encrypt(session_key) | ||
|
||
# Encrypt the data with the AES session key | ||
cipher_aes = AES.new(session_key, AES.MODE_EAX) | ||
ciphertext, tag = cipher_aes.encrypt_and_digest(data) | ||
for x in (enc_session_key, cipher_aes.nonce, tag, ciphertext): | ||
data_out += x | ||
return bytes(data_out) | ||
|
||
@staticmethod | ||
def _load_key(key_file, passphrase=""): | ||
if key_file: | ||
with open(key_file) as fl: | ||
return RSA.import_key(fl.read(), passphrase=passphrase) | ||
|
||
def __init__(self, pub_key=None, prv_key=None, passphrase=""): | ||
self.pub_key = self._load_key(pub_key, passphrase) | ||
self.prv_key = self._load_key(prv_key, passphrase) | ||
self.passphrase = passphrase | ||
|
||
|
||
if __name__ == "__main__": | ||
# RSATools.gen_keys(size=4028, passphrase="1234567890") | ||
rc = RSATools("public.pem", "private.pem", passphrase="1234567890") | ||
enc = rc.encrypt("Hello Word") | ||
print(enc) | ||
print("_"+rc.decrypt(enc)+"_") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import os, binascii, hashlib, base58, ecdsa | ||
import requests | ||
import cache | ||
|
||
|
||
def ripemd160(x): | ||
d = hashlib.new('ripemd160') | ||
d.update(x) | ||
return d | ||
|
||
|
||
def gen_btc_keys(): | ||
# generate private key , uncompressed WIF starts with "5" | ||
priv_key = os.urandom(32) | ||
fullkey = '80' + binascii.hexlify(priv_key).decode() | ||
sha256a = hashlib.sha256(binascii.unhexlify(fullkey)).hexdigest() | ||
sha256b = hashlib.sha256(binascii.unhexlify(sha256a)).hexdigest() | ||
WIF = base58.b58encode(binascii.unhexlify(fullkey + sha256b[:8])) | ||
|
||
# get public key , uncompressed address starts with "1" | ||
sk = ecdsa.SigningKey.from_string(priv_key, curve=ecdsa.SECP256k1) | ||
vk = sk.get_verifying_key() | ||
publ_key = '04' + binascii.hexlify(vk.to_string()).decode() | ||
hash160 = ripemd160(hashlib.sha256(binascii.unhexlify(publ_key)).digest()).digest() | ||
publ_addr_a = b"\x00" + hash160 | ||
checksum = hashlib.sha256(hashlib.sha256(publ_addr_a).digest()).digest()[:4] | ||
publ_addr_b = base58.b58encode(publ_addr_a + checksum) | ||
return { | ||
"private": WIF.decode(), | ||
"public": publ_addr_b.decode() | ||
} |
Oops, something went wrong.