Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Binarch00 committed Apr 13, 2020
0 parents commit 318efe8
Show file tree
Hide file tree
Showing 172 changed files with 49,448 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
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
64 changes: 64 additions & 0 deletions README.md
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 !

.
24 changes: 24 additions & 0 deletions cache/__init__.py
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)
13 changes: 13 additions & 0 deletions cache/tests.py
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)
1 change: 1 addition & 0 deletions core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

26 changes: 26 additions & 0 deletions core/crypto_ipn_client.py
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
13 changes: 13 additions & 0 deletions core/utils/__init__.py
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()
64 changes: 64 additions & 0 deletions core/utils/cached_objects.py
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)
39 changes: 39 additions & 0 deletions core/utils/emails.py
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()
21 changes: 21 additions & 0 deletions core/utils/tests.py
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)
75 changes: 75 additions & 0 deletions crypto/__init__.py
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)+"_")
31 changes: 31 additions & 0 deletions crypto/btc/__init__.py
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()
}
Loading

0 comments on commit 318efe8

Please sign in to comment.