From 6cffa1c0ca455d06f01d6d0cb999d749c4644247 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jul 2019 03:31:57 +0200 Subject: [PATCH 001/483] Change maxstdio using ctypes as win32file module is not included with Python3 by default --- src/util/Platform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/Platform.py b/src/util/Platform.py index 788e5949..74613302 100644 --- a/src/util/Platform.py +++ b/src/util/Platform.py @@ -5,11 +5,11 @@ import logging def setMaxfilesopened(limit): try: if sys.platform == "win32": - import win32file - maxstdio = win32file._getmaxstdio() + import ctypes + maxstdio = ctypes.cdll.msvcr100._getmaxstdio() if maxstdio < limit: logging.debug("Current maxstdio: %s, changing to %s..." % (maxstdio, limit)) - win32file._setmaxstdio(limit) + ctypes.cdll.msvcr100._setmaxstdio(limit) return True else: import resource From e4888410311a3b61b19de4950785c2733b91f78a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jul 2019 03:32:22 +0200 Subject: [PATCH 002/483] Display loaded verify lib path in benchmark --- plugins/Stats/StatsPlugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index 6fb182ae..ad46ef7f 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -564,6 +564,11 @@ class UiRequestPlugin(object): try: CryptBitcoin.loadLib(lib_verify) loaded = True + if lib_verify == "openssl": + yield "+ Loaded lib: %s
" % html.escape(str(CryptBitcoin.bitcoin.core.key._ssl)) + elif lib_verify == "libsecp256k1": + import coincurve + yield "+ Loaded lib: %s
" % type(coincurve._libsecp256k1.lib).__name__ except Exception as err: yield "- Error loading %s: %s
" % (lib_verify, err) loaded = False @@ -702,7 +707,7 @@ class UiRequestPlugin(object): if u % 10 == 0: yield "." - yield " - Total rows in db: %s
" % db.execute("SELECT COUNT(*) AS num FROM test").fetchone()[0] + yield " + Total rows in db: %s
" % db.execute("SELECT COUNT(*) AS num FROM test").fetchone()[0] with benchmark("Indexed query x 1000", 0.25): found = 0 From 27fcb70774c76c304875b88b951abcdf3270236c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jul 2019 03:32:45 +0200 Subject: [PATCH 003/483] Log loaded verify lib path and load time --- src/Crypt/CryptBitcoin.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index a558e676..be0c1ba2 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -1,5 +1,6 @@ import logging import base64 +import time from util import OpensslFindPatch from lib import pybitcointools as btctools @@ -11,15 +12,25 @@ lib_verify_best = "btctools" def loadLib(lib_name): global bitcoin, libsecp256k1message, lib_verify_best if lib_name == "libsecp256k1": + s = time.time() from lib import libsecp256k1message + import coincurve lib_verify_best = "libsecp256k1" - logging.info("Libsecpk256k1 loaded") + logging.info( + "Libsecpk256k1 loaded: %s in %.3fs" % + (type(coincurve._libsecp256k1.lib).__name__, time.time() - s) + ) elif lib_name == "openssl": + s = time.time() import bitcoin.signmessage import bitcoin.core.key import bitcoin.wallet - logging.info("OpenSSL loaded, version: %.9X" % bitcoin.core.key._ssl.SSLeay()) + logging.info( + "OpenSSL loaded: %s, version: %.9X in %.3fs" % + (bitcoin.core.key._ssl, bitcoin.core.key._ssl.SSLeay(), time.time() - s) + ) + try: if not config.use_libsecp256k1: From c9a2b86c16b3e709bd8eb453672e864c70611d2b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jul 2019 03:33:35 +0200 Subject: [PATCH 004/483] Log possible OpenSSL cert generation error message at the same line --- src/Crypt/CryptConnection.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 86a40f5e..9d705671 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -29,6 +29,8 @@ class CryptConnectionManager: self.cert_csr = config.data_dir + "/cert-rsa.csr" self.key_pem = config.data_dir + "/key-rsa.pem" + self.log = logging.getLogger("CryptConnectionManager") + # Select crypt that supported by both sides # Return: Name of the crypto def selectCrypt(self, client_supported): @@ -107,18 +109,20 @@ class CryptConnectionManager: self.cacert_pem ) cmd = "%s req -new -newkey rsa:2048 -days 3650 -nodes -x509 -config %s -subj %s -keyout %s -out %s -batch" % cmd_params - logging.debug("Generating RSA CAcert and CAkey PEM files...") + self.log.debug("Generating RSA CAcert and CAkey PEM files...") + self.log.debug("Running: %s" % cmd) proc = subprocess.Popen( cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env ) back = proc.stdout.read().strip().decode().replace("\r", "") proc.wait() - logging.debug("%s\n%s" % (cmd, back)) if not (os.path.isfile(self.cacert_pem) and os.path.isfile(self.cakey_pem)): - logging.error("RSA ECC SSL CAcert generation failed, CAcert or CAkey files not exist.") + self.log.error("RSA ECC SSL CAcert generation failed, CAcert or CAkey files not exist. (%s)" % back) return False + else: + self.log.debug("Result: %s" % back) # Generate certificate key and signing request cmd_params = helper.shellquote( @@ -129,14 +133,14 @@ class CryptConnectionManager: self.openssl_env["OPENSSL_CONF"], ) cmd = "%s req -new -newkey rsa:2048 -keyout %s -out %s -subj %s -sha256 -nodes -batch -config %s" % cmd_params - logging.debug("Generating certificate key and signing request...") + self.log.debug("Generating certificate key and signing request...") proc = subprocess.Popen( cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env ) back = proc.stdout.read().strip().decode().replace("\r", "") proc.wait() - logging.debug("%s\n%s" % (cmd, back)) + self.log.debug("Running: %s\n%s" % (cmd, back)) # Sign request and generate certificate cmd_params = helper.shellquote( @@ -148,18 +152,18 @@ class CryptConnectionManager: self.openssl_env["OPENSSL_CONF"] ) cmd = "%s x509 -req -in %s -CA %s -CAkey %s -set_serial 01 -out %s -days 730 -sha256 -extensions x509_ext -extfile %s" % cmd_params - logging.debug("Generating RSA cert...") + self.log.debug("Generating RSA cert...") proc = subprocess.Popen( cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env ) back = proc.stdout.read().strip().decode().replace("\r", "") proc.wait() - logging.debug("%s\n%s" % (cmd, back)) + self.log.debug("Running: %s\n%s" % (cmd, back)) if os.path.isfile(self.cert_pem) and os.path.isfile(self.key_pem): return True else: - logging.error("RSA ECC SSL cert generation failed, cert or key files not exist.") + self.log.error("RSA ECC SSL cert generation failed, cert or key files not exist.") manager = CryptConnectionManager() From 902a1b1c88c4b07234c3a0f3545b064da7ccfe84 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jul 2019 03:33:56 +0200 Subject: [PATCH 005/483] Fix OpenSSL dll loading on Windows --- src/util/OpensslFindPatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index f5c3ace2..81f84c63 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -10,7 +10,7 @@ find_library_original = ctypes.util.find_library def getOpensslPath(): if sys.platform.startswith("win"): - lib_path = os.path.dirname(os.path.abspath(__file__)) + "/../../dist/openssl/libeay32.dll" + lib_path = os.path.join(os.getcwd(), "tools/openssl/libeay32.dll") elif sys.platform == "cygwin": lib_path = "/bin/cygcrypto-1.0.0.dll" elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX From d8a121cd0635e4d9f686cece6eac1a5f90a7159f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 18 Jul 2019 03:34:09 +0200 Subject: [PATCH 006/483] Rev4129 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 4613940b..ee6e939b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4126 + self.rev = 4129 self.argv = argv self.action = None self.pending_changes = {} From 5b91aef4ecb17f01343cebfa13eaac2b3e19ea1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=A0?= Date: Thu, 1 Aug 2019 19:16:10 +0200 Subject: [PATCH 007/483] Add response to some commands --- src/Ui/UiWebsocket.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 39012648..f964dd90 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -362,6 +362,8 @@ class UiWebsocket(object): if channel not in self.channels: self.channels.append(channel) + self.response(to, "ok") + # Server variables def actionServerInfo(self, to): back = self.formatServerInfo() @@ -878,6 +880,8 @@ class UiWebsocket(object): if self not in site.websockets: site.websockets.append(self) + self.response(to, "ok") + # Update site content.json def actionSiteUpdate(self, to, address, check_files=False, since=None, announce=False): def updateThread(): From 06406fa46c93a6a68cece738fca072fbc666ddba Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 14:04:18 +0200 Subject: [PATCH 008/483] Avoid bare exceptions --- plugins/Cors/CorsPlugin.py | 2 +- src/util/helper.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/Cors/CorsPlugin.py b/plugins/Cors/CorsPlugin.py index af501462..413f21f4 100644 --- a/plugins/Cors/CorsPlugin.py +++ b/plugins/Cors/CorsPlugin.py @@ -99,6 +99,6 @@ class UiRequestPlugin(object): site = self.server.sites[path_parts["address"]] try: path_parts["address"], path_parts["inner_path"] = getCorsPath(site, path_parts["inner_path"]) - except: + except Exception: return None return path_parts diff --git a/src/util/helper.py b/src/util/helper.py index 94a883f5..6e7c43b6 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -144,7 +144,7 @@ def getFilename(path): def getFilesize(path): try: s = os.stat(path) - except: + except Exception: return None if stat.S_ISREG(s.st_mode): # Test if it's file return s.st_size @@ -246,14 +246,14 @@ def isIp(ip): try: socket.inet_pton(socket.AF_INET6, ip) return True - except: + except Exception: return False else: # IPv4 try: socket.inet_aton(ip) return True - except: + except Exception: return False @@ -291,12 +291,12 @@ def getInterfaceIps(ip_type="ipv4"): s = createSocket(test_ip, sock_type=socket.SOCK_DGRAM) s.connect((test_ip, 1)) res.append(s.getsockname()[0]) - except: + except Exception: pass try: res += [ip[4][0] for ip in socket.getaddrinfo(socket.gethostname(), 1)] - except: + except Exception: pass res = [re.sub("%.*", "", ip) for ip in res if getIpType(ip) == ip_type and isIp(ip)] From 5e90cd9714de01e0db02f0cdf189730c94482c0d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 14:05:14 +0200 Subject: [PATCH 009/483] Move advanced json formatter to helper.py --- src/Site/SiteStorage.py | 27 +-------------------------- src/util/helper.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index f9e0ef25..81738e52 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -326,35 +326,10 @@ class SiteStorage(object): with self.open(inner_path, "r", encoding="utf8") as file: return json.load(file) - def formatJson(self, data): - content = json.dumps(data, indent=1, sort_keys=True) - - # Make it a little more compact by removing unnecessary white space - def compact_dict(match): - if "\n" in match.group(0): - return match.group(0).replace(match.group(1), match.group(1).strip()) - else: - return match.group(0) - - content = re.sub("\{(\n[^,\[\{]{10,100}?)\}[, ]{0,2}\n", compact_dict, content, flags=re.DOTALL) - - def compact_list(match): - if "\n" in match.group(0): - stripped_lines = re.sub("\n[ ]*", "", match.group(1)) - return match.group(0).replace(match.group(1), stripped_lines) - else: - return match.group(0) - - content = re.sub("\[([^\[\{]{2,300}?)\][, ]{0,2}\n", compact_list, content, flags=re.DOTALL) - - # Remove end of line whitespace - content = re.sub("(?m)[ ]+$", "", content) - return content - # Write formatted json file def writeJson(self, inner_path, data): # Write to disk - self.write(inner_path, self.formatJson(data).encode("utf8")) + self.write(inner_path, helper.jsonDumps(data).encode("utf8")) # Get file size def getSize(self, inner_path): diff --git a/src/util/helper.py b/src/util/helper.py index 6e7c43b6..8e0f285f 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -7,6 +7,8 @@ import collections import time import logging import base64 +import json + import gevent from Config import config @@ -37,6 +39,32 @@ def atomicWrite(dest, content, mode="wb"): return False +def jsonDumps(data): + content = json.dumps(data, indent=1, sort_keys=True) + + # Make it a little more compact by removing unnecessary white space + def compact_dict(match): + if "\n" in match.group(0): + return match.group(0).replace(match.group(1), match.group(1).strip()) + else: + return match.group(0) + + content = re.sub(r"\{(\n[^,\[\{]{10,100}?)\}[, ]{0,2}\n", compact_dict, content, flags=re.DOTALL) + + def compact_list(match): + if "\n" in match.group(0): + stripped_lines = re.sub("\n[ ]*", "", match.group(1)) + return match.group(0).replace(match.group(1), stripped_lines) + else: + return match.group(0) + + content = re.sub(r"\[([^\[\{]{2,300}?)\][, ]{0,2}\n", compact_list, content, flags=re.DOTALL) + + # Remove end of line whitespace + content = re.sub(r"(?m)[ ]+$", "", content) + return content + + def openLocked(path, mode="wb"): try: if os.name == "posix": From f6e06456b0659e6242bea5b9ac468babbde90e5c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 14:06:05 +0200 Subject: [PATCH 010/483] Use advaced json dumper to save sites.json and users.json --- src/Site/SiteManager.py | 2 +- src/User/User.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 40724866..9da2b867 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -102,7 +102,7 @@ class SiteManager(object): s = time.time() if data: - helper.atomicWrite("%s/sites.json" % config.data_dir, json.dumps(data, indent=2, sort_keys=True).encode()) + helper.atomicWrite("%s/sites.json" % config.data_dir, helper.jsonDumps(data).encode("utf8")) else: self.log.debug("Save error: No data") time_write = time.time() - s diff --git a/src/User/User.py b/src/User/User.py index 2557abc0..dbcfc56f 100644 --- a/src/User/User.py +++ b/src/User/User.py @@ -45,7 +45,7 @@ class User(object): user_data["sites"] = self.sites user_data["certs"] = self.certs user_data["settings"] = self.settings - helper.atomicWrite("%s/users.json" % config.data_dir, json.dumps(users, indent=2, sort_keys=True).encode("utf8")) + helper.atomicWrite("%s/users.json" % config.data_dir, helper.jsonDumps(users).encode("utf8")) self.log.debug("Saved in %.3fs" % (time.time() - s)) self.delayed_save_thread = None From 1eb97ea3816c51f16345c5c37de528a1d9abdfa1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 14:06:25 +0200 Subject: [PATCH 011/483] Delayed save of sites.json --- src/Site/Site.py | 2 +- src/Site/SiteManager.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 7b4f5ec3..967a776f 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -125,7 +125,7 @@ class Site(object): if not SiteManager.site_manager.sites.get(self.address): SiteManager.site_manager.sites[self.address] = self SiteManager.site_manager.load(False) - SiteManager.site_manager.save() + SiteManager.site_manager.saveDelayed() def isServing(self): if config.offline: diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 9da2b867..4af4e2d5 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -11,6 +11,7 @@ from Plugin import PluginManager from Content import ContentDb from Config import config from util import helper +from util import RateLimit @PluginManager.acceptPlugins @@ -82,6 +83,9 @@ class SiteManager(object): self.log.debug("SiteManager added %s sites" % added) self.loaded = True + def saveDelayed(self): + RateLimit.callAsync("Save sites.json", allowed_again=5, func=self.save) + def save(self, recalculate_size=False): if not self.sites: self.log.debug("Save skipped: No sites found") From 3e97c154a07a2a584bceaa70f9a047c593fb7fa9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:05:19 +0200 Subject: [PATCH 012/483] Remove hard-coded directory path from plugins --- plugins/ContentFilter/ContentFilterPlugin.py | 7 +++++-- plugins/Cors/CorsPlugin.py | 7 ++++++- plugins/MergerSite/MergerSitePlugin.py | 8 ++++++-- plugins/OptionalManager/UiWebsocketPlugin.py | 8 ++++++-- plugins/Sidebar/SidebarPlugin.py | 3 +-- plugins/Trayicon/TrayiconPlugin.py | 7 ++++--- plugins/UiConfig/UiConfigPlugin.py | 7 +++++-- plugins/disabled-UiPassword/UiPasswordPlugin.py | 9 ++++++--- 8 files changed, 39 insertions(+), 17 deletions(-) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index f6d74e7a..cd3a020b 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -2,6 +2,7 @@ import time import re import html import hashlib +import os from Plugin import PluginManager from Translate import Translate @@ -10,8 +11,10 @@ from Config import config from .ContentFilterStorage import ContentFilterStorage +plugin_dir = os.path.dirname(__file__) + if "_" not in locals(): - _ = Translate("plugins/ContentFilter/languages/") + _ = Translate(plugin_dir + "/languages/") @PluginManager.registerTo("SiteManager") @@ -210,7 +213,7 @@ class UiRequestPlugin(object): def actionUiMedia(self, path, *args, **kwargs): if path.startswith("/uimedia/plugins/contentfilter/"): - file_path = path.replace("/uimedia/plugins/contentfilter/", "plugins/ContentFilter/media/") + file_path = path.replace("/uimedia/plugins/contentfilter/", plugin_dir + "/media/") return self.actionFile(file_path) else: return super(UiRequestPlugin, self).actionUiMedia(path) diff --git a/plugins/Cors/CorsPlugin.py b/plugins/Cors/CorsPlugin.py index 413f21f4..fa9774c4 100644 --- a/plugins/Cors/CorsPlugin.py +++ b/plugins/Cors/CorsPlugin.py @@ -1,11 +1,16 @@ import re import html import copy +import os from Plugin import PluginManager from Translate import Translate + + +plugin_dir = os.path.dirname(__file__) + if "_" not in locals(): - _ = Translate("plugins/Cors/languages/") + _ = Translate(plugin_dir + "/languages/") def getCorsPath(site, inner_path): diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index 36c1dbae..77d83931 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -1,6 +1,7 @@ import re import time import copy +import os from Plugin import PluginManager from Translate import Translate @@ -18,8 +19,11 @@ if "merger_db" not in locals().keys(): # To keep merger_sites between module re merged_to_merger = {} # {address: [site1, site2, ...]} cache site_manager = None # Site manager for merger sites + +plugin_dir = os.path.dirname(__file__) + if "_" not in locals(): - _ = Translate("plugins/MergerSite/languages/") + _ = Translate(plugin_dir + "/languages/") # Check if the site has permission to this merger site @@ -221,7 +225,7 @@ class UiWebsocketPlugin(object): site = self.server.sites.get(address) try: merged_sites.append(site.content_manager.contents.get("content.json").get("title", address)) - except Exception as err: + except Exception: merged_sites.append(address) details = _["Read and write permissions to sites with merged type of %s "] % merger_type diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 626dc2e1..9c6ad742 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -1,6 +1,7 @@ import re import time import html +import os import gevent @@ -9,8 +10,11 @@ from Config import config from util import helper from Translate import Translate + +plugin_dir = os.path.dirname(__file__) + if "_" not in locals(): - _ = Translate("plugins/OptionalManager/languages/") + _ = Translate(plugin_dir + "/languages/") bigfile_sha512_cache = {} @@ -285,7 +289,7 @@ class UiWebsocketPlugin(object): def actionOptionalLimitSet(self, to, limit): if "ADMIN" not in self.site.settings["permissions"]: return self.response(to, {"error": "Forbidden"}) - config.optional_limit = re.sub("\.0+$", "", limit) # Remove unnecessary digits from end + config.optional_limit = re.sub(r"\.0+$", "", limit) # Remove unnecessary digits from end config.saveValue("optional_limit", limit) self.response(to, "ok") diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index dc6a9e9c..46219d8a 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -18,9 +18,8 @@ from Translate import Translate from util import helper from .ZipStream import ZipStream -plugin_dir = "plugins/Sidebar" +plugin_dir = os.path.dirname(__file__) media_dir = plugin_dir + "/media" -sys.path.append(plugin_dir) # To able to load geoip lib loc_cache = {} if "_" not in locals(): diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index 667a90e5..794ced77 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -8,8 +8,11 @@ from Translate import Translate allow_reload = False # No source reload supported in this plugin + +plugin_dir = os.path.dirname(__file__) + if "_" not in locals(): - _ = Translate("plugins/Trayicon/languages/") + _ = Translate(plugin_dir + "/languages/") @PluginManager.registerTo("Actions") @@ -23,8 +26,6 @@ class ActionsPlugin(object): self.main = main - fs_encoding = sys.getfilesystemencoding() - icon = notificationicon.NotificationIcon( os.path.join(os.path.dirname(os.path.abspath(__file__)), 'trayicon.ico'), "ZeroNet %s" % config.version diff --git a/plugins/UiConfig/UiConfigPlugin.py b/plugins/UiConfig/UiConfigPlugin.py index ae9e6950..fc71e28b 100644 --- a/plugins/UiConfig/UiConfigPlugin.py +++ b/plugins/UiConfig/UiConfigPlugin.py @@ -1,12 +1,15 @@ import io +import os from Plugin import PluginManager from Config import config from Translate import Translate +plugin_dir = os.path.dirname(__file__) + if "_" not in locals(): - _ = Translate("plugins/UiConfig/languages/") + _ = Translate(plugin_dir + "/languages/") @PluginManager.afterLoad @@ -35,7 +38,7 @@ class UiRequestPlugin(object): def actionUiMedia(self, path, *args, **kwargs): if path.startswith("/uimedia/plugins/uiconfig/"): - file_path = path.replace("/uimedia/plugins/uiconfig/", "plugins/UiConfig/media/") + file_path = path.replace("/uimedia/plugins/uiconfig/", plugin_dir + "/media/") if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")): # If debugging merge *.css to all.css and *.js to all.js from Debug import DebugMedia diff --git a/plugins/disabled-UiPassword/UiPasswordPlugin.py b/plugins/disabled-UiPassword/UiPasswordPlugin.py index 944804d7..1962d5e6 100644 --- a/plugins/disabled-UiPassword/UiPasswordPlugin.py +++ b/plugins/disabled-UiPassword/UiPasswordPlugin.py @@ -3,12 +3,15 @@ import random import time import json import re - +import os from Config import config from Plugin import PluginManager from util import helper + +plugin_dir = os.path.dirname(__file__) + if "sessions" not in locals().keys(): # To keep sessions between module reloads sessions = {} @@ -21,6 +24,7 @@ def showPasswordAdvice(password): error_msgs.append("You are using a very short UI password!") return error_msgs + @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): sessions = sessions @@ -45,7 +49,7 @@ class UiRequestPlugin(object): # Action: Login @helper.encodeResponse def actionLogin(self): - template = open("plugins/UiPassword/login.html").read() + template = open(plugin_dir + "/login.html").read() self.sendHeader() posted = self.getPosted() if posted: # Validate http posted data @@ -108,7 +112,6 @@ class UiRequestPlugin(object): yield "Error: Invalid session id" - @PluginManager.registerTo("ConfigPlugin") class ConfigPlugin(object): def createArguments(self): From be742c78e70026d00fcf933582724155b6065dbd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:06:22 +0200 Subject: [PATCH 013/483] Formatting for better readability --- plugins/ContentFilter/ContentFilterPlugin.py | 5 +++-- plugins/Cors/CorsPlugin.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index cd3a020b..0a45ceec 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -56,9 +56,10 @@ class UiWebsocketPlugin(object): if "ADMIN" in self.getPermissions(to): self.cbMuteRemove(to, auth_address) else: + cert_user_id = html.escape(filter_storage.file_content["mutes"][auth_address]["cert_user_id"]) self.cmd( "confirm", - [_["Unmute %s?"] % html.escape(filter_storage.file_content["mutes"][auth_address]["cert_user_id"]), _["Unmute"]], + [_["Unmute %s?"] % cert_user_id, _["Unmute"]], lambda res: self.cbMuteRemove(to, auth_address) ) @@ -180,7 +181,7 @@ class SiteStoragePlugin(object): @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): def actionWrapper(self, path, extra_headers=None): - match = re.match("/(?P
[A-Za-z0-9\._-]+)(?P/.*|$)", path) + match = re.match(r"/(?P
[A-Za-z0-9\._-]+)(?P/.*|$)", path) if not match: return False address = match.group("address") diff --git a/plugins/Cors/CorsPlugin.py b/plugins/Cors/CorsPlugin.py index fa9774c4..a26c5c2b 100644 --- a/plugins/Cors/CorsPlugin.py +++ b/plugins/Cors/CorsPlugin.py @@ -32,7 +32,11 @@ class UiWebsocketPlugin(object): if super(UiWebsocketPlugin, self).hasSitePermission(address, cmd=cmd): return True - if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in ["fileGet", "fileList", "dirList", "fileRules", "optionalFileInfo", "fileQuery", "dbQuery", "userGetSettings", "siteInfo"]: + allowed_commands = [ + "fileGet", "fileList", "dirList", "fileRules", "optionalFileInfo", + "fileQuery", "dbQuery", "userGetSettings", "siteInfo" + ] + if not "Cors:%s" % address in self.site.settings["permissions"] or cmd not in allowed_commands: return False else: return True From fbafd231770b1892ff771f28c4534eec5ef1b35d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:13:54 +0200 Subject: [PATCH 014/483] Add OpenSSL 1.1 support to CryptMessage plugin by using radfish's pyelliptic version --- plugins/CryptMessage/CryptMessage.py | 4 +- plugins/CryptMessage/CryptMessagePlugin.py | 4 +- src/lib/pyelliptic/LICENSE | 674 +++++++++++++++++++++ src/lib/pyelliptic/README.md | 96 +++ src/lib/pyelliptic/__init__.py | 19 + src/lib/pyelliptic/arithmetic.py | 144 +++++ src/lib/pyelliptic/cipher.py | 84 +++ src/lib/pyelliptic/ecc.py | 505 +++++++++++++++ src/lib/pyelliptic/hash.py | 69 +++ src/lib/pyelliptic/openssl.py | 551 +++++++++++++++++ src/lib/pyelliptic/setup.py | 23 + 11 files changed, 2169 insertions(+), 4 deletions(-) create mode 100644 src/lib/pyelliptic/LICENSE create mode 100644 src/lib/pyelliptic/README.md create mode 100644 src/lib/pyelliptic/__init__.py create mode 100644 src/lib/pyelliptic/arithmetic.py create mode 100644 src/lib/pyelliptic/cipher.py create mode 100644 src/lib/pyelliptic/ecc.py create mode 100644 src/lib/pyelliptic/hash.py create mode 100644 src/lib/pyelliptic/openssl.py create mode 100644 src/lib/pyelliptic/setup.py diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index 00c5d7c6..6f2cbdc2 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -7,7 +7,7 @@ ecc_cache = {} def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): - import pyelliptic + from lib import pyelliptic pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey)) curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey_openssl) if ephemcurve is None: @@ -34,7 +34,7 @@ def split(encrypted): def getEcc(privatekey=None): - import pyelliptic + from lib import pyelliptic global ecc_cache if privatekey not in ecc_cache: if privatekey: diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index 6cfb8fc5..64c8ac81 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -60,7 +60,7 @@ class UiWebsocketPlugin(object): # Encrypt a text using AES # Return: Iv, AES key, Encrypted text def actionAesEncrypt(self, to, text, key=None, iv=None): - import pyelliptic + from lib import pyelliptic if key: key = base64.b64decode(key) @@ -83,7 +83,7 @@ class UiWebsocketPlugin(object): # Decrypt a text using AES # Return: Decrypted text def actionAesDecrypt(self, to, *args): - import pyelliptic + from lib import pyelliptic if len(args) == 3: # Single decrypt encrypted_texts = [(args[0], args[1])] diff --git a/src/lib/pyelliptic/LICENSE b/src/lib/pyelliptic/LICENSE new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/src/lib/pyelliptic/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/src/lib/pyelliptic/README.md b/src/lib/pyelliptic/README.md new file mode 100644 index 00000000..3acf819c --- /dev/null +++ b/src/lib/pyelliptic/README.md @@ -0,0 +1,96 @@ +# PyElliptic + +PyElliptic is a high level wrapper for the cryptographic library : OpenSSL. +Under the GNU General Public License + +Python3 compatible. For GNU/Linux and Windows. +Require OpenSSL + +## Version + +The [upstream pyelliptic](https://github.com/yann2192/pyelliptic) has been +deprecated by the author at 1.5.8 and ECC API has been removed. + +This version is a fork of the pyelliptic extracted from the [BitMessage source +tree](https://github.com/Bitmessage/PyBitmessage), and does contain the ECC +API. To minimize confusion but to avoid renaming the module, major version has +been bumped. + +BitMessage is actively maintained, and this fork of pyelliptic will track and +incorporate any changes to pyelliptic from BitMessage. Ideally, in the future, +BitMessage would import this module as a dependency instead of maintaining a +copy of the source in its repository. + +The BitMessage fork forked from v1.3 of upstream pyelliptic. The commits in +this repository are the commits extracted from the BitMessage repository and +applied to pyelliptic v1.3 upstream repository (i.e. to the base of the fork), +so history with athorship is preserved. + +Some of the changes in upstream pyelliptic between 1.3 and 1.5.8 came from +BitMessage, those changes are present in this fork. Other changes do not exist +in this fork (they may be added in the future). + +Also, a few minor changes exist in this fork but is not (yet) present in +BitMessage source. See: + + git log 1.3-PyBitmessage-37489cf7feff8d5047f24baa8f6d27f353a6d6ac..HEAD + +## Features + +### Asymmetric cryptography using Elliptic Curve Cryptography (ECC) + +* Key agreement : ECDH +* Digital signatures : ECDSA +* Hybrid encryption : ECIES (like RSA) + +### Symmetric cryptography + +* AES-128 (CBC, OFB, CFB, CTR) +* AES-256 (CBC, OFB, CFB, CTR) +* Blowfish (CFB and CBC) +* RC4 + +### Other + +* CSPRNG +* HMAC (using SHA512) +* PBKDF2 (SHA256 and SHA512) + +## Example + +```python +#!/usr/bin/python + +import pyelliptic + +# Symmetric encryption +iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') +ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') + +ciphertext = ctx.update('test1') +ciphertext += ctx.update('test2') +ciphertext += ctx.final() + +ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') +print ctx2.ciphering(ciphertext) + +# Asymmetric encryption +alice = pyelliptic.ECC() # default curve: sect283r1 +bob = pyelliptic.ECC(curve='sect571r1') + +ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) +print bob.decrypt(ciphertext) + +signature = bob.sign("Hello Alice") +# alice's job : +print pyelliptic.ECC(pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") + +# ERROR !!! +try: + key = alice.get_ecdh_key(bob.get_pubkey()) +except: print("For ECDH key agreement, the keys must be defined on the same curve !") + +alice = pyelliptic.ECC(curve='sect571r1') +print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') +print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') +``` diff --git a/src/lib/pyelliptic/__init__.py b/src/lib/pyelliptic/__init__.py new file mode 100644 index 00000000..761d08af --- /dev/null +++ b/src/lib/pyelliptic/__init__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2010 +# Author: Yann GUIBET +# Contact: + +__version__ = '1.3' + +__all__ = [ + 'OpenSSL', + 'ECC', + 'Cipher', + 'hmac_sha256', + 'hmac_sha512', + 'pbkdf2' +] + +from .openssl import OpenSSL +from .ecc import ECC +from .cipher import Cipher +from .hash import hmac_sha256, hmac_sha512, pbkdf2 diff --git a/src/lib/pyelliptic/arithmetic.py b/src/lib/pyelliptic/arithmetic.py new file mode 100644 index 00000000..95c85b93 --- /dev/null +++ b/src/lib/pyelliptic/arithmetic.py @@ -0,0 +1,144 @@ +# pylint: disable=missing-docstring,too-many-function-args + +import hashlib +import re + +P = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 +A = 0 +Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 +Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 +G = (Gx, Gy) + + +def inv(a, n): + lm, hm = 1, 0 + low, high = a % n, n + while low > 1: + r = high / low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return lm % n + + +def get_code_string(base): + if base == 2: + return '01' + elif base == 10: + return '0123456789' + elif base == 16: + return "0123456789abcdef" + elif base == 58: + return "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + elif base == 256: + return ''.join([chr(x) for x in range(256)]) + else: + raise ValueError("Invalid base!") + + +def encode(val, base, minlen=0): + code_string = get_code_string(base) + result = "" + while val > 0: + result = code_string[val % base] + result + val /= base + if len(result) < minlen: + result = code_string[0] * (minlen - len(result)) + result + return result + + +def decode(string, base): + code_string = get_code_string(base) + result = 0 + if base == 16: + string = string.lower() + while string: + result *= base + result += code_string.find(string[0]) + string = string[1:] + return result + + +def changebase(string, frm, to, minlen=0): + return encode(decode(string, frm), to, minlen) + + +def base10_add(a, b): + if a is None: + return b[0], b[1] + if b is None: + return a[0], a[1] + if a[0] == b[0]: + if a[1] == b[1]: + return base10_double(a[0], a[1]) + return None + m = ((b[1] - a[1]) * inv(b[0] - a[0], P)) % P + x = (m * m - a[0] - b[0]) % P + y = (m * (a[0] - x) - a[1]) % P + return (x, y) + + +def base10_double(a): + if a is None: + return None + m = ((3 * a[0] * a[0] + A) * inv(2 * a[1], P)) % P + x = (m * m - 2 * a[0]) % P + y = (m * (a[0] - x) - a[1]) % P + return (x, y) + + +def base10_multiply(a, n): + if n == 0: + return G + if n == 1: + return a + if (n % 2) == 0: + return base10_double(base10_multiply(a, n / 2)) + if (n % 2) == 1: + return base10_add(base10_double(base10_multiply(a, n / 2)), a) + return None + + +def hex_to_point(h): + return (decode(h[2:66], 16), decode(h[66:], 16)) + + +def point_to_hex(p): + return '04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) + + +def multiply(privkey, pubkey): + return point_to_hex(base10_multiply(hex_to_point(pubkey), decode(privkey, 16))) + + +def privtopub(privkey): + return point_to_hex(base10_multiply(G, decode(privkey, 16))) + + +def add(p1, p2): + if len(p1) == 32: + return encode(decode(p1, 16) + decode(p2, 16) % P, 16, 32) + return point_to_hex(base10_add(hex_to_point(p1), hex_to_point(p2))) + + +def hash_160(string): + intermed = hashlib.sha256(string).digest() + ripemd160 = hashlib.new('ripemd160') + ripemd160.update(intermed) + return ripemd160.digest() + + +def dbl_sha256(string): + return hashlib.sha256(hashlib.sha256(string).digest()).digest() + + +def bin_to_b58check(inp): + inp_fmtd = '\x00' + inp + leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0)) + checksum = dbl_sha256(inp_fmtd)[:4] + return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58) + +# Convert a public key (in hex) to a Bitcoin address + + +def pubkey_to_address(pubkey): + return bin_to_b58check(hash_160(changebase(pubkey, 16, 256))) diff --git a/src/lib/pyelliptic/cipher.py b/src/lib/pyelliptic/cipher.py new file mode 100644 index 00000000..b597cafa --- /dev/null +++ b/src/lib/pyelliptic/cipher.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2011 Yann GUIBET +# See LICENSE for details. + +from pyelliptic.openssl import OpenSSL + + +class Cipher: + """ + Symmetric encryption + + import pyelliptic + iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') + ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') + ciphertext = ctx.update('test1') + ciphertext += ctx.update('test2') + ciphertext += ctx.final() + + ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') + print ctx2.ciphering(ciphertext) + """ + def __init__(self, key, iv, do, ciphername='aes-256-cbc'): + """ + do == 1 => Encrypt; do == 0 => Decrypt + """ + self.cipher = OpenSSL.get_cipher(ciphername) + self.ctx = OpenSSL.EVP_CIPHER_CTX_new() + if do == 1 or do == 0: + k = OpenSSL.malloc(key, len(key)) + IV = OpenSSL.malloc(iv, len(iv)) + OpenSSL.EVP_CipherInit_ex( + self.ctx, self.cipher.get_pointer(), 0, k, IV, do) + else: + raise Exception("RTFM ...") + + @staticmethod + def get_all_cipher(): + """ + static method, returns all ciphers available + """ + return OpenSSL.cipher_algo.keys() + + @staticmethod + def get_blocksize(ciphername): + cipher = OpenSSL.get_cipher(ciphername) + return cipher.get_blocksize() + + @staticmethod + def gen_IV(ciphername): + cipher = OpenSSL.get_cipher(ciphername) + return OpenSSL.rand(cipher.get_blocksize()) + + def update(self, input): + i = OpenSSL.c_int(0) + buffer = OpenSSL.malloc(b"", len(input) + self.cipher.get_blocksize()) + inp = OpenSSL.malloc(input, len(input)) + if OpenSSL.EVP_CipherUpdate(self.ctx, OpenSSL.byref(buffer), + OpenSSL.byref(i), inp, len(input)) == 0: + raise Exception("[OpenSSL] EVP_CipherUpdate FAIL ...") + return buffer.raw[0:i.value] + + def final(self): + i = OpenSSL.c_int(0) + buffer = OpenSSL.malloc(b"", self.cipher.get_blocksize()) + if (OpenSSL.EVP_CipherFinal_ex(self.ctx, OpenSSL.byref(buffer), + OpenSSL.byref(i))) == 0: + raise Exception("[OpenSSL] EVP_CipherFinal_ex FAIL ...") + return buffer.raw[0:i.value] + + def ciphering(self, input): + """ + Do update and final in one method + """ + buff = self.update(input) + return buff + self.final() + + def __del__(self): + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_CIPHER_CTX_reset(self.ctx) + else: + OpenSSL.EVP_CIPHER_CTX_cleanup(self.ctx) + OpenSSL.EVP_CIPHER_CTX_free(self.ctx) diff --git a/src/lib/pyelliptic/ecc.py b/src/lib/pyelliptic/ecc.py new file mode 100644 index 00000000..a70a1a5b --- /dev/null +++ b/src/lib/pyelliptic/ecc.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +pyelliptic/ecc.py +===================== +""" +# pylint: disable=protected-access + +# Copyright (C) 2011 Yann GUIBET +# See LICENSE for details. + +from hashlib import sha512 +from struct import pack, unpack + +from pyelliptic.cipher import Cipher +from pyelliptic.hash import equals, hmac_sha256 +from pyelliptic.openssl import OpenSSL + + +class ECC(object): + """ + Asymmetric encryption with Elliptic Curve Cryptography (ECC) + ECDH, ECDSA and ECIES + + >>> import pyelliptic + + >>> alice = pyelliptic.ECC() # default curve: sect283r1 + >>> bob = pyelliptic.ECC(curve='sect571r1') + + >>> ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) + >>> print bob.decrypt(ciphertext) + + >>> signature = bob.sign("Hello Alice") + >>> # alice's job : + >>> print pyelliptic.ECC( + >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") + + >>> # ERROR !!! + >>> try: + >>> key = alice.get_ecdh_key(bob.get_pubkey()) + >>> except: + >>> print("For ECDH key agreement, the keys must be defined on the same curve !") + + >>> alice = pyelliptic.ECC(curve='sect571r1') + >>> print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') + >>> print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') + + """ + + def __init__( + self, + pubkey=None, + privkey=None, + pubkey_x=None, + pubkey_y=None, + raw_privkey=None, + curve='sect283r1', + ): # pylint: disable=too-many-arguments + """ + For a normal and High level use, specifie pubkey, + privkey (if you need) and the curve + """ + if isinstance(curve, str): + self.curve = OpenSSL.get_curve(curve) + else: + self.curve = curve + + if pubkey_x is not None and pubkey_y is not None: + self._set_keys(pubkey_x, pubkey_y, raw_privkey) + elif pubkey is not None: + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) + if privkey is not None: + curve2, raw_privkey, _ = ECC._decode_privkey(privkey) + if curve != curve2: + raise Exception("Bad ECC keys ...") + self.curve = curve + self._set_keys(pubkey_x, pubkey_y, raw_privkey) + else: + self.privkey, self.pubkey_x, self.pubkey_y = self._generate() + + def _set_keys(self, pubkey_x, pubkey_y, privkey): + if self.raw_check_key(privkey, pubkey_x, pubkey_y) < 0: + self.pubkey_x = None + self.pubkey_y = None + self.privkey = None + raise Exception("Bad ECC keys ...") + else: + self.pubkey_x = pubkey_x + self.pubkey_y = pubkey_y + self.privkey = privkey + + @staticmethod + def get_curves(): + """ + static method, returns the list of all the curves available + """ + return OpenSSL.curves.keys() + + def get_curve(self): + """Encryption object from curve name""" + return OpenSSL.get_curve_by_id(self.curve) + + def get_curve_id(self): + """Currently used curve""" + return self.curve + + def get_pubkey(self): + """ + High level function which returns : + curve(2) + len_of_pubkeyX(2) + pubkeyX + len_of_pubkeyY + pubkeyY + """ + return b''.join(( + pack('!H', self.curve), + pack('!H', len(self.pubkey_x)), + self.pubkey_x, + pack('!H', len(self.pubkey_y)), + self.pubkey_y, + )) + + def get_privkey(self): + """ + High level function which returns + curve(2) + len_of_privkey(2) + privkey + """ + return b''.join(( + pack('!H', self.curve), + pack('!H', len(self.privkey)), + self.privkey, + )) + + @staticmethod + def _decode_pubkey(pubkey): + i = 0 + curve = unpack('!H', pubkey[i:i + 2])[0] + i += 2 + tmplen = unpack('!H', pubkey[i:i + 2])[0] + i += 2 + pubkey_x = pubkey[i:i + tmplen] + i += tmplen + tmplen = unpack('!H', pubkey[i:i + 2])[0] + i += 2 + pubkey_y = pubkey[i:i + tmplen] + i += tmplen + return curve, pubkey_x, pubkey_y, i + + @staticmethod + def _decode_privkey(privkey): + i = 0 + curve = unpack('!H', privkey[i:i + 2])[0] + i += 2 + tmplen = unpack('!H', privkey[i:i + 2])[0] + i += 2 + privkey = privkey[i:i + tmplen] + i += tmplen + return curve, privkey, i + + def _generate(self): + try: + pub_key_x = OpenSSL.BN_new() + pub_key_y = OpenSSL.BN_new() + + key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + if key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + if (OpenSSL.EC_KEY_generate_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_generate_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + priv_key = OpenSSL.EC_KEY_get0_private_key(key) + + group = OpenSSL.EC_KEY_get0_group(key) + pub_key = OpenSSL.EC_KEY_get0_public_key(key) + + if OpenSSL.EC_POINT_get_affine_coordinates_GFp( + group, pub_key, pub_key_x, pub_key_y, 0) == 0: + raise Exception("[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ...") + + privkey = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(priv_key)) + pubkeyx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_x)) + pubkeyy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_y)) + OpenSSL.BN_bn2bin(priv_key, privkey) + privkey = privkey.raw + OpenSSL.BN_bn2bin(pub_key_x, pubkeyx) + pubkeyx = pubkeyx.raw + OpenSSL.BN_bn2bin(pub_key_y, pubkeyy) + pubkeyy = pubkeyy.raw + self.raw_check_key(privkey, pubkeyx, pubkeyy) + + return privkey, pubkeyx, pubkeyy + + finally: + OpenSSL.EC_KEY_free(key) + OpenSSL.BN_free(pub_key_x) + OpenSSL.BN_free(pub_key_y) + + def get_ecdh_key(self, pubkey): + """ + High level function. Compute public key with the local private key + and returns a 512bits shared key + """ + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) + if curve != self.curve: + raise Exception("ECC keys must be from the same curve !") + return sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() + + def raw_get_ecdh_key(self, pubkey_x, pubkey_y): + """ECDH key as binary data""" + try: + ecdh_keybuffer = OpenSSL.malloc(0, 32) + + other_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + if other_key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + + other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) + other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) + + other_group = OpenSSL.EC_KEY_get0_group(other_key) + other_pub_key = OpenSSL.EC_POINT_new(other_group) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group, + other_pub_key, + other_pub_key_x, + other_pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") + if (OpenSSL.EC_KEY_set_public_key(other_key, other_pub_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(other_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + + own_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + if own_key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + own_priv_key = OpenSSL.BN_bin2bn( + self.privkey, len(self.privkey), 0) + + if (OpenSSL.EC_KEY_set_private_key(own_key, own_priv_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") + + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EC_KEY_set_method(own_key, OpenSSL.EC_KEY_OpenSSL()) + else: + OpenSSL.ECDH_set_method(own_key, OpenSSL.ECDH_OpenSSL()) + ecdh_keylen = OpenSSL.ECDH_compute_key( + ecdh_keybuffer, 32, other_pub_key, own_key, 0) + + if ecdh_keylen != 32: + raise Exception("[OpenSSL] ECDH keylen FAIL ...") + + return ecdh_keybuffer.raw + + finally: + OpenSSL.EC_KEY_free(other_key) + OpenSSL.BN_free(other_pub_key_x) + OpenSSL.BN_free(other_pub_key_y) + OpenSSL.EC_POINT_free(other_pub_key) + OpenSSL.EC_KEY_free(own_key) + OpenSSL.BN_free(own_priv_key) + + def check_key(self, privkey, pubkey): + """ + Check the public key and the private key. + The private key is optional (replace by None) + """ + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) + if privkey is None: + raw_privkey = None + curve2 = curve + else: + curve2, raw_privkey, _ = ECC._decode_privkey(privkey) + if curve != curve2: + raise Exception("Bad public and private key") + return self.raw_check_key(raw_privkey, pubkey_x, pubkey_y, curve) + + def raw_check_key(self, privkey, pubkey_x, pubkey_y, curve=None): + """Check key validity, key is supplied as binary data""" + # pylint: disable=too-many-branches + if curve is None: + curve = self.curve + elif isinstance(curve, str): + curve = OpenSSL.get_curve(curve) + else: + curve = curve + try: + key = OpenSSL.EC_KEY_new_by_curve_name(curve) + if key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + if privkey is not None: + priv_key = OpenSSL.BN_bin2bn(privkey, len(privkey), 0) + pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) + pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) + + if privkey is not None: + if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: + raise Exception( + "[OpenSSL] EC_KEY_set_private_key FAIL ...") + + group = OpenSSL.EC_KEY_get0_group(key) + pub_key = OpenSSL.EC_POINT_new(group) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") + if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + return 0 + + finally: + OpenSSL.EC_KEY_free(key) + OpenSSL.BN_free(pub_key_x) + OpenSSL.BN_free(pub_key_y) + OpenSSL.EC_POINT_free(pub_key) + if privkey is not None: + OpenSSL.BN_free(priv_key) + + def sign(self, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): + """ + Sign the input with ECDSA method and returns the signature + """ + # pylint: disable=too-many-branches,too-many-locals + try: + size = len(inputb) + buff = OpenSSL.malloc(inputb, size) + digest = OpenSSL.malloc(0, 64) + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + md_ctx = OpenSSL.EVP_MD_CTX_new() + else: + md_ctx = OpenSSL.EVP_MD_CTX_create() + dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) + siglen = OpenSSL.pointer(OpenSSL.c_int(0)) + sig = OpenSSL.malloc(0, 151) + + key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + if key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + + priv_key = OpenSSL.BN_bin2bn(self.privkey, len(self.privkey), 0) + pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) + pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) + + if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") + + group = OpenSSL.EC_KEY_get0_group(key) + pub_key = OpenSSL.EC_POINT_new(group) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") + if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_MD_CTX_new(md_ctx) + else: + OpenSSL.EVP_MD_CTX_init(md_ctx) + OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) + + if (OpenSSL.EVP_DigestUpdate(md_ctx, buff, size)) == 0: + raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") + OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) + OpenSSL.ECDSA_sign(0, digest, dgst_len.contents, sig, siglen, key) + if (OpenSSL.ECDSA_verify(0, digest, dgst_len.contents, sig, + siglen.contents, key)) != 1: + raise Exception("[OpenSSL] ECDSA_verify FAIL ...") + + return sig.raw[:siglen.contents.value] + + finally: + OpenSSL.EC_KEY_free(key) + OpenSSL.BN_free(pub_key_x) + OpenSSL.BN_free(pub_key_y) + OpenSSL.BN_free(priv_key) + OpenSSL.EC_POINT_free(pub_key) + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_MD_CTX_free(md_ctx) + else: + OpenSSL.EVP_MD_CTX_destroy(md_ctx) + + def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): + """ + Verify the signature with the input and the local public key. + Returns a boolean + """ + # pylint: disable=too-many-branches + try: + bsig = OpenSSL.malloc(sig, len(sig)) + binputb = OpenSSL.malloc(inputb, len(inputb)) + digest = OpenSSL.malloc(0, 64) + dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + md_ctx = OpenSSL.EVP_MD_CTX_new() + else: + md_ctx = OpenSSL.EVP_MD_CTX_create() + key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + + if key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + + pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) + pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) + group = OpenSSL.EC_KEY_get0_group(key) + pub_key = OpenSSL.EC_POINT_new(group) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") + if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_MD_CTX_new(md_ctx) + else: + OpenSSL.EVP_MD_CTX_init(md_ctx) + OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) + if (OpenSSL.EVP_DigestUpdate(md_ctx, binputb, len(inputb))) == 0: + raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") + + OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) + ret = OpenSSL.ECDSA_verify( + 0, digest, dgst_len.contents, bsig, len(sig), key) + + if ret == -1: + return False # Fail to Check + if ret == 0: + return False # Bad signature ! + return True # Good + + finally: + OpenSSL.EC_KEY_free(key) + OpenSSL.BN_free(pub_key_x) + OpenSSL.BN_free(pub_key_y) + OpenSSL.EC_POINT_free(pub_key) + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_MD_CTX_free(md_ctx) + else: + OpenSSL.EVP_MD_CTX_destroy(md_ctx) + + @staticmethod + def encrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): + """ + Encrypt data with ECIES method using the public key of the recipient. + """ + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) + return ECC.raw_encrypt(data, pubkey_x, pubkey_y, curve=curve, + ephemcurve=ephemcurve, ciphername=ciphername) + + @staticmethod + def raw_encrypt( + data, + pubkey_x, + pubkey_y, + curve='sect283r1', + ephemcurve=None, + ciphername='aes-256-cbc', + ): # pylint: disable=too-many-arguments + """ECHD encryption, keys supplied in binary data format""" + + if ephemcurve is None: + ephemcurve = curve + ephem = ECC(curve=ephemcurve) + key = sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() + key_e, key_m = key[:32], key[32:] + pubkey = ephem.get_pubkey() + iv = OpenSSL.rand(OpenSSL.get_cipher(ciphername).get_blocksize()) + ctx = Cipher(key_e, iv, 1, ciphername) + ciphertext = iv + pubkey + ctx.ciphering(data) + mac = hmac_sha256(key_m, ciphertext) + return ciphertext + mac + + def decrypt(self, data, ciphername='aes-256-cbc'): + """ + Decrypt data with ECIES method using the local private key + """ + # pylint: disable=too-many-locals + blocksize = OpenSSL.get_cipher(ciphername).get_blocksize() + iv = data[:blocksize] + i = blocksize + _, pubkey_x, pubkey_y, i2 = ECC._decode_pubkey(data[i:]) + i += i2 + ciphertext = data[i:len(data) - 32] + i += len(ciphertext) + mac = data[i:] + key = sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() + key_e, key_m = key[:32], key[32:] + if not equals(hmac_sha256(key_m, data[:len(data) - 32]), mac): + raise RuntimeError("Fail to verify data") + ctx = Cipher(key_e, iv, 0, ciphername) + return ctx.ciphering(ciphertext) diff --git a/src/lib/pyelliptic/hash.py b/src/lib/pyelliptic/hash.py new file mode 100644 index 00000000..fb910dd4 --- /dev/null +++ b/src/lib/pyelliptic/hash.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2011 Yann GUIBET +# See LICENSE for details. + +from pyelliptic.openssl import OpenSSL + + +# For python3 +def _equals_bytes(a, b): + if len(a) != len(b): + return False + result = 0 + for x, y in zip(a, b): + result |= x ^ y + return result == 0 + + +def _equals_str(a, b): + if len(a) != len(b): + return False + result = 0 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 + + +def equals(a, b): + if isinstance(a, str): + return _equals_str(a, b) + else: + return _equals_bytes(a, b) + + +def hmac_sha256(k, m): + """ + Compute the key and the message with HMAC SHA5256 + """ + key = OpenSSL.malloc(k, len(k)) + d = OpenSSL.malloc(m, len(m)) + md = OpenSSL.malloc(0, 32) + i = OpenSSL.pointer(OpenSSL.c_int(0)) + OpenSSL.HMAC(OpenSSL.EVP_sha256(), key, len(k), d, len(m), md, i) + return md.raw + + +def hmac_sha512(k, m): + """ + Compute the key and the message with HMAC SHA512 + """ + key = OpenSSL.malloc(k, len(k)) + d = OpenSSL.malloc(m, len(m)) + md = OpenSSL.malloc(0, 64) + i = OpenSSL.pointer(OpenSSL.c_int(0)) + OpenSSL.HMAC(OpenSSL.EVP_sha512(), key, len(k), d, len(m), md, i) + return md.raw + + +def pbkdf2(password, salt=None, i=10000, keylen=64): + if salt is None: + salt = OpenSSL.rand(8) + p_password = OpenSSL.malloc(password, len(password)) + p_salt = OpenSSL.malloc(salt, len(salt)) + output = OpenSSL.malloc(0, keylen) + OpenSSL.PKCS5_PBKDF2_HMAC(p_password, len(password), p_salt, + len(p_salt), i, OpenSSL.EVP_sha256(), + keylen, output) + return salt, output.raw diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py new file mode 100644 index 00000000..02551267 --- /dev/null +++ b/src/lib/pyelliptic/openssl.py @@ -0,0 +1,551 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2011 Yann GUIBET +# See LICENSE for details. +# +# Software slightly changed by Jonathan Warren + +import sys +import ctypes + +OpenSSL = None + + +class CipherName: + def __init__(self, name, pointer, blocksize): + self._name = name + self._pointer = pointer + self._blocksize = blocksize + + def __str__(self): + return "Cipher : " + self._name + " | Blocksize : " + str(self._blocksize) + " | Fonction pointer : " + str(self._pointer) + + def get_pointer(self): + return self._pointer() + + def get_name(self): + return self._name + + def get_blocksize(self): + return self._blocksize + + +def get_version(library): + version = None + hexversion = None + cflags = None + try: + #OpenSSL 1.1 + OPENSSL_VERSION = 0 + OPENSSL_CFLAGS = 1 + library.OpenSSL_version.argtypes = [ctypes.c_int] + library.OpenSSL_version.restype = ctypes.c_char_p + version = library.OpenSSL_version(OPENSSL_VERSION) + cflags = library.OpenSSL_version(OPENSSL_CFLAGS) + library.OpenSSL_version_num.restype = ctypes.c_long + hexversion = library.OpenSSL_version_num() + except AttributeError: + try: + #OpenSSL 1.0 + SSLEAY_VERSION = 0 + SSLEAY_CFLAGS = 2 + library.SSLeay.restype = ctypes.c_long + library.SSLeay_version.restype = ctypes.c_char_p + library.SSLeay_version.argtypes = [ctypes.c_int] + version = library.SSLeay_version(SSLEAY_VERSION) + cflags = library.SSLeay_version(SSLEAY_CFLAGS) + hexversion = library.SSLeay() + except AttributeError: + #raise NotImplementedError('Cannot determine version of this OpenSSL library.') + pass + return (version, hexversion, cflags) + + +class _OpenSSL: + """ + Wrapper for OpenSSL using ctypes + """ + def __init__(self, library): + """ + Build the wrapper + """ + self._lib = ctypes.CDLL(library) + self._version, self._hexversion, self._cflags = get_version(self._lib) + self._libreSSL = self._version.startswith(b"LibreSSL") + + self.pointer = ctypes.pointer + self.c_int = ctypes.c_int + self.byref = ctypes.byref + self.create_string_buffer = ctypes.create_string_buffer + + self.BN_new = self._lib.BN_new + self.BN_new.restype = ctypes.c_void_p + self.BN_new.argtypes = [] + + self.BN_free = self._lib.BN_free + self.BN_free.restype = None + self.BN_free.argtypes = [ctypes.c_void_p] + + self.BN_num_bits = self._lib.BN_num_bits + self.BN_num_bits.restype = ctypes.c_int + self.BN_num_bits.argtypes = [ctypes.c_void_p] + + self.BN_bn2bin = self._lib.BN_bn2bin + self.BN_bn2bin.restype = ctypes.c_int + self.BN_bn2bin.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.BN_bin2bn = self._lib.BN_bin2bn + self.BN_bin2bn.restype = ctypes.c_void_p + self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p] + + self.EC_KEY_free = self._lib.EC_KEY_free + self.EC_KEY_free.restype = None + self.EC_KEY_free.argtypes = [ctypes.c_void_p] + + self.EC_KEY_new_by_curve_name = self._lib.EC_KEY_new_by_curve_name + self.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p + self.EC_KEY_new_by_curve_name.argtypes = [ctypes.c_int] + + self.EC_KEY_generate_key = self._lib.EC_KEY_generate_key + self.EC_KEY_generate_key.restype = ctypes.c_int + self.EC_KEY_generate_key.argtypes = [ctypes.c_void_p] + + self.EC_KEY_check_key = self._lib.EC_KEY_check_key + self.EC_KEY_check_key.restype = ctypes.c_int + self.EC_KEY_check_key.argtypes = [ctypes.c_void_p] + + self.EC_KEY_get0_private_key = self._lib.EC_KEY_get0_private_key + self.EC_KEY_get0_private_key.restype = ctypes.c_void_p + self.EC_KEY_get0_private_key.argtypes = [ctypes.c_void_p] + + self.EC_KEY_get0_public_key = self._lib.EC_KEY_get0_public_key + self.EC_KEY_get0_public_key.restype = ctypes.c_void_p + self.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p] + + self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group + self.EC_KEY_get0_group.restype = ctypes.c_void_p + self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] + + self.EC_POINT_get_affine_coordinates_GFp = self._lib.EC_POINT_get_affine_coordinates_GFp + self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int + self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key + self.EC_KEY_set_private_key.restype = ctypes.c_int + self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + self.EC_KEY_set_public_key = self._lib.EC_KEY_set_public_key + self.EC_KEY_set_public_key.restype = ctypes.c_int + self.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + self.EC_KEY_set_group = self._lib.EC_KEY_set_group + self.EC_KEY_set_group.restype = ctypes.c_int + self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.EC_POINT_set_affine_coordinates_GFp = self._lib.EC_POINT_set_affine_coordinates_GFp + self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int + self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.EC_POINT_new = self._lib.EC_POINT_new + self.EC_POINT_new.restype = ctypes.c_void_p + self.EC_POINT_new.argtypes = [ctypes.c_void_p] + + self.EC_POINT_free = self._lib.EC_POINT_free + self.EC_POINT_free.restype = None + self.EC_POINT_free.argtypes = [ctypes.c_void_p] + + self.BN_CTX_free = self._lib.BN_CTX_free + self.BN_CTX_free.restype = None + self.BN_CTX_free.argtypes = [ctypes.c_void_p] + + self.EC_POINT_mul = self._lib.EC_POINT_mul + self.EC_POINT_mul.restype = ctypes.c_int + self.EC_POINT_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key + self.EC_KEY_set_private_key.restype = ctypes.c_int + self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + if self._hexversion >= 0x10100000 and not self._libreSSL: + self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL + self._lib.EC_KEY_OpenSSL.restype = ctypes.c_void_p + self._lib.EC_KEY_OpenSSL.argtypes = [] + + self.EC_KEY_set_method = self._lib.EC_KEY_set_method + self._lib.EC_KEY_set_method.restype = ctypes.c_int + self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + else: + self.ECDH_OpenSSL = self._lib.ECDH_OpenSSL + self._lib.ECDH_OpenSSL.restype = ctypes.c_void_p + self._lib.ECDH_OpenSSL.argtypes = [] + + self.ECDH_set_method = self._lib.ECDH_set_method + self._lib.ECDH_set_method.restype = ctypes.c_int + self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.BN_CTX_new = self._lib.BN_CTX_new + self._lib.BN_CTX_new.restype = ctypes.c_void_p + self._lib.BN_CTX_new.argtypes = [] + + self.ECDH_compute_key = self._lib.ECDH_compute_key + self.ECDH_compute_key.restype = ctypes.c_int + self.ECDH_compute_key.argtypes = [ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_CipherInit_ex = self._lib.EVP_CipherInit_ex + self.EVP_CipherInit_ex.restype = ctypes.c_int + self.EVP_CipherInit_ex.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_CIPHER_CTX_new = self._lib.EVP_CIPHER_CTX_new + self.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p + self.EVP_CIPHER_CTX_new.argtypes = [] + + # Cipher + self.EVP_aes_128_cfb128 = self._lib.EVP_aes_128_cfb128 + self.EVP_aes_128_cfb128.restype = ctypes.c_void_p + self.EVP_aes_128_cfb128.argtypes = [] + + self.EVP_aes_256_cfb128 = self._lib.EVP_aes_256_cfb128 + self.EVP_aes_256_cfb128.restype = ctypes.c_void_p + self.EVP_aes_256_cfb128.argtypes = [] + + self.EVP_aes_128_cbc = self._lib.EVP_aes_128_cbc + self.EVP_aes_128_cbc.restype = ctypes.c_void_p + self.EVP_aes_128_cbc.argtypes = [] + + self.EVP_aes_256_cbc = self._lib.EVP_aes_256_cbc + self.EVP_aes_256_cbc.restype = ctypes.c_void_p + self.EVP_aes_256_cbc.argtypes = [] + + #self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr + #self.EVP_aes_128_ctr.restype = ctypes.c_void_p + #self.EVP_aes_128_ctr.argtypes = [] + + #self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr + #self.EVP_aes_256_ctr.restype = ctypes.c_void_p + #self.EVP_aes_256_ctr.argtypes = [] + + self.EVP_aes_128_ofb = self._lib.EVP_aes_128_ofb + self.EVP_aes_128_ofb.restype = ctypes.c_void_p + self.EVP_aes_128_ofb.argtypes = [] + + self.EVP_aes_256_ofb = self._lib.EVP_aes_256_ofb + self.EVP_aes_256_ofb.restype = ctypes.c_void_p + self.EVP_aes_256_ofb.argtypes = [] + + self.EVP_bf_cbc = self._lib.EVP_bf_cbc + self.EVP_bf_cbc.restype = ctypes.c_void_p + self.EVP_bf_cbc.argtypes = [] + + self.EVP_bf_cfb64 = self._lib.EVP_bf_cfb64 + self.EVP_bf_cfb64.restype = ctypes.c_void_p + self.EVP_bf_cfb64.argtypes = [] + + self.EVP_rc4 = self._lib.EVP_rc4 + self.EVP_rc4.restype = ctypes.c_void_p + self.EVP_rc4.argtypes = [] + + if self._hexversion >= 0x10100000 and not self._libreSSL: + self.EVP_CIPHER_CTX_reset = self._lib.EVP_CIPHER_CTX_reset + self.EVP_CIPHER_CTX_reset.restype = ctypes.c_int + self.EVP_CIPHER_CTX_reset.argtypes = [ctypes.c_void_p] + else: + self.EVP_CIPHER_CTX_cleanup = self._lib.EVP_CIPHER_CTX_cleanup + self.EVP_CIPHER_CTX_cleanup.restype = ctypes.c_int + self.EVP_CIPHER_CTX_cleanup.argtypes = [ctypes.c_void_p] + + self.EVP_CIPHER_CTX_free = self._lib.EVP_CIPHER_CTX_free + self.EVP_CIPHER_CTX_free.restype = None + self.EVP_CIPHER_CTX_free.argtypes = [ctypes.c_void_p] + + self.EVP_CipherUpdate = self._lib.EVP_CipherUpdate + self.EVP_CipherUpdate.restype = ctypes.c_int + self.EVP_CipherUpdate.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] + + self.EVP_CipherFinal_ex = self._lib.EVP_CipherFinal_ex + self.EVP_CipherFinal_ex.restype = ctypes.c_int + self.EVP_CipherFinal_ex.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_DigestInit = self._lib.EVP_DigestInit + self.EVP_DigestInit.restype = ctypes.c_int + self._lib.EVP_DigestInit.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex + self.EVP_DigestInit_ex.restype = ctypes.c_int + self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p] + + self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate + self.EVP_DigestUpdate.restype = ctypes.c_int + self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_int] + + self.EVP_DigestFinal = self._lib.EVP_DigestFinal + self.EVP_DigestFinal.restype = ctypes.c_int + self.EVP_DigestFinal.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_DigestFinal_ex = self._lib.EVP_DigestFinal_ex + self.EVP_DigestFinal_ex.restype = ctypes.c_int + self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.ECDSA_sign = self._lib.ECDSA_sign + self.ECDSA_sign.restype = ctypes.c_int + self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.ECDSA_verify = self._lib.ECDSA_verify + self.ECDSA_verify.restype = ctypes.c_int + self.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] + + if self._hexversion >= 0x10100000 and not self._libreSSL: + self.EVP_MD_CTX_new = self._lib.EVP_MD_CTX_new + self.EVP_MD_CTX_new.restype = ctypes.c_void_p + self.EVP_MD_CTX_new.argtypes = [] + + self.EVP_MD_CTX_reset = self._lib.EVP_MD_CTX_reset + self.EVP_MD_CTX_reset.restype = None + self.EVP_MD_CTX_reset.argtypes = [ctypes.c_void_p] + + self.EVP_MD_CTX_free = self._lib.EVP_MD_CTX_free + self.EVP_MD_CTX_free.restype = None + self.EVP_MD_CTX_free.argtypes = [ctypes.c_void_p] + + self.EVP_sha1 = self._lib.EVP_sha1 + self.EVP_sha1.restype = ctypes.c_void_p + self.EVP_sha1.argtypes = [] + + self.digest_ecdsa_sha1 = self.EVP_sha1 + else: + self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create + self.EVP_MD_CTX_create.restype = ctypes.c_void_p + self.EVP_MD_CTX_create.argtypes = [] + + self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init + self.EVP_MD_CTX_init.restype = None + self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p] + + self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy + self.EVP_MD_CTX_destroy.restype = None + self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p] + + self.EVP_ecdsa = self._lib.EVP_ecdsa + self._lib.EVP_ecdsa.restype = ctypes.c_void_p + self._lib.EVP_ecdsa.argtypes = [] + + self.digest_ecdsa_sha1 = self.EVP_ecdsa + + self.RAND_bytes = self._lib.RAND_bytes + self.RAND_bytes.restype = ctypes.c_int + self.RAND_bytes.argtypes = [ctypes.c_void_p, ctypes.c_int] + + self.EVP_sha256 = self._lib.EVP_sha256 + self.EVP_sha256.restype = ctypes.c_void_p + self.EVP_sha256.argtypes = [] + + self.i2o_ECPublicKey = self._lib.i2o_ECPublicKey + self.i2o_ECPublicKey.restype = ctypes.c_int + self.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_sha512 = self._lib.EVP_sha512 + self.EVP_sha512.restype = ctypes.c_void_p + self.EVP_sha512.argtypes = [] + + self.HMAC = self._lib.HMAC + self.HMAC.restype = ctypes.c_void_p + self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] + + try: + self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC + except: + # The above is not compatible with all versions of OSX. + self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 + + self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int + self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p, ctypes.c_int, + ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p] + + self._set_ciphers() + self._set_curves() + + def _set_ciphers(self): + self.cipher_algo = { + 'aes-128-cbc': CipherName('aes-128-cbc', self.EVP_aes_128_cbc, 16), + 'aes-256-cbc': CipherName('aes-256-cbc', self.EVP_aes_256_cbc, 16), + 'aes-128-cfb': CipherName('aes-128-cfb', self.EVP_aes_128_cfb128, 16), + 'aes-256-cfb': CipherName('aes-256-cfb', self.EVP_aes_256_cfb128, 16), + 'aes-128-ofb': CipherName('aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), + 'aes-256-ofb': CipherName('aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), + #'aes-128-ctr': CipherName('aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), + #'aes-256-ctr': CipherName('aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), + 'bf-cfb': CipherName('bf-cfb', self.EVP_bf_cfb64, 8), + 'bf-cbc': CipherName('bf-cbc', self.EVP_bf_cbc, 8), + 'rc4': CipherName('rc4', self.EVP_rc4, 128), # 128 is the initialisation size not block size + } + + def _set_curves(self): + self.curves = { + 'secp112r1': 704, + 'secp112r2': 705, + 'secp128r1': 706, + 'secp128r2': 707, + 'secp160k1': 708, + 'secp160r1': 709, + 'secp160r2': 710, + 'secp192k1': 711, + 'secp224k1': 712, + 'secp224r1': 713, + 'secp256k1': 714, + 'secp384r1': 715, + 'secp521r1': 716, + 'sect113r1': 717, + 'sect113r2': 718, + 'sect131r1': 719, + 'sect131r2': 720, + 'sect163k1': 721, + 'sect163r1': 722, + 'sect163r2': 723, + 'sect193r1': 724, + 'sect193r2': 725, + 'sect233k1': 726, + 'sect233r1': 727, + 'sect239k1': 728, + 'sect283k1': 729, + 'sect283r1': 730, + 'sect409k1': 731, + 'sect409r1': 732, + 'sect571k1': 733, + 'sect571r1': 734, + } + + def BN_num_bytes(self, x): + """ + returns the length of a BN (OpenSSl API) + """ + return int((self.BN_num_bits(x) + 7) / 8) + + def get_cipher(self, name): + """ + returns the OpenSSL cipher instance + """ + if name not in self.cipher_algo: + raise Exception("Unknown cipher") + return self.cipher_algo[name] + + def get_curve(self, name): + """ + returns the id of a elliptic curve + """ + if name not in self.curves: + raise Exception("Unknown curve") + return self.curves[name] + + def get_curve_by_id(self, id): + """ + returns the name of a elliptic curve with his id + """ + res = None + for i in self.curves: + if self.curves[i] == id: + res = i + break + if res is None: + raise Exception("Unknown curve") + return res + + def rand(self, size): + """ + OpenSSL random function + """ + buffer = self.malloc(0, size) + # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is + # evidently possible that it returned an error and not-actually-random data. However, in + # tests on various operating systems, while generating hundreds of gigabytes of random + # strings of various sizes I could not get an error to occur. Also Bitcoin doesn't check + # the return value of RAND_bytes either. + # Fixed in Bitmessage version 0.4.2 (in source code on 2013-10-13) + while self.RAND_bytes(buffer, size) != 1: + import time + time.sleep(1) + return buffer.raw + + def malloc(self, data, size): + """ + returns a create_string_buffer (ctypes) + """ + buffer = None + if data != 0: + if sys.version_info.major == 3 and isinstance(data, type('')): + data = data.encode() + buffer = self.create_string_buffer(data, size) + else: + buffer = self.create_string_buffer(size) + return buffer + +def loadOpenSSL(): + global OpenSSL + from os import path, environ + from ctypes.util import find_library + + libdir = [] + if getattr(sys,'frozen', None): + if 'darwin' in sys.platform: + libdir.extend([ + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.1.0.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.2.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.1.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.0.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.0.9.8.dylib'), + ]) + elif 'win32' in sys.platform or 'win64' in sys.platform: + libdir.append(path.join(sys._MEIPASS, 'libeay32.dll')) + else: + libdir.extend([ + path.join(sys._MEIPASS, 'libcrypto.so'), + path.join(sys._MEIPASS, 'libssl.so'), + path.join(sys._MEIPASS, 'libcrypto.so.1.1.0'), + path.join(sys._MEIPASS, 'libssl.so.1.1.0'), + path.join(sys._MEIPASS, 'libcrypto.so.1.0.2'), + path.join(sys._MEIPASS, 'libssl.so.1.0.2'), + path.join(sys._MEIPASS, 'libcrypto.so.1.0.1'), + path.join(sys._MEIPASS, 'libssl.so.1.0.1'), + path.join(sys._MEIPASS, 'libcrypto.so.1.0.0'), + path.join(sys._MEIPASS, 'libssl.so.1.0.0'), + path.join(sys._MEIPASS, 'libcrypto.so.0.9.8'), + path.join(sys._MEIPASS, 'libssl.so.0.9.8'), + ]) + if 'darwin' in sys.platform: + libdir.extend(['libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) + elif 'win32' in sys.platform or 'win64' in sys.platform: + libdir.append('libeay32.dll') + else: + libdir.append('libcrypto.so') + libdir.append('libssl.so') + libdir.append('libcrypto.so.1.0.0') + libdir.append('libssl.so.1.0.0') + if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: + libdir.append(find_library('ssl')) + elif 'win32' in sys.platform or 'win64' in sys.platform: + libdir.append(find_library('libeay32')) + for library in libdir: + try: + OpenSSL = _OpenSSL(library) + return + except: + pass + raise Exception("Failed to load OpenSSL library, searched for: " + " ".join(libdir)) + +loadOpenSSL() diff --git a/src/lib/pyelliptic/setup.py b/src/lib/pyelliptic/setup.py new file mode 100644 index 00000000..cc9c0a21 --- /dev/null +++ b/src/lib/pyelliptic/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +setup( + name="pyelliptic", + version='2.0.1', + url='https://github.com/radfish/pyelliptic', + license='GPL', + description="Python OpenSSL wrapper for ECC (ECDSA, ECIES), AES, HMAC, Blowfish, ...", + author='Yann GUIBET', + author_email='yannguibet@gmail.com', + maintainer="redfish", + maintainer_email='redfish@galactica.pw', + packages=find_packages(), + classifiers=[ + 'Operating System :: Unix', + 'Operating System :: Microsoft :: Windows', + 'Environment :: MacOS X', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.7', + 'Topic :: Security :: Cryptography', + ], +) From fa970fa1023ff2b40bd9df7127fe5f1291aad509 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:14:17 +0200 Subject: [PATCH 015/483] Test CryptMessage plugin using testAction function --- plugins/CryptMessage/Test/TestCrypt.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/plugins/CryptMessage/Test/TestCrypt.py b/plugins/CryptMessage/Test/TestCrypt.py index 681c4790..05cc6e44 100644 --- a/plugins/CryptMessage/Test/TestCrypt.py +++ b/plugins/CryptMessage/Test/TestCrypt.py @@ -47,26 +47,25 @@ class TestCrypt: assert pub1 != pub2 def testEcies(self, ui_websocket): - ui_websocket.actionUserPublickey(0, 0) - pub = ui_websocket.ws.getResult() + pub = ui_websocket.testAction("UserPublickey") - ui_websocket.actionEciesEncrypt(0, "hello", pub) - encrypted = ui_websocket.ws.getResult() + encrypted = ui_websocket.testAction("EciesEncrypt", "hello", pub) assert len(encrypted) == 180 # Don't allow decrypt using other privatekey index - ui_websocket.actionEciesDecrypt(0, encrypted, 123) - decrypted = ui_websocket.ws.getResult() + decrypted = ui_websocket.testAction("EciesDecrypt", encrypted, 123) assert decrypted != "hello" # Decrypt using correct privatekey - ui_websocket.actionEciesDecrypt(0, encrypted) - decrypted = ui_websocket.ws.getResult() + decrypted = ui_websocket.testAction("EciesDecrypt", encrypted) assert decrypted == "hello" + # Decrypt incorrect text + decrypted = ui_websocket.testAction("EciesDecrypt", "baad") + assert decrypted == None + # Decrypt batch - ui_websocket.actionEciesDecrypt(0, [encrypted, "baad", encrypted]) - decrypted = ui_websocket.ws.getResult() + decrypted = ui_websocket.testAction("EciesDecrypt", [encrypted, "baad", encrypted]) assert decrypted == ["hello", None, "hello"] def testEciesUtf8(self, ui_websocket): From c5116fb3185e364ed121be4292d61b477ee168a5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:14:44 +0200 Subject: [PATCH 016/483] Modify testAction command to use handleRequest instead of directly calling the function --- src/Test/conftest.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 1f068d69..99fdae13 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -38,7 +38,7 @@ else: CHROMEDRIVER_PATH = "chromedriver" SITE_URL = "http://127.0.0.1:43110" -TEST_DATA_PATH = 'src/Test/testdata' +TEST_DATA_PATH = 'src/Test/testdata' sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + "/../lib")) # External modules directory sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + "/..")) # Imports relative to src dir @@ -335,9 +335,8 @@ def ui_websocket(site, user): ui_websocket = UiWebsocket(ws_mock, site, None, user, None) def testAction(action, *args, **kwargs): - func = getattr(ui_websocket, "action%s" % action) - func(0, *args, **kwargs) - return ui_websocket.ws.result.get() + ui_websocket.handleRequest({"id": 0, "cmd": action, "params": list(args) if args else kwargs}) + return ui_websocket.ws.getResult() ui_websocket.testAction = testAction return ui_websocket From 26678a65f829ba70b655e4f8bb7633bad30c13b0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:15:45 +0200 Subject: [PATCH 017/483] Limit notifications max with --- src/Ui/media/Notifications.coffee | 2 +- src/Ui/media/Wrapper.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/media/Notifications.coffee b/src/Ui/media/Notifications.coffee index b31067fb..7264bb2d 100644 --- a/src/Ui/media/Notifications.coffee +++ b/src/Ui/media/Notifications.coffee @@ -51,7 +51,7 @@ class Notifications ), timeout # Animate - width = elem.outerWidth() + width = Math.min(elem.outerWidth(), 580) if not timeout then width += 20 # Add space for close button if elem.outerHeight() > 55 then elem.addClass("long") elem.css({"width": "50px", "transform": "scale(0.01)"}) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index b78427d6..ff613e87 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -58,7 +58,7 @@ a { color: black } .notification .message-outer { display: table-row } .notification .buttons { display: table-cell; vertical-align: top; padding-top: 9px; } .notification.long .body { padding-top: 10px; padding-bottom: 10px } -.notification .message { display: table-cell; vertical-align: middle; } +.notification .message { display: table-cell; vertical-align: middle; max-width: 400px; white-space: normal; } .notification.visible { max-width: 350px } From 0c659a477d776a787a9c78dcf2ce8403c3677829 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:16:19 +0200 Subject: [PATCH 018/483] Remove hard-coded translate files directory --- src/Translate/Translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Translate/Translate.py b/src/Translate/Translate.py index 4163d333..066133d1 100644 --- a/src/Translate/Translate.py +++ b/src/Translate/Translate.py @@ -28,7 +28,7 @@ class EscapeProxy(dict): class Translate(dict): def __init__(self, lang_dir=None, lang=None): if not lang_dir: - lang_dir = "src/Translate/languages/" + lang_dir = os.path.dirname(__file__) + "/languages/" if not lang: lang = config.language self.lang = lang From 713ff17e91ce5826018baa4f34905f602834d95b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:18:37 +0200 Subject: [PATCH 019/483] Allow load installed third-party plugins and enable/disable plugins in config file data/plugins.json --- src/Plugin/PluginManager.py | 157 +++++++++++++++++++++++++++--------- 1 file changed, 121 insertions(+), 36 deletions(-) diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py index fbf37d3c..dae4f2ec 100644 --- a/src/Plugin/PluginManager.py +++ b/src/Plugin/PluginManager.py @@ -5,62 +5,143 @@ import shutil import time from collections import defaultdict +import importlib +import json + from Debug import Debug from Config import config - import plugins -import importlib - class PluginManager: def __init__(self): self.log = logging.getLogger("PluginManager") - self.plugin_path = os.path.abspath(os.path.dirname(plugins.__file__)) + self.path_plugins = os.path.abspath(os.path.dirname(plugins.__file__)) + self.path_installed_plugins = config.data_dir + "/__plugins__" self.plugins = defaultdict(list) # Registered plugins (key: class name, value: list of plugins for class) self.subclass_order = {} # Record the load order of the plugins, to keep it after reload self.pluggable = {} self.plugin_names = [] # Loaded plugin names - self.after_load = [] # Execute functions after loaded plugins + self.plugins_updated = {} # List of updated plugins since restart + self.plugins_rev = {} # Installed plugins revision numbers + self.after_load = [] # Execute functions after loaded plugins + self.function_flags = {} # Flag function for permissions self.reloading = False + self.config_path = config.data_dir + "/plugins.json" + self.loadConfig() - sys.path.append(os.path.join(os.getcwd(), self.plugin_path)) + self.config.setdefault("builtin", {}) + + sys.path.append(os.path.join(os.getcwd(), self.path_plugins)) self.migratePlugins() if config.debug: # Auto reload Plugins on file change from Debug import DebugReloader DebugReloader.watcher.addCallback(self.reloadPlugins) + def loadConfig(self): + if os.path.isfile(self.config_path): + try: + self.config = json.load(open(self.config_path, encoding="utf8")) + except Exception as err: + self.log.error("Error loading %s: %s" % (self.config_path, err)) + self.config = {} + else: + self.config = {} + + def saveConfig(self): + f = open(self.config_path, "w", encoding="utf8") + json.dump(self.config, f, ensure_ascii=False, sort_keys=True, indent=2) + def migratePlugins(self): - for dir_name in os.listdir(self.plugin_path): + for dir_name in os.listdir(self.path_plugins): if dir_name == "Mute": self.log.info("Deleting deprecated/renamed plugin: %s" % dir_name) - shutil.rmtree("%s/%s" % (self.plugin_path, dir_name)) + shutil.rmtree("%s/%s" % (self.path_plugins, dir_name)) # -- Load / Unload -- + def listPlugins(self, list_disabled=False): + plugins = [] + for dir_name in sorted(os.listdir(self.path_plugins)): + dir_path = os.path.join(self.path_plugins, dir_name) + plugin_name = dir_name.replace("disabled-", "") + if dir_name.startswith("disabled"): + is_enabled = False + else: + is_enabled = True + + plugin_config = self.config["builtin"].get(plugin_name, {}) + if "enabled" in plugin_config: + is_enabled = plugin_config["enabled"] + + if dir_name == "__pycache__" or not os.path.isdir(dir_path): + continue # skip + if dir_name.startswith("Debug") and not config.debug: + continue # Only load in debug mode if module name starts with Debug + if not is_enabled and not list_disabled: + continue # Dont load if disabled + + plugin = {} + plugin["source"] = "builtin" + plugin["name"] = plugin_name + plugin["dir_name"] = dir_name + plugin["dir_path"] = dir_path + plugin["inner_path"] = plugin_name + plugin["enabled"] = is_enabled + plugin["rev"] = config.rev + plugin["loaded"] = plugin_name in self.plugin_names + plugins.append(plugin) + + plugins += self.listInstalledPlugins(list_disabled) + return plugins + + def listInstalledPlugins(self, list_disabled=False): + plugins = [] + + for address, site_plugins in self.config.items(): + if address == "builtin": + continue + for plugin_inner_path, plugin_config in site_plugins.items(): + is_enabled = plugin_config.get("enabled", False) + if not is_enabled and not list_disabled: + continue + plugin_name = os.path.basename(plugin_inner_path) + + dir_path = "%s/%s/%s" % (self.path_installed_plugins, address, plugin_inner_path) + + plugin = {} + plugin["source"] = address + plugin["name"] = plugin_name + plugin["dir_name"] = plugin_name + plugin["dir_path"] = dir_path + plugin["inner_path"] = plugin_inner_path + plugin["enabled"] = is_enabled + plugin["rev"] = plugin_config.get("rev", 0) + plugin["loaded"] = plugin_name in self.plugin_names + plugins.append(plugin) + + return plugins + # Load all plugin def loadPlugins(self): all_loaded = True s = time.time() - for dir_name in sorted(os.listdir(self.plugin_path)): - dir_path = os.path.join(self.plugin_path, dir_name) - if dir_name == "__pycache__": - continue # skip - if dir_name.startswith("disabled"): - continue # Dont load if disabled - if not os.path.isdir(dir_path): - continue # Dont load if not dir - if dir_name.startswith("Debug") and not config.debug: - continue # Only load in debug mode if module name starts with Debug - self.log.debug("Loading plugin: %s" % dir_name) + print(sys.path) + for plugin in self.listPlugins(): + self.log.debug("Loading plugin: %s (%s)" % (plugin["name"], plugin["source"])) + if plugin["source"] != "builtin": + self.plugins_rev[plugin["name"]] = plugin["rev"] + site_plugin_dir = os.path.dirname(plugin["dir_path"]) + if site_plugin_dir not in sys.path: + sys.path.append(site_plugin_dir) try: - __import__(dir_name) + sys.modules[plugin["name"]] = __import__(plugin["dir_name"]) except Exception as err: - self.log.error("Plugin %s load error: %s" % (dir_name, Debug.formatException(err))) + self.log.error("Plugin %s load error: %s" % (plugin["name"], Debug.formatException(err))) all_loaded = False - if dir_name not in self.plugin_names: - self.plugin_names.append(dir_name) + if plugin["name"] not in self.plugin_names: + self.plugin_names.append(plugin["name"]) self.log.debug("Plugins loaded in %.3fs" % (time.time() - s)) for func in self.after_load: @@ -74,19 +155,23 @@ class PluginManager: self.plugins_before = self.plugins self.plugins = defaultdict(list) # Reset registered plugins for module_name, module in list(sys.modules.items()): - if module and getattr(module, "__file__", None) and self.plugin_path in module.__file__: # Module file in plugin_path - if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled - # Re-add non-reloadable plugins - for class_name, classes in self.plugins_before.items(): - for c in classes: - if c.__module__ != module.__name__: - continue - self.plugins[class_name].append(c) - else: - try: - importlib.reload(module) - except Exception as err: - self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err))) + if not module or not getattr(module, "__file__", None): + continue + if self.path_plugins not in module.__file__ and self.path_installed_plugins not in module.__file__: + continue + + if "allow_reload" in dir(module) and not module.allow_reload: # Reload disabled + # Re-add non-reloadable plugins + for class_name, classes in self.plugins_before.items(): + for c in classes: + if c.__module__ != module.__name__: + continue + self.plugins[class_name].append(c) + else: + try: + importlib.reload(module) + except Exception as err: + self.log.error("Plugin %s reload error: %s" % (module_name, Debug.formatException(err))) self.loadPlugins() # Load new plugins From c4a3a53be093da94f993cb40a1961517036bfbc6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:19:05 +0200 Subject: [PATCH 020/483] Also reload source code on file changes in installed plugins --- src/Debug/DebugReloader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Debug/DebugReloader.py b/src/Debug/DebugReloader.py index 29ca7029..4bc0bfbb 100644 --- a/src/Debug/DebugReloader.py +++ b/src/Debug/DebugReloader.py @@ -19,7 +19,9 @@ else: class DebugReloader: - def __init__(self, paths=["src", "plugins"]): + def __init__(self, paths=None): + if not paths: + paths = ["src", "plugins", config.data_dir + "/__plugins__"] self.log = logging.getLogger("DebugReloader") self.last_chaged = 0 self.callbacks = [] From bb705ae863e2943c20500305067cd57f0986f969 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 16:19:35 +0200 Subject: [PATCH 021/483] Fix source code reloader crash on directory modifications/file deletions --- src/Debug/DebugReloader.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Debug/DebugReloader.py b/src/Debug/DebugReloader.py index 4bc0bfbb..482c7921 100644 --- a/src/Debug/DebugReloader.py +++ b/src/Debug/DebugReloader.py @@ -31,6 +31,8 @@ class DebugReloader: event_handler.on_modified = event_handler.on_deleted = self.onChanged event_handler.on_created = event_handler.on_moved = self.onChanged for path in paths: + if not os.path.isdir(path): + continue self.log.debug("Adding autoreload: %s" % path) self.observer.schedule(event_handler, path, recursive=True) self.observer.start() @@ -44,7 +46,10 @@ class DebugReloader: if ext not in ["py", "json"] or "Test" in path or time.time() - self.last_chaged < 1.0: return False self.last_chaged = time.time() - time_modified = os.path.getmtime(path) + if os.path.isfile(path): + time_modified = os.path.getmtime(path) + else: + time_modified = 0 self.log.debug("File changed: %s reloading source code (modified %.3fs ago)" % (evt, time.time() - time_modified)) if time.time() - time_modified > 5: # Probably it's just an attribute change, ignore it return False From f40c3e6b8196025dab744d805dcfec420d977542 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 2 Aug 2019 20:11:00 +0200 Subject: [PATCH 022/483] Add notification messages max-width --- src/Ui/media/all.css | 2 +- src/Ui/media/all.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index d742c3b5..c1b25cdf 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -62,7 +62,7 @@ a { color: black } .notification .message-outer { display: table-row } .notification .buttons { display: table-cell; vertical-align: top; padding-top: 9px; } .notification.long .body { padding-top: 10px; padding-bottom: 10px } -.notification .message { display: table-cell; vertical-align: middle; } +.notification .message { display: table-cell; vertical-align: middle; max-width: 400px; white-space: normal; } .notification.visible { max-width: 350px } diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index 86c61b51..e41d2476 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -835,7 +835,7 @@ $.extend( $.easing, }; })(this)), timeout); } - width = elem.outerWidth(); + width = Math.min(elem.outerWidth(), 580); if (!timeout) { width += 20; } @@ -901,6 +901,7 @@ $.extend( $.easing, }).call(this); + /* ---- src/Ui/media/Wrapper.coffee ---- */ @@ -2091,4 +2092,4 @@ $.extend( $.easing, } }); -}).call(this); +}).call(this); \ No newline at end of file From 0877fec638af66e4e03f00ba022197fb18251c24 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Aug 2019 01:29:27 +0200 Subject: [PATCH 023/483] Restrict plugin commands in multi-user mode --- plugins/disabled-Multiuser/MultiuserPlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 2406e224..96354245 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -106,7 +106,8 @@ class UiWebsocketPlugin(object): "siteSetOwned", "siteSetAutodownloadoptional", "dbReload", "dbRebuild", "mergerSiteDelete", "siteSetLimit", "siteSetAutodownloadBigfileLimit", "optionalLimitSet", "optionalHelp", "optionalHelpRemove", "optionalHelpAll", "optionalFilePin", "optionalFileUnpin", "optionalFileDelete", - "muteAdd", "muteRemove", "siteblockAdd", "siteblockRemove", "filterIncludeAdd", "filterIncludeRemove" + "muteAdd", "muteRemove", "siteblockAdd", "siteblockRemove", "filterIncludeAdd", "filterIncludeRemove", + "pluginConfigSet", "pluginAdd", "pluginRemove", "pluginUpdate" ) if config.multiuser_no_new_sites: self.multiuser_denied_cmds += ("mergerSiteAdd", ) From 4094d3a9bf0d0e99b523a5f77a91298263452740 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Aug 2019 01:31:11 +0200 Subject: [PATCH 024/483] Plugin to install, update and delete third-party plugins using the web interface --- .../UiPluginManager/UiPluginManagerPlugin.py | 220 +++ plugins/UiPluginManager/__init__.py | 1 + .../media/css/PluginManager.css | 75 + plugins/UiPluginManager/media/css/all.css | 129 ++ plugins/UiPluginManager/media/css/button.css | 12 + plugins/UiPluginManager/media/css/fonts.css | 30 + plugins/UiPluginManager/media/img/loading.gif | Bin 0 -> 723 bytes .../media/js/PluginList.coffee | 132 ++ .../media/js/UiPluginManager.coffee | 71 + plugins/UiPluginManager/media/js/all.js | 1606 +++++++++++++++++ .../UiPluginManager/media/js/lib/Class.coffee | 23 + .../media/js/lib/Promise.coffee | 74 + .../media/js/lib/Prototypes.coffee | 8 + .../UiPluginManager/media/js/lib/maquette.js | 770 ++++++++ .../media/js/utils/Animation.coffee | 138 ++ .../media/js/utils/Dollar.coffee | 3 + .../media/js/utils/ZeroFrame.coffee | 85 + .../UiPluginManager/media/plugin_manager.html | 19 + 18 files changed, 3396 insertions(+) create mode 100644 plugins/UiPluginManager/UiPluginManagerPlugin.py create mode 100644 plugins/UiPluginManager/__init__.py create mode 100644 plugins/UiPluginManager/media/css/PluginManager.css create mode 100644 plugins/UiPluginManager/media/css/all.css create mode 100644 plugins/UiPluginManager/media/css/button.css create mode 100644 plugins/UiPluginManager/media/css/fonts.css create mode 100644 plugins/UiPluginManager/media/img/loading.gif create mode 100644 plugins/UiPluginManager/media/js/PluginList.coffee create mode 100644 plugins/UiPluginManager/media/js/UiPluginManager.coffee create mode 100644 plugins/UiPluginManager/media/js/all.js create mode 100644 plugins/UiPluginManager/media/js/lib/Class.coffee create mode 100644 plugins/UiPluginManager/media/js/lib/Promise.coffee create mode 100644 plugins/UiPluginManager/media/js/lib/Prototypes.coffee create mode 100644 plugins/UiPluginManager/media/js/lib/maquette.js create mode 100644 plugins/UiPluginManager/media/js/utils/Animation.coffee create mode 100644 plugins/UiPluginManager/media/js/utils/Dollar.coffee create mode 100644 plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee create mode 100644 plugins/UiPluginManager/media/plugin_manager.html diff --git a/plugins/UiPluginManager/UiPluginManagerPlugin.py b/plugins/UiPluginManager/UiPluginManagerPlugin.py new file mode 100644 index 00000000..df6236db --- /dev/null +++ b/plugins/UiPluginManager/UiPluginManagerPlugin.py @@ -0,0 +1,220 @@ +import io +import os +import json +import shutil +import time + +from Plugin import PluginManager +from Config import config +from Debug import Debug +from Translate import Translate + + +plugin_dir = os.path.dirname(__file__) + +if "_" not in locals(): + _ = Translate(plugin_dir + "/languages/") + + +@PluginManager.afterLoad +def importPluginnedClasses(): + from Ui import UiWebsocket + UiWebsocket.admin_commands.update([ + "pluginList", "pluginConfigSet", "pluginAdd", "pluginRemove", "pluginUpdate" + ]) + + +# Convert non-str,int,float values to str in a dict +def restrictDictValues(input_dict): + allowed_types = (int, str, float) + return { + key: val if type(val) in allowed_types else str(val) + for key, val in input_dict.items() + } + + +@PluginManager.registerTo("UiRequest") +class UiRequestPlugin(object): + def actionWrapper(self, path, extra_headers=None): + if path.strip("/") != "Plugins": + return super(UiRequestPlugin, self).actionWrapper(path, extra_headers) + + if not extra_headers: + extra_headers = {} + + script_nonce = self.getScriptNonce() + + self.sendHeader(extra_headers=extra_headers, script_nonce=script_nonce) + site = self.server.site_manager.get(config.homepage) + return iter([super(UiRequestPlugin, self).renderWrapper( + site, path, "uimedia/plugins/plugin_manager/plugin_manager.html", + "Plugin Manager", extra_headers, show_loadingscreen=False, script_nonce=script_nonce + )]) + + def actionUiMedia(self, path, *args, **kwargs): + if path.startswith("/uimedia/plugins/plugin_manager/"): + file_path = path.replace("/uimedia/plugins/plugin_manager/", plugin_dir + "/media/") + if config.debug and (file_path.endswith("all.js") or file_path.endswith("all.css")): + # If debugging merge *.css to all.css and *.js to all.js + from Debug import DebugMedia + DebugMedia.merge(file_path) + + if file_path.endswith("js"): + data = _.translateData(open(file_path).read(), mode="js").encode("utf8") + elif file_path.endswith("html"): + data = _.translateData(open(file_path).read(), mode="html").encode("utf8") + else: + data = open(file_path, "rb").read() + + return self.actionFile(file_path, file_obj=io.BytesIO(data), file_size=len(data)) + else: + return super(UiRequestPlugin, self).actionUiMedia(path) + + +@PluginManager.registerTo("UiWebsocket") +class UiWebsocketPlugin(object): + def actionPluginList(self, to): + plugins = [] + for plugin in PluginManager.plugin_manager.listPlugins(list_disabled=True): + plugin_info_path = plugin["dir_path"] + "/plugin_info.json" + plugin_info = {} + if os.path.isfile(plugin_info_path): + try: + plugin_info = json.load(open(plugin_info_path)) + except Exception as err: + self.log.error( + "Error loading plugin info for %s: %s" % + (plugin["name"], Debug.formatException(err)) + ) + if plugin_info: + plugin_info = restrictDictValues(plugin_info) # For security reasons don't allow complex values + plugin["info"] = plugin_info + + if plugin["source"] != "builtin": + plugin_site = self.server.sites.get(plugin["source"]) + if plugin_site: + try: + plugin_site_info = plugin_site.storage.loadJson(plugin["inner_path"] + "/plugin_info.json") + plugin_site_info = restrictDictValues(plugin_site_info) + plugin["site_info"] = plugin_site_info + plugin["site_title"] = plugin_site.content_manager.contents["content.json"].get("title") + plugin_key = "%s/%s" % (plugin["source"], plugin["inner_path"]) + plugin["updated"] = plugin_key in PluginManager.plugin_manager.plugins_updated + except Exception: + pass + + plugins.append(plugin) + + return {"plugins": plugins} + + def actionPluginConfigSet(self, to, source, inner_path, key, value): + plugin_manager = PluginManager.plugin_manager + plugins = plugin_manager.listPlugins(list_disabled=True) + plugin = None + for item in plugins: + if item["source"] == source and item["inner_path"] in (inner_path, "disabled-" + inner_path): + plugin = item + break + + if not plugin: + return {"error": "Plugin not found"} + + config_source = plugin_manager.config.setdefault(source, {}) + config_plugin = config_source.setdefault(inner_path, {}) + + if key in config_plugin and value is None: + del config_plugin[key] + else: + config_plugin[key] = value + + plugin_manager.saveConfig() + + return "ok" + + def pluginAction(self, action, address, inner_path): + site = self.server.sites.get(address) + plugin_manager = PluginManager.plugin_manager + + # Install/update path should exists + if action in ("add", "update", "add_request"): + if not site: + raise Exception("Site not found") + + if not site.storage.isDir(inner_path): + raise Exception("Directory not found on the site") + + try: + plugin_info = site.storage.loadJson(inner_path + "/plugin_info.json") + plugin_data = (plugin_info["rev"], plugin_info["description"], plugin_info["name"]) + except Exception as err: + raise Exception("Invalid plugin_info.json: %s" % Debug.formatExceptionMessage(err)) + + source_path = site.storage.getPath(inner_path) + + target_path = plugin_manager.path_installed_plugins + "/" + address + "/" + inner_path + plugin_config = plugin_manager.config.setdefault(site.address, {}).setdefault(inner_path, {}) + + # Make sure plugin (not)installed + if action in ("add", "add_request") and os.path.isdir(target_path): + raise Exception("Plugin already installed") + + if action in ("update", "remove") and not os.path.isdir(target_path): + raise Exception("Plugin not installed") + + # Do actions + if action == "add": + shutil.copytree(source_path, target_path) + + plugin_config["date_added"] = int(time.time()) + plugin_config["rev"] = plugin_info["rev"] + plugin_config["enabled"] = True + + if action == "update": + shutil.rmtree(target_path) + + shutil.copytree(source_path, target_path) + + plugin_config["rev"] = plugin_info["rev"] + plugin_config["date_updated"] = time.time() + + if action == "remove": + del plugin_manager.config[address][inner_path] + shutil.rmtree(target_path) + + def doPluginAdd(self, to, inner_path, res): + if not res: + return None + + self.pluginAction("add", self.site.address, inner_path) + PluginManager.plugin_manager.saveConfig() + + self.cmd( + "confirm", + ["Plugin installed!
You have to restart the client to load the plugin", "Restart"], + lambda res: self.actionServerShutdown(to, restart=True) + ) + + self.response(to, "ok") + + def actionPluginAddRequest(self, to, inner_path): + self.pluginAction("add_request", self.site.address, inner_path) + plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json") + warning = "Warning!
Plugins has the same permissions as the ZeroNet client.
" + warning += "Do not install it if you don't trust the developer.
" + + self.cmd( + "confirm", + ["Install new plugin: %s?
%s" % (plugin_info["name"], warning), "Trust & Install"], + lambda res: self.doPluginAdd(to, inner_path, res) + ) + + def actionPluginRemove(self, to, address, inner_path): + self.pluginAction("remove", address, inner_path) + PluginManager.plugin_manager.saveConfig() + return "ok" + + def actionPluginUpdate(self, to, address, inner_path): + self.pluginAction("update", address, inner_path) + PluginManager.plugin_manager.saveConfig() + PluginManager.plugin_manager.plugins_updated["%s/%s" % (address, inner_path)] = True + return "ok" diff --git a/plugins/UiPluginManager/__init__.py b/plugins/UiPluginManager/__init__.py new file mode 100644 index 00000000..d29ae44b --- /dev/null +++ b/plugins/UiPluginManager/__init__.py @@ -0,0 +1 @@ +from . import UiPluginManagerPlugin diff --git a/plugins/UiPluginManager/media/css/PluginManager.css b/plugins/UiPluginManager/media/css/PluginManager.css new file mode 100644 index 00000000..30f36717 --- /dev/null +++ b/plugins/UiPluginManager/media/css/PluginManager.css @@ -0,0 +1,75 @@ +body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; } +h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px } +h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } +h2 { margin-top: 10px; } +h3 { font-weight: normal } +h4 { font-size: 19px; font-weight: lighter; margin-right: 100px; margin-top: 30px; } +a { color: #9760F9 } +a:hover { text-decoration: none } + +.link { background-color: transparent; outline: 5px solid transparent; transition: all 0.3s } +.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; transition: none } + +.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; box-sizing: border-box; padding-bottom: 150px; } +.section { margin: 0px 10%; } +.plugins { font-size: 19px; margin-top: 25px; margin-bottom: 75px; } +.plugin { transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: relative; padding-bottom: 20px; padding-top: 10px; } +.plugin.hidden { opacity: 0; height: 0px; padding: 0px; } +.plugin .title { display: inline-block; line-height: 36px; } +.plugin .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; } +.plugin .title .version { font-size: 70%; margin-left: 5px; } +.plugin .title .version .version-latest { color: #2ecc71; font-weight: normal; } +.plugin .title .version .version-missing { color: #ffa200; font-weight: normal; } +.plugin .title .version .version-update { padding: 0px 15px; margin-left: 5px; line-height: 28px; } +.plugin .description { font-size: 14px; color: #666; line-height: 24px; } +.plugin .description .source { color: #999; font-size: 90%; } +.plugin .description .source a { color: #666; } +.plugin .value { display: inline-block; white-space: nowrap; } +.plugin .value-right { right: 0px; position: absolute; } +.plugin .value-fullwidth { width: 100% } +.plugin .marker { + font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px; + opacity: 0; pointer-events: none; transition: all 0.6s; transform: scale(2); color: #9760F9; +} +.plugin .marker.visible { opacity: 1; pointer-events: all; transform: scale(1); } +.plugin .marker.changed { color: #2ecc71; } +.plugin .marker.pending { color: #ffa200; } + + +.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; border-radius: 3px; font-size: 17px; box-sizing: border-box; } +.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; } +.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; } + +.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; } + +.value-right .input-text { text-align: right; width: 100px; } +.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; } +.value-fullwidth { margin-top: 10px; } + +/* Checkbox */ +.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; border-radius: 15px; transition: all 0.3s ease-in-out; display: inline-block; } +.checkbox-skin:before { + content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; border-radius: 100%; margin-top: 2px; margin-left: 2px; + transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); +} +.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; } +.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px } +.checkbox.checked .checkbox-skin:before { margin-left: 27px; } +.checkbox.checked .checkbox-skin { background-color: #2ECC71 } + +/* Bottom */ + +.bottom { + width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); + transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box; +} +.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; } +.bottom .button { float: right; } +.bottom.visible { bottom: 0px; box-shadow: 0px 0px 35px #dcdcdc; } +.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; } +.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; } +.bottom-restart .title:before { color: #ffa200; } + +.animate { transition: all 0.3s ease-out !important; } +.animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; } +.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; } \ No newline at end of file diff --git a/plugins/UiPluginManager/media/css/all.css b/plugins/UiPluginManager/media/css/all.css new file mode 100644 index 00000000..ba72fa0d --- /dev/null +++ b/plugins/UiPluginManager/media/css/all.css @@ -0,0 +1,129 @@ + +/* ---- PluginManager.css ---- */ + + +body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; } +h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px } +h1 { background: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } +h2 { margin-top: 10px; } +h3 { font-weight: normal } +h4 { font-size: 19px; font-weight: lighter; margin-right: 100px; margin-top: 30px; } +a { color: #9760F9 } +a:hover { text-decoration: none } + +.link { background-color: transparent; outline: 5px solid transparent; -webkit-transition: all 0.3s ; -moz-transition: all 0.3s ; -o-transition: all 0.3s ; -ms-transition: all 0.3s ; transition: all 0.3s } +.link:active { background-color: #EFEFEF; outline: 5px solid #EFEFEF; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } + +.content { max-width: 800px; margin: auto; background-color: white; padding: 60px 20px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; padding-bottom: 150px; } +.section { margin: 0px 10%; } +.plugins { font-size: 19px; margin-top: 25px; margin-bottom: 75px; } +.plugin { -webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: relative; padding-bottom: 20px; padding-top: 10px; } +.plugin.hidden { opacity: 0; height: 0px; padding: 0px; } +.plugin .title { display: inline-block; line-height: 36px; } +.plugin .title h3 { font-size: 20px; font-weight: lighter; margin-right: 100px; } +.plugin .title .version { font-size: 70%; margin-left: 5px; } +.plugin .title .version .version-latest { color: #2ecc71; font-weight: normal; } +.plugin .title .version .version-missing { color: #ffa200; font-weight: normal; } +.plugin .title .version .version-update { padding: 0px 15px; margin-left: 5px; line-height: 28px; } +.plugin .description { font-size: 14px; color: #666; line-height: 24px; } +.plugin .description .source { color: #999; font-size: 90%; } +.plugin .description .source a { color: #666; } +.plugin .value { display: inline-block; white-space: nowrap; } +.plugin .value-right { right: 0px; position: absolute; } +.plugin .value-fullwidth { width: 100% } +.plugin .marker { + font-weight: bold; text-decoration: none; font-size: 25px; position: absolute; padding: 2px 15px; line-height: 32px; + opacity: 0; pointer-events: none; -webkit-transition: all 0.6s; -moz-transition: all 0.6s; -o-transition: all 0.6s; -ms-transition: all 0.6s; transition: all 0.6s ; -webkit-transform: scale(2); -moz-transform: scale(2); -o-transform: scale(2); -ms-transform: scale(2); transform: scale(2) ; color: #9760F9; +} +.plugin .marker.visible { opacity: 1; pointer-events: all; -webkit-transform: scale(1); -moz-transform: scale(1); -o-transform: scale(1); -ms-transform: scale(1); transform: scale(1) ; } +.plugin .marker.changed { color: #2ecc71; } +.plugin .marker.pending { color: #ffa200; } + + +.input-text, .input-select { padding: 8px 18px; border: 1px solid #CCC; -webkit-border-radius: 3px; -moz-border-radius: 3px; -o-border-radius: 3px; -ms-border-radius: 3px; border-radius: 3px ; font-size: 17px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; } +.input-text:focus, .input-select:focus { border: 1px solid #3396ff; outline: none; } +.input-textarea { overflow-x: auto; overflow-y: hidden; white-space: pre; line-height: 22px; } + +.input-select { width: initial; font-size: 14px; padding-right: 10px; padding-left: 10px; } + +.value-right .input-text { text-align: right; width: 100px; } +.value-fullwidth .input-text { width: 100%; font-size: 14px; font-family: 'Segoe UI', Arial, 'Helvetica Neue'; } +.value-fullwidth { margin-top: 10px; } + +/* Checkbox */ +.checkbox-skin { background-color: #CCC; width: 50px; height: 24px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; border-radius: 15px ; -webkit-transition: all 0.3s ease-in-out; -moz-transition: all 0.3s ease-in-out; -o-transition: all 0.3s ease-in-out; -ms-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out ; display: inline-block; } +.checkbox-skin:before { + content: ""; position: relative; width: 20px; background-color: white; height: 20px; display: block; -webkit-border-radius: 100%; -moz-border-radius: 100%; -o-border-radius: 100%; -ms-border-radius: 100%; border-radius: 100% ; margin-top: 2px; margin-left: 2px; + -webkit-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -moz-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -o-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); -ms-transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86); transition: all 0.5s cubic-bezier(0.785, 0.135, 0.15, 0.86) ; +} +.checkbox { font-size: 14px; font-weight: normal; display: inline-block; cursor: pointer; margin-top: 5px; } +.checkbox .title { display: inline; line-height: 30px; vertical-align: 4px; margin-left: 11px } +.checkbox.checked .checkbox-skin:before { margin-left: 27px; } +.checkbox.checked .checkbox-skin { background-color: #2ECC71 } + +/* Bottom */ + +.bottom { + width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); + -webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; +} +.bottom-content { max-width: 750px; width: 100%; margin: 0px auto; } +.bottom .button { float: right; } +.bottom.visible { bottom: 0px; -webkit-box-shadow: 0px 0px 35px #dcdcdc; -moz-box-shadow: 0px 0px 35px #dcdcdc; -o-box-shadow: 0px 0px 35px #dcdcdc; -ms-box-shadow: 0px 0px 35px #dcdcdc; box-shadow: 0px 0px 35px #dcdcdc ; } +.bottom .title { padding: 10px 10px; color: #363636; float: left; text-transform: uppercase; letter-spacing: 1px; } +.bottom .title:before { content: "•"; display: inline-block; color: #2ecc71; font-size: 31px; vertical-align: -7px; margin-right: 8px; line-height: 25px; } +.bottom-restart .title:before { color: #ffa200; } + +.animate { -webkit-transition: all 0.3s ease-out !important; -moz-transition: all 0.3s ease-out !important; -o-transition: all 0.3s ease-out !important; -ms-transition: all 0.3s ease-out !important; transition: all 0.3s ease-out !important ; } +.animate-back { -webkit-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -moz-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -o-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; -ms-transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important ; } +.animate-inout { -webkit-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -moz-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -o-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -ms-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important ; } + +/* ---- button.css ---- */ + + +/* Button */ +.button { + background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center; + -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; border-bottom: 2px solid #E8BE29; -webkit-transition: all 0.5s ease-out; -moz-transition: all 0.5s ease-out; -o-transition: all 0.5s ease-out; -ms-transition: all 0.5s ease-out; transition: all 0.5s ease-out ; text-decoration: none; +} +.button:hover { border-color: white; border-bottom: 2px solid #BD960C; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none ; background-color: #FDEB07 } +.button:active { position: relative; top: 1px } +.button.loading { + color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center; + -webkit-transition: all 0.5s ease-out ; -moz-transition: all 0.5s ease-out ; -o-transition: all 0.5s ease-out ; -ms-transition: all 0.5s ease-out ; transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666 +} +.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } + +/* ---- fonts.css ---- */ + + +/* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */ +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on January 21, 2015 */ + + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: + local('Roboto'), + url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: bold; + src: + local('Roboto Medium'), + url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 200; + src: + local('Roboto Light'), + url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); +} \ No newline at end of file diff --git a/plugins/UiPluginManager/media/css/button.css b/plugins/UiPluginManager/media/css/button.css new file mode 100644 index 00000000..9f46d478 --- /dev/null +++ b/plugins/UiPluginManager/media/css/button.css @@ -0,0 +1,12 @@ +/* Button */ +.button { + background-color: #FFDC00; color: black; padding: 10px 20px; display: inline-block; background-position: left center; + border-radius: 2px; border-bottom: 2px solid #E8BE29; transition: all 0.5s ease-out; text-decoration: none; +} +.button:hover { border-color: white; border-bottom: 2px solid #BD960C; transition: none ; background-color: #FDEB07 } +.button:active { position: relative; top: 1px } +.button.loading { + color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center; + transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666 +} +.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } \ No newline at end of file diff --git a/plugins/UiPluginManager/media/css/fonts.css b/plugins/UiPluginManager/media/css/fonts.css new file mode 100644 index 00000000..f5576c5a --- /dev/null +++ b/plugins/UiPluginManager/media/css/fonts.css @@ -0,0 +1,30 @@ +/* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */ +/* Generated by Font Squirrel (http://www.fontsquirrel.com) on January 21, 2015 */ + + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + src: + local('Roboto'), + url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: bold; + src: + local('Roboto Medium'), + url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff'), +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 200; + src: + local('Roboto Light'), + url(data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAEScABMAAAAAdFQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABqAAAABwAAAAcXzC5yUdERUYAAAHEAAAAHgAAACAAzgAER1BPUwAAAeQAAAVxAAANIkezYOlHU1VCAAAHWAAAACwAAAAwuP+4/k9TLzIAAAeEAAAAVgAAAGC3ouDrY21hcAAAB9wAAAG+AAACioYHy/VjdnQgAAAJnAAAADQAAAA0CnAOGGZwZ20AAAnQAAABsQAAAmVTtC+nZ2FzcAAAC4QAAAAIAAAACAAAABBnbHlmAAALjAAAMaIAAFTUMXgLR2hlYWQAAD0wAAAAMQAAADYBsFYkaGhlYQAAPWQAAAAfAAAAJA7cBhlobXR4AAA9hAAAAeEAAAKEbjk+b2xvY2EAAD9oAAABNgAAAUQwY0cibWF4cAAAQKAAAAAgAAAAIAG+AZluYW1lAABAwAAAAZAAAANoT6qDDHBvc3QAAEJQAAABjAAAAktoPRGfcHJlcAAAQ9wAAAC2AAABI0qzIoZ3ZWJmAABElAAAAAYAAAAGVU1R3QAAAAEAAAAAzD2izwAAAADE8BEuAAAAAM4DBct42mNgZGBg4ANiCQYQYGJgBMIFQMwC5jEAAAsqANMAAHjapZZ5bNRFFMff79dtd7u03UNsORWwKYhWGwFLsRBiGuSKkdIDsBg0kRCVGq6GcpSEFINKghzlMDFBVBITNRpDJEGCBlBBRSEQIQYJyLHd/pA78a99fn6zy3ZbykJxXr7zm3nz5s2b7xy/EUtE/FIiY8SuGDe5SvLeeHlhvfQRD3pRFbc9tWy9/ur8evG5JQOP2Hxt8ds7xLJrjO1AmYxUyiyZLQtlpayRmOWx/FbQGmSVWM9aVdZs6z1rk/WZFbU9dtgutIeCsVivND1dsWSG9JAMKZOeMkrCUi756MI6AN0g3Se1ellm6GlqOXpBxuoNmYXGlgn6D/qo9JOA5ksIFOoBKY79K6V4qtC/ZJy2yXNgPJgIKkEVqMbPNHpO14jUgXr6LcK+gbbFoBEsoX0pWE55Bd8W/G8BW9WNboZ+b/KPyWslDy5K9biU6TkZpY6U6ymiLdUv0Vyi9jvt1boT+x9lTmyXzNUhaHKIcqyEaDkLfw8YTQBNDpo2NHmsVjZtrl2u/kZLmDlHaT0BJ1HTZ45+gbdfTSznJVOK4WQkWAAWgiYQQB/EVzAxYhheIvASgZcIvETgJGK8NfDdgN1GsAlsBllYO1g7WDtYO1g7WDrMcAK+a2UA6xci+kp0i0EjWA4s2nMZO6DNrE4zDDbDYDMMNptIHSJ1iNQhUodI3R4DafGzG8JSKEUyRB6VJ+RJGSbDZQSrWsb+KJfR7OAJ8rxUM/Z0xq6Tl6Re3iTyjUS9WezsQ+7e9L7j24G//uznFl2th/WAOrqPNelG0hq5z6Srk6Ub4Kau0Mv6qe7W7ZQPsxIhPcgeX3sPns6DCDjYSX/9rj3/7ka8bbeNGQXHE/UzyZb3Naqtt/W+FAepZ1J3mVOWPoW7ipYzFE8hSiE3Erfcabyo/I+kF7TVzPBMiq6VU3Wr/FGy9F2y1MD5aLfeG7ukh3SKztOQHtOldxmvgTW/3uWKBeLrqifdSuxbPeNypiOTPb/StfqBbgBrYCOIKkifoH6ou3S//oxFky4jLzLWvTSoV/RrU96pR/UY36Mdx9VzerNDbA+b/M8UzXE97TKTYCcvdY079Fxl8v2duY3vJb3Y3lvbjK+QWdMjScujKb226ze6V0+AH9gHId3G3ghxPk5yZs+m2BVzo4j+otuYZ3wX5ibGa4uP3R5tYufcaU32pGm7er+ninU2ffVaVz47Mt+tHXstTVvae0Cv3PeYTjqG4n5v927ukWDyTnDucuZXdXEerpqzcsc10D9M3nKnmNPFnZ6n7nOlY/RxrdBhYDA7yovKyx/Mq5N0vr6l67EIaA4ne4k5369QP6Kvpd4r8RRjZ+hP4PPkPrp4i832qOJ/AP1E1+ke7uE9nPDWJJ+Jrx4Cu92zEZtr6m93h6H2O7CDtjENA6eSpZOdzwL/84C8m3g93kuyeVN44C/L1LyIT7J5D3gNqz0SVjloc7lZuAc7/RfC3NHu/+dBU8tP6vORAnN/90poeoM+5H3vIaYsM3omo/oYwfVdgLgpk6+vWxvGSuQWfkuMV4v5+Q1TAaIMIr2ZVYhyIWLzCipijKGIT4qRPvIU4uNFNJz8aaQvL6NSeBqJ+HkjlcHUKCRHnkEKeDGVw9dopJdUIBkyTsbD80TEIy/IFKKoRLJkKpIpVYhHahCvTEPyeGVNJ7oXkX68tuooz0SCvLrqiXCezCeSBbz//bIIyZAGxCOLpRGfS2QpHpYhPlmOZEkT4pcVSJ6sk/XM1325WdKC5JsXnCVbZCtlG75djiSFI9uwkwE37hv6Md6G2cx+NJYVzKs3MxtPlJOQ/sxtqjzEO7FaBpk5PMIMZtKznvgGm/hKiKsJPjcw3oj/AIgWgIQAAAB42mNgZGBg4GLQYdBjYHJx8wlh4MtJLMljkGBgAYoz/P8PJBAsIAAAnsoHa3jaY2BmvsGow8DKwMI6i9WYgYFRHkIzX2RIY2JgYABhCHjAwPQ/gEEhGshUAPHd8/PTgRTvAwa2tH9pDAwcSUzBCgyM8/0ZGRhYrFg3gNUxAQCExA4aAAB42mNgYGBmgGAZBkYgycDYAuQxgvksjBlAOozBgYGVQQzI4mWoY1jAsJhhKcNKhtUM6xi2MOxg2M1wkOEkw1mGywzXGG4x3GF4yPCS4S3DZ4ZvDL8Y/jAGMhYyHWO6xXRHgUtBREFKQU5BTUFfwUohXmGNotIDhv//QTYCzVUAmrsIaO4KoLlriTA3gLEAai6DgoCChIIM2FxLJHMZ/3/9//j/of8H/x/4v+//3v97/m//v+X/pv9r/y/7v/j/vP9z/s/8P+P/lP+9/7v+t/5v/t/wv/6/zn++v7v+Lv+77EHzg7oH1Q+qHhQ/yH6Q9MDu/qf7tQoLIOFDC8DIxgA3nJEJSDChKwBGEQsrGzsHJxc3Dy8fv4CgkLCIqJi4hKSUtIysnLyCopKyiqqauoamlraOrp6+gaGRsYmpmbmFpZW1ja2dvYOjk7OLq5u7h6eXt4+vn39AYFBwSGhYeERkVHRMbFx8QiLIlnyGopJSiIVlQFwOYlQwMFQyVDEwVDMwJKeABLLS52enQZ2ViumVjNyZSWDGxEnTpk+eAmbOmz0HRE2dASTyGBgKgFQhEBcDcUMTkGjMARIAqVuf0QAAAAAEOgWvAGYAqABiAGUAZwBoAGkAagBrAHUApABcAHgAZQBsAHIAeAB8AHAAegBaAEQFEXjaXVG7TltBEN0NDwOBxNggOdoUs5mQxnuhBQnE1Y1iZDuF5QhpN3KRi3EBH0CBRA3arxmgoaRImwYhF0h8Qj4hEjNriKI0Ozuzc86ZM0vKkap36WvPU+ckkMLdBs02/U5ItbMA96Tr642MtIMHWmxm9Mp1+/4LBpvRlDtqAOU9bykPGU07gVq0p/7R/AqG+/wf8zsYtDTT9NQ6CekhBOabcUuD7xnNussP+oLV4WIwMKSYpuIuP6ZS/rc052rLsLWR0byDMxH5yTRAU2ttBJr+1CHV83EUS5DLprE2mJiy/iQTwYXJdFVTtcz42sFdsrPoYIMqzYEH2MNWeQweDg8mFNK3JMosDRH2YqvECBGTHAo55dzJ/qRA+UgSxrxJSjvjhrUGxpHXwKA2T7P/PJtNbW8dwvhZHMF3vxlLOvjIhtoYEWI7YimACURCRlX5hhrPvSwG5FL7z0CUgOXxj3+dCLTu2EQ8l7V1DjFWCHp+29zyy4q7VrnOi0J3b6pqqNIpzftezr7HA54eC8NBY8Gbz/v+SoH6PCyuNGgOBEN6N3r/orXqiKu8Fz6yJ9O/sVoAAAAAAQAB//8AD3jarXwHfBRl+v/7TtuWLbMlm54smwIJJLBLCKGJCOqJgIp6NBEiiUgNiCb0IgiIFU9FkKCABKXNbAIqcoAUC3Y9I6ioh5yaE8RT9CeQHf7P885sCgS4/+/zE7OZzO7O+z79+5QZwpG+hHBjxNsIT0wkX6WkoEfEJCScDKmS+FWPCM/BIVF5PC3i6YhJSmzoEaF4PiwH5KyAHOjLZWiZdIU2Vrzt7Ka+wvsELkmqCKHtRYVdt4BE4FyeSoX6iMiRPKqYCxShTiEh1eSsV7iQaqF5RBWp7FaE4o6dwoVhHy+H5apHH6iorqZf85805OM15wrd6edSAhGJjfSCa1KSp0jhWk4gFiFPMYeoEleg0DpVcNXXii6SBCcFl2qieaoVztjYGdUOS3XslExxjbAHX+fyZYFqoTQgdCfnvz6snaPcl/AK611DiLAGaEgm6fRmEkkCGiK++MRwOBwxARkRsy0OjmsJTTLZ82o4OSU10x9WiaO+xutPSM70h2pFgb3Fu9LS8S1RrK+RLFY7vEWVjAIlqU5NdNUrifomza76iMlszavpbRIsQI9LjYezPjjri8ezPg+c9blUG5yNc9WrAZqndEna2etfp3OJL8+6s9e3p514oCS5argkkwfWZa8SvsIiNZZEMxzEu2qs8TYPXqrG7ouDD7jYq8xevfiKn/Gzz8C3Eti34JrJseukxK6Tip+pSYt9Mh3P871dHI9EumTkQkpqWnr+Bf8pvZNABJ7CgCcAP2Eef8K+IB/wBfigB3+K4K1rqGuwVk/bDRoziHaDl3/9z2ByXjs1YMwA7S14uY92G6y9SVfeQV8bRZ/X2M8o7bo7tDK6En/gPKggqTzfkY9Kj5AO5CkSyQMJKm1BDub6SJ6IPM3LteRFZBCm4g2rKZb6iJyCp2W3BbQ0v0Bx1KnpoKIko05WOXe9ku5SZWB7bkj1guDahhSvSzXDicSQmuWsV/3uerUAxCOngyrHFSteucYmprTJ9BcrZrcSLCZqiii7txPq8CdkwVngQlHYGx8OdSnsnJ2TTws7dykClUyjThrsnB1sI/m88f406vNKJl+wMJ9W8uWHHvvblsd3fPT225vLtu3l+PLnH//bs0ve+PCtj5TS7afoc5L63KqKSQ9f3WfnS2vfcxw65Pr+gLhi96r7py7r3e+V6g1vOXb/3fYxWNCk8z+JC8WDxI7aDdzpTh7S+aN2ctRHBOCImuCor+2amSfY89SucCjb2KHsqKdKjwKF1KkOYIHDpXp13UWFzYDDfDjMd6md4bAtaGlP+O11yO4am5ACRlCsds6HP1Iz89LgD6J27SS71ZT04mI1QYaj1LRiZArwIRyKT6VeKdgmu4gxqCfVGeKhfpp1mfcnrZ43d/Vzc+ZXjbprxNDRJcOG3VXLvXVDtJjOgTeqVsMbo0v0N0qE/gPmbt06d8CcLVvmDJk1a8iAIXPmDGmQhakdzz26euCcrVvnDIy9NXD4jJnDCHiz4ed/El4DvrUhHUlPUkEiKegVMpBx2VJ9xIqM684Di3oxFgVBeYK6eXeCw04utSsc2kGT7C7VB4fxcr16FfxGPmy3ChnZHWRkks8OTHInprZjTOqeLbt3EJM9MbVDZ11rOne5ijJ1ATaAdjgp7QUeDdTEbwrmOGgjV4rgUzkmB/WAHhXBRxiPhj+x1HnzwMiqx18adtsa+lynLpP+0u81bumM2w7d9/Hpyk1rR2y7VisRTVzBtEEPXXW12q3TPSPLJtN7K98YYxvz4l+rNq+dOWzB1TO09OuUMfM+/+th8ZGBt9ZFZlVffw09JpqEzJEruEN9Hr1pYYeSroPGLgAbnCb0IceY387WvbbhsqkiXeCvkVGN3nmauSxb6EOt7+3XThK05Ye1TtxEaSiRiYdQxc0YbAWr87AveQpdpCidSpzsc7mBDdnkYRq/SUp64vDhJ5KkLdoJrqeTjud6l9C/3B39Vdvu1bZHfx1/7RiuM17brXWivza/Nl+n2puu3cUtF7q4nKJwPIHLE1PQ/fiRow8nSS/TeO3EZkmrKOPc9EYv/QvnK7u2JLpXe8qpPRx9bwzbdyo3m78B4oiD3EMgpIKzoQVUcbL9cyB7EczExZy5kp1EIQjnv0NUQvPfQfd+ovP+TPTqDoW4FMdeQaEuhdvLqZwjP58qDnSmVBU58Dc20BQeY6jE/IrIh/ksv+gx2WiOJzWD3iiMNdO+Aa3mm9vq3rvtiHBr6Uw6VVs2t/Re7YuraCft4560PWH77U+WC52EHRBlbyEKKVBMYZXa6hUxBMJD70is4DQpwUPKo6OEsGutY3EcdFwIRSxWfM9igo9ZLXhoJZZY5AW3D6EdXL0clPvTyHT6utZvOjetnH6i5ZdrafSYvofBmkadZBfoTBbuATXG2kxjQDJoUwKSKxY3qszgfhXj4Iv+6pe1E/p1OnHdOBe3Biy3DV5HpVI9/lBFKAAW59XyXtREwB7G3nyd6Ddct9JS/G41vHQk6+G77WIIxl7feICXQAny3nr2o18CsUv10vXr8ftp5x/g/s0wkEwAMiHwgVX1z/lpmKZxoyZEX5gtdTjzKcNMi8G3BA2f3I1EbLiQLMW8MTqVFN3vOpv8LjAi1fCwqk0oRlZ4ZJc7HHInUhcXbMN59PAi695x8ekjR/44feTw/1SqGzZsU6qrt3KFtB9NpCHtA+0H7XXte+0j2omavv799Dd0/Lf/+c+3QMeu82e4DWItyKI7iQjo7zjcEeVcGXsLEO8wsQjACidslkeBC9SiGzNoMxMRMjcLRL6L/rtSNN865Gw/sRvyaDJgLBloToKjiAMptgHFaCRqPF8fiWdXi09CLUvWAZPMABPYpSrBcpIHPyDZQdU8Eh56HLByCrzrSZTdEd5mLQamqDbgj+IsVuLliEQ8xSzIZBvO00T9oI6FNOYefcHJ4h+f7Dr2zGJtMsf93FBJjy6c+OzDGzZPFjw7Gg7vqPyfFVo3sXQEl/rUOyOWrH91JdIx9vxP/GmgIxe0JtIW6RCBDrEtbkkEZkRSkCQvkORlCMObYMmrtce1TYGQakfR5unuACID51L8iDcS4DihADEFnEKUgRBDyXIp6fiuDMdyAaKTiJzOMEscEN4ewYcfYgegjrYsdsQB4FBJVnGxYpeVNgBJ3GpienFL5JEHxsMOGPU5jYxhyCPYJnMsV/7Gs6u27nhp2bI161eueLimnBP/3L3/h3nTliw+d3CP9jNdJC1TXnj62SfL1sxesvbFxdLLx+p23729fc5rc/Z9fQR1ux/IuT/YgpU4yRASscS0qJbYLJwdgDoAZ6lekQAYuwoUS50SF0LlVvhQxMxciFkCJloYPLagN5FRuWyoXLRY4WTFwVSMhmVAkqBnkJjkmPpxax44frwi+h2XKoVpeV++oSGrVHuclpfyvbiJzD9sBZszw77SyX4SSW2UW2qj3FwoN4+tvsaR6jLn1fptqS4Qmd9WzxC8s64myUkceSoHcRxFlOSMAXPmyx1O9OVOh+7Lr9p8ZjH6clFxuhTXXjBixbN351UP/tkVztpqvA6PJy8CrxkPZTwUlEBli4nizacRl8erw2aqmtHTpxYrSaABbtRsB8g3QsxJxRfIFERpyvEgpO5Fi7q4fV5wBtlbufHVy9a+8MITDz8ZGH0ztz+6rkvRwik7jx/9uvYXOl168rkDO9cdHDrMxadOjp4JdeH58+TwUe3PdwjzTyuAV+nMVnPIXSSSgNxKi/knG19f685MQIjoFoE5bZk+J6OrCinJLmSK6gPmtIPfgWTQUMHkTmAampkGGupzAgS0uYE4c7EiyIoJqZE7E9BEvykfAI2UCgYKbo0RQoqak7mCpn3cf3lxenH5wLWf9dg55cDx3w+8o52r3Pv08m0vV03fHuBS6OQG2qtNRklGWsP78weO1H498rn2I23f8PGv/3pxW92cu5guDAAdRV2II51JxIwaik5bJWie9gLFXIfpaixFg8CnOlAHiRk2zRfr0cNKeVOwyE08A/jXT5zNtVXacqn5C/GGsjLtx+gebemMGXQq91dqIoglxwA/7cBPPwlCjnw/ifiQo8nAUQuu2wE4mhPwWYCjObiFjoyjCcBRCR1AJhwkuNQ04KcbDnPxXBwwuBOcyM0ENGnhfckBJ2MxMlx1E3ACObLq5OF3B7caJxXrULKoGZJkNi+AzTfnsKfZ8ZiqRfcuPvn3Xf956N5FL2hnP/hEi1bse27FgbefXnGg3ZYli7aqCxdvpgvm72nXVrl/10cfv36/2rbdnnkHPv3kwGNr1z360JYtXMH8Vavmz6l+HnVqKPjNfxk6BejIGot5LAJkAQcS0qw8cCBBatIpbz0qFIQ/JRBSTV5dp5LRFdhZymV18LpmyVb9XAK6BzUL9Yz4dKIJi5BeAkaRU5RGWQKBuJkzcLNO7FByftenmnb6i4Grr4vvu2jwhgOFNZPe+m3W5uULtmVtX/XIK/zuozRXO6md1QZHtfq09DEZKV9/uHzEGOr9cuOxRSUrP/zytG47GCSCQldWD+nQhCYYIEAsYUbSADshlAAvyBCFpRFR8PCzculSwBX83xBbcARhTo7QDWKyhXQiEROgalXCC1ljAEkxh7D8IeH1CljR4AK0ZMOXcYCY0pbGMJOwAq+u28IMfgn/EVydgFf1UZPPT30D+O7RlRMmcGX099F0xhztlxQpRTs9B/fzFN3Af85vYvQl6UjLqlNnZdQZxKCNUPh5iu/TsJvvQzeMG0dXjRunrzkL1nxHX7OokBYV5lBYeRZXOWFCdAk/YMYs6k4GL+CcqT04mvH0ZjCi65nupJFJJJKMPE2xx9CDrSV6SNfRg5uhB4CiSnIIzaU2zUu6C3lKXCOkYElsXBLoCh8PhuKRVYsLHW18CjpaKe4C8OCgviB42Bh4MAWRqzfzdRtq3l00o1dyBc29Y8JdS+bcD1GHtlkmlLy4+9DmxR9PLRwx6oG7byt/Ztq8h5fed279ypVAzwytu/S5+DAJk2vIFhJxYrXCElaLxHolLaR0KlBzHfXK1QWqD35lFqg8Aq++zCRyIOfO0X2sBMlEP70ydNW+s1P11KGnS+m1FzzLGSVpL6lJSu7ZC+swtPGIhZYcsCCVtgWaA3Jvi4WXM3PzOxV2w+KF5FZNbZAJzlz4TId88NVXFwE7EhINdrhJIIPwEsYYI/3s4mauO8xLzJ70D3AkAMd++EQGofobPWiRh/n3GW76Ga2gi+lS2Vr3wcB75MLnyh5Y4vGf2Dhyaj+OD1lvKnr0RZtbU7Sntb9rI2QPnUhvHlLbK733B3dqC7VRXLHr1lG3P9KZFmQM7PigQr+mGzlJS9WGHNb2lQ0fNfqXgxoNFxZx0X0LR515iy6i27R22jxtkdahfbB/u470Nzp11au3T4UMlsvwJ/0M8oCsXvgG4oEJMqH2us0qfJgFhVrJTCi4JQlxQFwBy21UipHAigVMAPdBPsB7AkAo124KlzXr6Wjp07u5G7WvJVE5exN9WhvHUcg9WBzYA+ssZvmhH9Ycb3gHJ3hBFn8y0Av62XLMCwaYyJ3o/kMAJJje2pz1NaLNYwYDgPMpYHagyG0o/slCKlH9TpYioi+ECJuhY3JIxJojvayA7uUDhbGDPfSl76JzJy7aEP2HNo/Oe+HV6jXaRDqoasurivaBqOzZW74hI+HQwv2flK557IGNpcsWP7RMt+WFENs2g22mkrGGZXqAHk8yg+jxgKsYaIgDPBwn4Lk4CxppGiPNBSS4WPVTsYQYDDaF1HQslrhA+4TkYqRClRJRIeM8cMqUoFeNXODVBUj9UZ+4VOp1o4KF/RLEM7KQ5v72I3V5uPKEd17d88MPe1495C/nPNrP3/+m1XGjT9J4OvqPb6Tte7XDP5z6t3Zk1+vSl+fonehnUD7vg3wsxEM6GtKxxqTjwdDsjdUiFKsLUQHzIz7dfcug+FgzCAB3SU/amSBXq6mNjtDWa79DutXxMPVrP36ufSQq2nNa/evaj1pVKc3/Yfdxms94iesPhfVt5DpjdUtsdQF0Q9RVUeSZKuJGYmk4S9EtgFQUa0jPx40kXE/A9Z89/FMNx7i/R6/hg6JSFj1aFl1fShrXHcXo7q2ve/GaJj3itLamsaDtggX38C801HEHoj1wsbfujt6ur7Uc9OUD0JcMrKmlxfSlFSWpTUhMQ5DJ8uFAK/qCkNMUisQzVYuHNIvZga46aaA6yTKzhwRQHCW5WI2DNNFAmy3Uxyfr6iODMchMg5bTwj9+ohYfNzlp364Dp7T3n3g3S5tNz3XSogc17XVuCMjUQW/9aZe0fLt2/Gvtt+PaVzd3pLPKomevm0mHNfG0nsnyKsOjmHSPoojhWivPuGptkqSN9UcUm15lFljDpFGG2IAJQ64DTK3ge1RUNBwQleit3OazN3FV0RJ9PUi+6M2sBhFoJsPG2gVcDX/ExiseqUT/pH/3FsBmKnzXg3rnaMyNHI25kYVdCpTfHctcWQ5k05Vfz1UcwGsL5CiKu3l+AithZpmTXdj5Fq5843OLNlee3PV+xVS6TKpat32F4Dl38q2fxpXtNcd49jPzjzGeWZp4xtsZz3j0jM7G8ggXwooaUXm7nlFQPaNACsE5+y0U4nQQ2PYW13MxF93ALeIejT7/NrCvhKsSo8XRgMhtiQ421jbB2mIsAuBKBg+lGA8jPNN6XrTEKphMOL49lRwY9dntTfYkdYRryeQ241qmuHAjJbGKJkvsdUaa9AKkKhPGSMUs13BinB0jskmv92F1JcLbHCwKM9ooaoQnhwapySPvWc35JS6xqsIqRb8bHD0u2WA7msiBhjzAzebOakIDjS6Jzm7SzVNMN6+9SDebKyRoo2Dszo7ixt1xLGszG1tSeUtsQ0WootQk76nku0ugowchAJ5Lo8I/z94kHKfnUsG/zgLb//7Cupc5VveyXLHuJdj0uhf4/5ivzSAeNF83+Fssgvlm0Y6UUIF20d7VGs4T7cPK+o8+O3nqHx/9iK4/kY7U1mo/nNS+19bTETTpZ+1bmn7q1AmaoX17QsfvyJu/sfqFh/Rp7g3B/9dabEwHLS1DgS2E0cCJBV4jGqgem9wy8AYDibQp1v7+r3Pn/qUtoHNqt9du1xaISv3efT9G13H7X1n28Gv6Pmadby86gFcesOebSURGXvljvEpDXrVhG/DCBrwuNcngVRBLE17Muh2yjbWjZEiMABXIumalyaBOzVjo5Ux+UxbDaZdg5MTSs4O1P7s/cP0lubleOzP4RP8zqakXs5Qju4CfH4nbALsHSamhbS5d29QgsDQxmbE0EVmayShKAoqSQ0qSnvmlM/SuiCE1C9UgSTfzOFmRgapEomMd5uqV4EVYB6BBvN8Hfp41jZqJYBc9+e+zD85YXJGRNSMrbcsqbSy9++CO7a9oD4nb3j847ZXcNtsWLu07oU1C5oJrFz24KjqJ+3PN4sdXge1gLl8JculAyluv/2GTUU2BUJYi47mUhJYdxvbNOoytNBTN7bGmZ5ODLK/FJmKNw5fVvtUWYmY45AdCfaaWLUQhKKG7HcNN0jZv+Sxy9NQf1HP4nw89yE/6UN12cMc3P/2ufXf0i7VVdIX08voVsyue6dZj77rqT2ZP3yqK0vJdz02b9GTXHu9Vb/2AThp3SEJ/0QFk+BjDx2C1UvN6icKHWEor1aHuR0RWmRUBFEQk1naVsILXlBFiL6CDUKLZKrFScnaHeAPzR9Ws14b+skjPhlTJ8L2KtdFd8lgkdOHFWPUD3SWkLljsZaVwiDONAQfLGtWVX6m1xyq0o//+QTtGP+O/bMja+e6h1/H3zw1R3Q8i7v+Q4Z6AUakkHBs1QKzDAI1KLLGiT5j6w0WI9zMW0B2pkJ9uXxD95xTwcdeOHi3shFBKSTH4fewD+EitXuNRnGF2yQjFAACXjWekUEjVqUuNww4hyl7P4t7485erWVufuBTfXofe/9m5r+rkcaOUmO9Q5L2q2XdGVEzwxuyfb8FqIsSQGpfs9ORF4LVZQbGGM7tklv3t4Exmp0v2NXXlKaxthGziQ8fKvDiQmE6RRP9VFAmlOUETDRbPpJb2UhHtPIV2LpQKqGmG9tAU7bVsKUvbMRXIP/EN/VbwnjvxT/wFvv6OZ589t07nb3fgr8LiTLZh+eYwKwYbcUbPpjiMI4KVxREL1f8PWmh3elpLfoI+S1c9oaXQ049pt2m3c8e4D6LLuUnRUDSNWxCdA2sEYI2dsIYZEbupUYY8LGApUEx1DKFbEambWPQCivUDpBfWooirltG9dP+y6MkKUWn4nG/XMCZ6gkvWaYDEQBjPdCQ/FstjeJXn65sUxaRXqAE0G425cCENYBEk4LuTH9bwBv9xwzp+9gjh57K/noszcMI67W16UpoHdlXIKimA7LGSQvlYnajW5CV2IQ9RDphX7C8+FDMpgB5BOexbR2/45BPtbdOrZWe8ZXDdjucf4MVYP4q07EeBkIMd7+NG3ScqZz6FzxLYQ3+2h15EMRXoRl2A2J/twVQHy9VK+sKSS6VghRTs3RXbjClW8fFB+AcEHfj0U9pf2/6JdKLsz+uxvsQd4RoY/xp7YwbLYC8sfQYt4wfQvGE0d9qBNCntDfjC59F29Pi4cVqKzid6fhU/lWXQSc2wGR40IywM7oXyUxoeK2XfuUPYSfeLB4hA2hC9AcELxIWdRZFxFnLyOAG0Qt9IUdgTvINbeeg+cY+o/YHx927AxG8LAyFq5ZMTemarJIUjAVw9xwoZLhbizBDA+PYBD+JSLNIUMPPGgm2mS7Ghp2cTAECvG09hDTcipOaGQiFI0zGtVzsatn/tb/2Z7SfnC0rqXlFNij8jKAl7d+799XcLs/IEV01iQpInT0l11aSkJoO5w59N5h6Bc8zqExJTUmM1n8SURnvPtLNBFTUNgEnEE8hhzTI+AJbnx1zJLEdszni9xNM5s3usQVYAJt+5iFXAwL36IZAWNp85KITP3E35r0499eDsFydxk6Ztr/nC7pwdZ+3x9uyqbRXTx89/s/1/1u2nGU/XPjht4ZzhVJKkqcNG7Xg5eqJ4QmHRTe1uK9+4dMjk6SOPLWOYZzXEAUlKAE1JJ6MN7GVHhvsA+EjI8BQ8YH01iWJczWAMd+uJgOyqV9wuNQHnwPTujOpG2OPSywh2JDkF3Z2LN0CrzDoNst4zyTF5jPowIiDJtLqyy8Zp+7/66o2KzYV2ue2a+1dXPb969rNZUkK0cvhd2jta1Peb9s2dQ9fRjJGTfzzg+5Dys0Yz3RsNuvMO051RRNeYeNDX+ECsSBkRkBYnYAQnS3edNqRFRz8eoMXjUhNBL+JCaqqM5V0GfRKxACIEWHEuHg7NqcYEjbslDEDMg4Ew7Pf6vCbIvbjRv34Zuf9ebvy2uVurNygVO8ZxlbPXH/0PZ849QTveU7ZOEqUFq878PXfvn0umS5L4aEkpLWDymAx0fGrI404dr+vhGeUhxOQhMHkI5pbyMARhsoGux6SR4EYSnKBvVhmU0ZBGnMko6rBCImYROc0L9LKepU/+8sCUDUUV46xdXr5335eVq6umrcpr9/T0qjX0vI/ytGjUEG7BmR9X3z6CBn478OPYEbRh5H1a9ENGxwig4yOQRzzQMYxEvEiCXTJISMWqm8UrxKpuGc1LPIlG+oO7T7QirLZ7/Swtk1WXjLKw2FGhZEMWhE0rBXz61rH+2YZ4/AHdnEZQ2+63jkeFfVXlVV3DPV+f/67223yOm7Hh0UW1NFr0Iw01fFKW+sofvbrd0rs/bU8nimmP7H4X9KkPEFEjdSB+ciuJxDOrwPgjWQAk4WykHFaJCGoDWCyhQIlnExo+rJWEmk0URuJ9TP8QkSVixJLQJVjYvsN6W6ixAacjtT41654M9A06E8JtSsZSTtMq+cMlVesiVstdkmlWeVVJQ1v+MNMTrT9fB/xNJXlkmlEFDIBmmGFzOpPbmpkb9GIVtT1jcBrsL83FsE9mKMZuNl1WoHYAbqcR3XL9co0g25ONyToTcDwZ0htA/2pbe/OKIFOeIr3a0HqnJ6ZIRw/eu7HIUfrDBwOVPum9H7256oWijeX7j1Y+DyqVm/PM9Kq1hkqVjthy7h8f/5odKM0I7Fi75JahtM2v++vH3UH/GFmpNXygx6YqCEtfgI14yAAD41jDuq9yoq9yNvkqb6N9cyE0cZvhp7CCYvMw1ACmTQy8GfNO4HmD+kyHSa6q7FJbuemVymUzZr6YA27ontET/vFNtJRbrTw7f3xUYrq+BTaVCfthc76x/BWVBAOl0KIB5dQbUM7GBhQsiQ2oLRUVFUK3c2+K5Rs34jXPP6L1p3lwTSdQ2ZUwsaI0BQvAFZdCMc5hT99VoMp2PTMG2ODSpeoOGfVRXpdJrCKUje2Te+2urr6hYyqefzStkAoV2shS0TqzUnjy3MTq7VZTeqxHtQZ4jHNljlhdFOtCIs6X8XYiYvA11Ud4OyvNMFZfuj4ktlofWlM5hy5/mNMG0a/5pVr/h6SEhpH0gKglRF8VOWf0P7CHJr6mkEbo0XppbUuFlHDmR/jOCsgH5oJdZGGuyHCLKwXrQGgWqCJKXBjtRPGB4Wazi2Xp2pHlYkUPVuJng6hY+lRzcDJE1w8lVQZ1UVLQgBVZVuN86IsCLSoyfqY+/guUyNtcoVaMt3XeUjmrOrPT9gVbdlU+MmfZCjed/tjsuU+lCd1q7hxbOXPq/O//E13KTX/7xa1LTElStIKbfuCl+ROj5pjuHwH6Wuh+I3VoAJfXeo9BjE2+SPf9F+n+OFtndbryauWyeXPWBIVufx8z8fPj0Ync8p0rF02K2pnu48xmAuznorkq+v83V8X8OEllXWNS1KIsAhjm8BEqaecOf6Gdrdz9cvWevRs37ubiAqdwsupU4BftQ9rpl13ncZoq8Bo6TaOes1obJYiwN4ylQ4kBa6T6ZuyCWApJQCwAybrtcC5WJGyOaWRO5xpgGrt0AabxGJxrxDSJtCWmKXV22cRAzdRNXdqtmrZ63fqq6c9ka6PELzYOK4lhmttvin7IbRtadmK/7wMq3DtC9/Gj+A+M/d9pZOm4/yYfnwKZg63gAgwA4kaY29K/IxW2RixglplbbwULFGGJs3UsMLm6S9zYiqINkxgWKH+2fbtn7m3EAnfcvuZsNpc/6FbEAj+V/pVzD52infsw5q+554EOF+RcTd5R76vHxYGKyI2tBsizcNrHjf4jjsTuWQAO+3TLMuUwxbzHWVA10Z/ncA2d8kS60K02bky5SSiX5k6O+mC9SYA9VsN6Hci8S9SL6GXrRaT1epHPD7gKC0YOI+80p8vuWjFODuI0mJIlKwmx+hFx+BpH0HUXHBtBb71+xMr1RZ0Bz5vUygVPz16377WPN78yvoyb/My8Bx6Y8tIbe7+sfbN8PKXtpPvGTb35xqmZuQ/NmbVp2O3zAd4PXTjlxv4lWXlPzVtcPXLoDInxPPv8T9wUcRDgl9tIxIM8iItBF1GHLqbm0CXWYYpvHC6Nt7SELtgMRHBAZMWpAxhZnwdrhruyC+Xs16f//POA3qlFme602/OmzgX4Qn3aTyXRq8YNFaWhdsfjz3FvwP5Wgow+F7rpfgwtUy+3SmZjk1iE8l5QhFLsrDDJ/BirQ8msKoklFSqx2kqzqlRRI6rNXlm5eNaStRmV46ydlcpN++hb3L3RZW9unjGe5869qd55N8aN9uBX98N+mtWl6JXrUu1n0dyglE2zZ2mlo4RuDZ/NncvnnXsTvno1IeIBuJ6PfGPMHjmcEIfwojXUhH2GVktT3sbS1L6bfj7dSmnqtxPvtihNWUS9NNXzvVND9XmEOEiD94qKHSead+7bd/IelsuaXDVmkwVy2cbSFfzZLJeFc5jLbufMFptew4J8treVM8HfjmaVLCO51YtYBjc8wI3Yq1FcCF4961A7Kfz93d93ljocnKUdLPulQOp44m6hWzTrjTe4L6NZb77JfXnuTe74669HU4ArIeB/LfCrZd2K/nd1qxCdqz3xCA3SrEe1J+ich7X3tPe4HM6jXUt3Rk9Gj9D3tTCsEQTMfIjJxJiVh2tjh9UeVmVEyfEFyHwgTW4uaJAz0yID4F5Fg4tou2yJXveglpv74HxfD4cjrjBu4MhAMSjAT/P5p88lTlppEcdw4uS/Lme2iDc3bGG61aKehU6IN/139axh3MPRJbwzOoXbM4SfeffQhoVGPauvNoFbKfUkaeRGAuZc63eQRCGPzQhBbLMU1JrZCTajk8wwKHYvIM3NYJT6gZ8ebPpTGY3b4lZFux4OWABjdo23gsQK+ya9rt/3/imrXkmae9/wO+4YXjEv9ZVVU7j0sQ/OPL7pVNGgdoceOz5pbVbOuonHHjuYe1PRyZePzVjK9hrRfqV+ViNLIS1bpa569mOUy8ByI6Xar9LuM33Y9yxA450xGtMKaolOo79AjQcaHQW1ziYa+TrFqvep3QaNfhIbbIjHqKc43KrVzWjsRRmJOkkoXpbH+1g+L5kscytH3nXXyPvmJu14rryionzVK9qu3IOPHStfmxlcO+X44++0G1R0atPxGYvHLp1x7OWTRbo8HqPVQj3vIYnkJoLo3GKtR73iUb+SGLHGXWnM3IHmZCyuJyKIZJNQFuylk0S2W1XywG8eQrTdmCbEEKjHE7+edLHk0fdY1cy/Pjn0qvHFAyaUrJ0+5IkhvSd2HXQP/eKBHTfcWByeV+Kcv+u6QV0Kp4/R9zjjvI3/TswmQTJDr5UoaWE1XqyPBJj7D2QY5RK8OcEJpwWWUQniRRWTDL1vns6yGoyWRgklSa5HKWAJJT0D6MEyl15CqbHaEpP1yFjY2d3yfqymKko8uyUrm5vxwd8rq97l+cYyynhO+MdTlbvf58y5R2hOwldfyu+tblZIWbrP/d1xP80BGvH+wo7sXqJn9fuI1FRIlxJDEQnTeAdfX0toimTPU9xhVn/1hmpsKZIZKAyy+1Nk7DwzdMATnLfgUyzoOxUfYoM2QHCbAoULs5QfFC0ePh3fhgVML346Ppl9Wkfe7no1E6ck0KoTEXmrksMAvWGeybTxjjScKQbJmnBmPtyLFuZc867tH5HXd/F8+dLK2U/Y6D7talM4n6cNg63XXmviFpTRtu/Vf7hV+ttSZY12uEwZv693aanz+0ol1kNaDvYWjxUCR7M6fa1LdhA7G4BzIYIM1Xp97ARAAy+vQwM/wiGkzc7GHSN2NppgtwFhUijiYJmfwwV/eUMMKtsdsVq/r0WtH0jx6bUNcGX4r8MyWk03LtOK6b3acPqiNrxCv8GQThWVaAfu06hctq1M20mvhV86jl8revgs437XHiTWNVeJnWEWvS/WOOeJVeYErNizRjqWzOGvxn5YGBnrW7uVtt0ielbDf1jhHn/+J/EP8QDEHj8g1FV6/FedDmPa0QcHmQwx4gGrvGWCidSG8yyZkAiH4WxemN3wWIAW0oXtIs5F8vTRxwT9Zj2lrUvN18dqO8Jf6SGlowtxbq3EPqkW4e19bWX3DovTx2emhPXx7TzZvV2Kc6eTjrrR6C1kvQnf7NiYMW7NksBLjKdVtC3NoVXaaO0L7bBWchudSAVK6WRtuaZpDdqTNGnHM09uELjhk8ZNmjVz8vgJwznhxSef2cEdod2pot2kHdQOaANphPbQ6rW5dD71Ux/E3PnatorNn1c9JU2ZVD2/cuGLE6ZJT1d9xmQ2k6zle/ObiASZIU65YqA2fs2kOfdoJ6j3HkfsgEv10JnaTG0WnWkcXHB/EWlx9xCoNSkDmf1qyCxEuuNM50VSqwWQgPPNeNdlJyahToD0lbah2sTu7I3ExvstL5BXCCQUDikhFxNLu/YA/FPBVwfbhkJKagux4S2YRSHIA1BsGXh7oTsV9D8HhNcJpwKDxUpYrgUREnxT6Y43GFxGjpfoo+fRRBq7naTMkOYakOYRXZqTIAPj6CQmzai2HKTLPVn1l759e5gtZVbhxqG7tg8aP+Le568kzehA/pY5M/relZY4rn/Xtn18Lt/NuV1uvUF7ju65+frb9L7xNGEXPSK+CRJor1tiLblEj0flMfByen6fTMN+ftqHT/Jn4PtWSWvAa5VoA+hKuKoTpz5MDP7H1SvOWIBnd6uY6motumgsLpU37s5m96dIRL8P2CTrFVU9ySoKG/OWJcNmDh6bekfcoNFVT2qrenYv7mCe29syaPDwiUw/F4B+DojpZxE6Kh/Dk/BrAfVqJ+6hOdqRTxqP1tKFdJG2yKMtajzQ50vZHKspnc2xui47ySoX6Gltq5OsvAf4c9E4axEyrPlMKyU68/SZmaGwLq56xclF+UqTi+6LJhcpbqjZ+GL0XX0vxhCj5DOkiLw8BC8FsBeBmEkWiYgYaSQG7ywFiljHCj7YDjaLLKE31MFGAecdwqveUWlc7sxPxoAcr88tmTqzulIG6dnq5FKgtcpSm9g90YKN3RN9heElRuelJ5joZNzgFeeYuC90dgjGvpONe7+DpKyVnWNJLCOspkL8CoRikMogIwVcS7oewdIZwKoN6n8Fm0hEXJWRjiTKCbYrkxiLepemcjbGwysSyeezgMnpsyMgbxmQRffWpkf8rU2PJBhZe8Tp9hUXtz5BwqTRcozkLRTARcMkYodG/eON/YA/gMwukZRcvCMcZ4kPqx5gOD4dIqn59tCX+3QW+9ica22i/ldi09YRo8djrcwpXWLjMR632PtnyNaLtz4/hjtYv1v8GvQbrI/8j37Xl+IP6zO6mdb6iKux490uzRXreHdi2w/A9gMXd7wDLtxtREjKwY435nq+kBq6oOOdkC8oSXtF1Y8db1+zjrfPVRPv8+uPpEhMSvBgB8vfrEoA51jH2xefmKR3vP0J8YmNHe+A0fFOtgFscaVltu+AsEXxymp+AWt+411C3mSj+W33tNL8zr5s55uFkWbtb6m+ttX29x9MaZp64NP3tNYA52+OKRGv9ytBFtivzCQjrtSxzGqtY5ltdCy3Y8cyI/i/7VkyIi/XuDzHqLtk95K+0sw3PwuBVhPfbumb6X/lm5/VfbOwm13uXB/sT5HYcxoSxKMX+uYWVf/L+2bjeRVXKPwzb9B69Z+2ZX75cj0AbkPMJ+v7PdDok8c223EqeohAGO9tUjJCzQj4v/HKlyYu5jFap68L88iXJe+s7kbw/jespYKMPSQB51YvUU1NvEQ1NSnml2WvHwzyv6qoMslcWFa9k6nlRcVV/iddDryxT5x594MkFly4Ux+KIhEyUDuO6TRtPCW28RovT/A24cYEr4mKmuQ4C7yVoL+VUFCbrOd92GdKwCKXLOm3J1yRtJhcLqBuIvPlFxEn9GZSiMX9UUzHAiSHXN8qYmnbmlW0M6xiByKWNsFsfYRYzcy64uQ18xTBInilwUtH91/qFvG/l/1KzU9w2uEpVw7zNiqCvCQq6E7EsB/JcjFtLSz+8rShxbdC26XtozltrdvISy3puqyxfN6Sphhm6A+YwU9ScSb/YhST1hqKSTesZTugmITEFKQnTlaTki8HaAwqWuKa61vs/mKUMLL5jpntCFbxNMHKYjr2dC5h5RmXsPKAse9asPKkNGPbDtz25c2huRguMIlvW1JwsW2ktGA6Jc8Lx7l3xTqIRHns2Scie76YLOjBCJJH0UvMYLTWWKlfv3eosCgMiXCO6fnvSr4vr94gHPcd/dbNxiTA920SltKz4iesDnAjwYK3XgxWfAW1vJFGJsQy/CQ9wzfSd3wmDoZudxz4BwuPrPBByg6JZVO11dfsKUh6dN5017V9S0b3u65kYGF2VjiclV0otu83Gk6MGHFdTudw27aFXZDWMuEUdx5ipAd3BdhMEtmwBi/G+vO1Hj2t9TAx1Vr1cgJrbeHUGc9G59i8EClWeZeRM+q7aioAI2gqmzD46vWF+X1umnTLDSu7FPQW6e33Tbq+yDtk2qRru1y+jvK/f+9FbqvwHST7PPCddRv4en2ItmnqFb7yotCL21qG87FLuK3i3it+fonY1fj8cCFEZfZco8Zn1MSeakTY4Dt7Ro2o3x7Dvu0J877hk6+7SghtpV21t7fq+7zMdS7zrJvhV1VMhi923FGjvW9c53wHKlH+v76Onz3+bnjnijGfUut7+zS8LwP2wpmNZ+z1YRZw0RP2dNoU0cUqKDbjLiCDTEWS2egGu+k0RnK4kfB5zYg3WKCvab/8msYt7bHH+RlrGqRgeUUqVqzslqiWz/ZDJm1vxiiDXTgT0oX+Qd3/V2vqrDTWDFeO2di5cswhmrN9m/YpfAde0Z/jPS93s+cJYSWmn1EREczhMD4KQBUtoVCzpwvFxZ4uZJSJ8UkHism4w87beBegAQXwZ9dSKi8l55euZ//pOjGBrKUNrIYUIFQxxVyYTZ8XN8cEJ+jCYrXPCReVPOE6pXCd31teR+FCxqWarkPxOkapqrSVyhTb002Asd4TD4KHhXwyBwnOMB6dptjCqszjhGItoTlWO8Na2PpIxmcpshP4GEUeM8YaR44VeyHtC5TcOpWTsP4JMvImABdTc7F+lIodjvhQJJc9zSWXWLAThLVRlGOHZg9pseNDWuzGQ1p+nfzGNL197WAPabFjr3rn6bq951j6aXPVxEFamKe4XDVOlwPST/izWfoJ5zD9hICGqactzulq1o/OYNVWfbQyiOOV5ILxSvavecbVk9700ksvUedXxZN7W7pM6br5bS4YPYo/724qLu9s6XJf96+0U5yvbGNZ1mkadDnHuTw/vpUDf3rePCHLY50u2uZ3jx6HRvHPCNew+3X8pFKvjELOh0+w1MMR3/iAL3zWjtnpgfScRSapzng+W+t38qArAA2o9evRy+/C2bpaZ1P0ciG6tdoNPBVgD+iB7M0D/+Aohw/yJnkUnbfiBtpx5CZp65C/SM+HX5TE8f36ae3pP7T2XKI2lFZHf6BzqTaPPka1qUyPEPh1Zc/UIJ3kgIzH597+f+LPPhMAAHjaY2BkYGAAYqY1CuLx/DZfGeQ5GEDgHDPraRj9v/efIdsr9gQgl4OBCSQKAP2qCgwAAAB42mNgZGDgSPq7Fkgy/O/9f4rtFQNQBAUsBACcywcFAHjaNZJNSFRRGIafc853Z2rTohZu+lGiAknINv1trKZFP0ZWmxorNf8ycVqMkDpQlJQLIxCCEjWzRCmScBEExmyCpEXRrqBlizLJKGpr771Ni4f3fOec7573e7l+kcwKwP0s8ZYxf4Qr9of9luNytECXLZJ19eT9VQb9IKtDC+usn8NugBP+ENXuK1OhivX2mJvqmRM50S4OiBlxV9SKZnHKzTLsntNhZdrr445tohAmqEsfpdeWKbffFKMK+qMaijYiRlX3MBRNU/SVfLQ2jkdrtb+DYmpJZzOiiYL9kp6nEGXk4Z3eeklVdJYpW6I8Xcku+8Ie+0SFzXPOfeNh2MI2KeEktSGP8wc5Y7W0WZ5ReWqU5mwD9f4B+6xb6zxj7j1P3eflW+E79+N1ukyzaV9kkz71+Beq19Dlp9msejgssDW1ir3S7WKjOO0fkXGvmJWujHq5HWdvWc0/pNxfUxWKTKRauBgm6YszTnXQ6mvI615TGOdaktNIksebePYEzZrMG88g326eeyVfMcMxSU6qk3uxt0uMy8OTUKA1PIN0g/Ioqe/W//BB7P4Hi9IeabvO5Ok/0Q0mU9cZcJ36T2IayfpmcUHU6a0K5uI+30inaIm/adUcsx802E74C0holcIAAAB42mNgYNCBwjCGPsYCxj9MM5iNmMOYW5g3sXCx+LAUsPSxrGM5xirE6sC6hM2ErYFdjL2NfR+HA8cWjjucPJwqnG6ccZzHuPq4DnHrcE/ivsTDx+PCs4PnAy8fbxDvBN5tfGx8TnxT+G7w2/AvEZAT8BPoEtgkaCWYIzhH8JTgNyEeIRuhOKEKoRnCQcLbRKRE6kTuieqJrhH9IiYnFie2QGyXuJZ4kfgBCQWJFok9knaSfZLXJP9JTZM6Ic0ibSTdIb1E+peMDxDuk3WQXSJ7Ra5OboHcOvks+Qny5+Q/KegplCjMU/ilmKO4RUlA6Zqyk3KO8hEVE5UOlW+qKarn1NTUOtQ2qf1Td8EBg9QT1PPU29TnqR9Sf6bBoeGkUaOxTeODxgdNEU0rIPymFaeVBQDd1FqqAAAAAQAAAKEARAAFAAAAAAACAAEAAgAWAAABAAFRAAAAAHjadVLLSsNQED1Jq9IaRYuULoMLV22aVhGJIBVfWIoLLRbETfqyxT4kjYh7P8OvcVV/QvwUT26mNSlKuJMzcydnzswEQAZfSEBLpgAc8YRYg0EvxDrSqApOwEZdcBI5vAleQh7vgpcZnwpeQQXfglMwNFPwKra0vGADO1pF8Bruta7gddS1D8EbMPSs4E2k9W3BGeT0Gc8UWf1U8Cds/Q7nGGMEHybacPl2iVqMPeEVHvp4QE/dXjA2pjdAh16ZPZZorxlr8vg8tXn2LNdhZjTDjOQ4wmLj4N+cW9byMKEfaDRZ0eKxVe092sO5kt0YRyHCEefuk81UPfpkdtlzB0O+PTwyNkZ3oVMr5sVvgikNccIqnuL1aV2lM6wZaPcZD7QHelqMjOh3WNXEM3Fb5QRaemqqx5y6y7zQi3+TZ2RxHmWqsFWXPr90UOTzoh6LPL9cFvM96i5SeZRzwkgNl+zhDFe4oS0I5997/W9PDXI1ObvZn1RSHA3ptMpeBypq0wb7drivfdoy8XyDP0JQfA542m3Ou0+TcRTG8e+hpTcol9JSoCqKIiqI71taCqJCtS3ekIsWARVoUmxrgDaFd2hiTEx0AXVkZ1Q3Edlw0cHEwcEBBv1XlNLfAAnP8slzknNyKGM//56R5Kisg5SJCRNmyrFgxYYdBxVU4qSKamqoxUUdbjzU46WBRprwcYzjnKCZk5yihdOcoZWztHGO81ygnQ4u0sklNHT8dBEgSDcheujlMn1c4SrX6GeAMNe5QYQoMQa5yS1uc4e7DHGPYUYYZYz7PCDOOA+ZYJIpHvGYJ0wzwywJMfOK16zxjlXeSzkrvOUvH/jBHD/5RYrfpMmQY5kCz3nBS7GIVWxiZ4c/7IpDKqRSnFIl1VIjteKSOnGLR+rFyyc2+MIW3/jMJt/5KA1s81UapYk34rOk5gu5tG41FjOapkVKhjVlxDmcNhZTibyxMJ8wlp3ZQy1+qBkHW3Hfv3dQqSv9yi5lQBlUditDyh5lrzJcUld3dd3xNJMy8nPJxFK6NPLHSgZj5qiRzxZLdO+P/+/adfZ42j3OKRLCQBAF0Bkm+0JWE0Ex6LkCksTEUKikiuIGWCwYcHABOEQHReE5BYcJHWjG9fst/n/w/gj8zGpwlk3H+aXtKks1M4jbGvIVHod2ApZaNwyELEGoBRiyvItipL4wEcaUYMnyyUy+ZWQbn9ab4CDsF8FFODeCh3CvBB/hnQgBwq8IISL4V40RofyBQ0TTUkwj7OhEtUMmyHSjGSOTuWY2rI32PdNJPiQZL3TSQq4+STRSagAAAAFR3VVMAAA=) format('woff'); +} \ No newline at end of file diff --git a/plugins/UiPluginManager/media/img/loading.gif b/plugins/UiPluginManager/media/img/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..27d0aa8108b0800f9cddcf613f787347d9981e05 GIT binary patch literal 723 zcmZ?wbhEHb6ky0iXa9k4z>53@HBR_Hzvhc6JPKHPSO+W(0~W{*!Vp zN=+!e)X>LZSp~!n_rkGVK%h z9k_L9<(o^(d!N7A`+9eTzQ!EZMr*-N2_|eB&45;SC+a-zP~lXP;z?eTv`FKm^!Y8l zuZ^S*OlLmOv^VZNyYQx zE1Q1d^QD!~t!MErXFkzlm$bqCmuUZ)iN%&IQkAQ(b??%e8>EQMBqK<8T-y}!%q4L0 z4v$MoL7}cEx5PfOihDclHe=f1_`ny+jJ+qGonTF#=e6?cS1GK1Glv+XQW)E^VpGzx z%$u!=(=#3~+Lk*jmQUf$-=^(}f)AMWru(Y&&oE(%*JUs>JH24vgCGuUPSS^%^#tgi z6`S6zDw0tR+QR$5bp7w`G6mDQzjYm%RoE)?D^8cegv~i}{SvGJM6wyypd + @plugins = plugins + + savePluginStatus: (plugin, is_enabled) => + Page.cmd "pluginConfigSet", [plugin.source, plugin.inner_path, "enabled", is_enabled], (res) => + if res == "ok" + Page.updatePlugins() + else + Page.cmd "wrapperNotification", ["error", res.error] + + Page.projector.scheduleRender() + + handleCheckboxChange: (e) => + node = e.currentTarget + plugin = node["data-plugin"] + node.classList.toggle("checked") + value = node.classList.contains("checked") + + @savePluginStatus(plugin, value) + + handleResetClick: (e) => + node = e.currentTarget + plugin = node["data-plugin"] + + @savePluginStatus(plugin, null) + + handleUpdateClick: (e) => + node = e.currentTarget + plugin = node["data-plugin"] + node.classList.add("loading") + + Page.cmd "pluginUpdate", [plugin.source, plugin.inner_path], (res) => + if res == "ok" + Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} updated to latest version"] + Page.updatePlugins() + else + Page.cmd "wrapperNotification", ["error", res.error] + node.classList.remove("loading") + + return false + + handleDeleteClick: (e) => + node = e.currentTarget + plugin = node["data-plugin"] + if plugin.loaded + Page.cmd "wrapperNotification", ["info", "You can only delete plugin that are not currently active"] + return false + + node.classList.add("loading") + + Page.cmd "wrapperConfirm", ["Delete #{plugin.name} plugin?", "Delete"], (res) => + if not res + node.classList.remove("loading") + return false + + Page.cmd "pluginRemove", [plugin.source, plugin.inner_path], (res) => + if res == "ok" + Page.cmd "wrapperNotification", ["done", "Plugin #{plugin.name} deleted"] + Page.updatePlugins() + else + Page.cmd "wrapperNotification", ["error", res.error] + node.classList.remove("loading") + + return false + + render: -> + h("div.plugins", @plugins.map (plugin) => + if not plugin.info + return + descr = plugin.info.description + plugin.info.default ?= "enabled" + if plugin.info.default + descr += " (default: #{plugin.info.default})" + + tag_version = "" + tag_source = "" + tag_delete = "" + if plugin.source != "builtin" + tag_update = "" + if plugin.site_info?.rev + if plugin.site_info.rev > plugin.info.rev + tag_update = h("a.version-update.button", + {href: "#Update+plugin", onclick: @handleUpdateClick, "data-plugin": plugin}, + "Update to rev#{plugin.site_info.rev}" + ) + + else + tag_update = h("span.version-missing", "(unable to get latest vesion: update site missing)") + + tag_version = h("span.version",[ + "rev#{plugin.info.rev} ", + tag_update, + ]) + + tag_source = h("div.source",[ + "Source: ", + h("a", {"href": "/#{plugin.source}", "target": "_top"}, if plugin.site_title then plugin.site_title else plugin.source), + " /" + plugin.inner_path + ]) + + tag_delete = h("a.delete", {"href": "#Delete+plugin", onclick: @handleDeleteClick, "data-plugin": plugin}, "Delete plugin") + + + enabled_default = plugin.info.default == "enabled" + if plugin.enabled != plugin.loaded or plugin.updated + marker_title = "Change pending" + is_pending = true + else + marker_title = "Changed from default status (click to reset to #{plugin.info.default})" + is_pending = false + + is_changed = plugin.enabled != enabled_default and plugin.owner == "builtin" + + h("div.plugin", {key: plugin.name}, [ + h("div.title", [ + h("h3", [plugin.name, tag_version]), + h("div.description", [descr, tag_source, tag_delete]), + ]) + h("div.value.value-right", + h("div.checkbox", {onclick: @handleCheckboxChange, "data-plugin": plugin, classes: {checked: plugin.enabled}}, h("div.checkbox-skin")) + h("a.marker", { + href: "#Reset", title: marker_title, + onclick: @handleResetClick, "data-plugin": plugin, + classes: {visible: is_pending or is_changed, pending: is_pending} + }, "\u2022") + ) + ]) + ) + + +window.PluginList = PluginList \ No newline at end of file diff --git a/plugins/UiPluginManager/media/js/UiPluginManager.coffee b/plugins/UiPluginManager/media/js/UiPluginManager.coffee new file mode 100644 index 00000000..6a0adee5 --- /dev/null +++ b/plugins/UiPluginManager/media/js/UiPluginManager.coffee @@ -0,0 +1,71 @@ +window.h = maquette.h + +class UiPluginManager extends ZeroFrame + init: -> + @plugin_list_builtin = new PluginList() + @plugin_list_custom = new PluginList() + @plugins_changed = null + @need_restart = null + @ + + onOpenWebsocket: => + @cmd("wrapperSetTitle", "Plugin manager - ZeroNet") + @cmd "serverInfo", {}, (server_info) => + @server_info = server_info + @updatePlugins() + + updatePlugins: (cb) => + @cmd "pluginList", [], (res) => + @plugins_changed = (item for item in res.plugins when item.enabled != item.loaded or item.updated) + + plugins_builtin = (item for item in res.plugins when item.source == "builtin") + @plugin_list_builtin.plugins = plugins_builtin.sort (a, b) -> + return a.name.localeCompare(b.name) + + plugins_custom = (item for item in res.plugins when item.source != "builtin") + @plugin_list_custom.plugins = plugins_custom.sort (a, b) -> + return a.name.localeCompare(b.name) + + @projector.scheduleRender() + cb?() + + createProjector: => + @projector = maquette.createProjector() + @projector.replace($("#content"), @render) + @projector.replace($("#bottom-restart"), @renderBottomRestart) + + render: => + if not @plugin_list_builtin.plugins + return h("div.content") + + h("div.content", [ + h("div.section", [ + if @plugin_list_custom.plugins?.length + [ + h("h2", "Installed third-party plugins"), + @plugin_list_custom.render() + ] + h("h2", "Built-in plugins") + @plugin_list_builtin.render() + ]) + ]) + + handleRestartClick: => + @restart_loading = true + setTimeout ( => + Page.cmd("serverShutdown", {restart: true}) + ), 300 + Page.projector.scheduleRender() + return false + + renderBottomRestart: => + h("div.bottom.bottom-restart", {classes: {visible: @plugins_changed?.length}}, h("div.bottom-content", [ + h("div.title", "Some plugins status has been changed"), + h("a.button.button-submit.button-restart", + {href: "#Restart", classes: {loading: @restart_loading}, onclick: @handleRestartClick}, + "Restart ZeroNet client" + ) + ])) + +window.Page = new UiPluginManager() +window.Page.createProjector() diff --git a/plugins/UiPluginManager/media/js/all.js b/plugins/UiPluginManager/media/js/all.js new file mode 100644 index 00000000..25632862 --- /dev/null +++ b/plugins/UiPluginManager/media/js/all.js @@ -0,0 +1,1606 @@ + +/* ---- lib/Class.coffee ---- */ + + +(function() { + var Class, + slice = [].slice; + + Class = (function() { + function Class() {} + + Class.prototype.trace = true; + + Class.prototype.log = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + if (!this.trace) { + return; + } + if (typeof console === 'undefined') { + return; + } + args.unshift("[" + this.constructor.name + "]"); + console.log.apply(console, args); + return this; + }; + + Class.prototype.logStart = function() { + var args, name; + name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + if (!this.trace) { + return; + } + this.logtimers || (this.logtimers = {}); + this.logtimers[name] = +(new Date); + if (args.length > 0) { + this.log.apply(this, ["" + name].concat(slice.call(args), ["(started)"])); + } + return this; + }; + + Class.prototype.logEnd = function() { + var args, ms, name; + name = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + ms = +(new Date) - this.logtimers[name]; + this.log.apply(this, ["" + name].concat(slice.call(args), ["(Done in " + ms + "ms)"])); + return this; + }; + + return Class; + + })(); + + window.Class = Class; + +}).call(this); + + +/* ---- lib/Promise.coffee ---- */ + + +(function() { + var Promise, + slice = [].slice; + + Promise = (function() { + Promise.when = function() { + var args, fn, i, len, num_uncompleted, promise, task, task_id, tasks; + tasks = 1 <= arguments.length ? slice.call(arguments, 0) : []; + num_uncompleted = tasks.length; + args = new Array(num_uncompleted); + promise = new Promise(); + fn = function(task_id) { + return task.then(function() { + args[task_id] = Array.prototype.slice.call(arguments); + num_uncompleted--; + if (num_uncompleted === 0) { + return promise.complete.apply(promise, args); + } + }); + }; + for (task_id = i = 0, len = tasks.length; i < len; task_id = ++i) { + task = tasks[task_id]; + fn(task_id); + } + return promise; + }; + + function Promise() { + this.resolved = false; + this.end_promise = null; + this.result = null; + this.callbacks = []; + } + + Promise.prototype.resolve = function() { + var back, callback, i, len, ref; + if (this.resolved) { + return false; + } + this.resolved = true; + this.data = arguments; + if (!arguments.length) { + this.data = [true]; + } + this.result = this.data[0]; + ref = this.callbacks; + for (i = 0, len = ref.length; i < len; i++) { + callback = ref[i]; + back = callback.apply(callback, this.data); + } + if (this.end_promise) { + return this.end_promise.resolve(back); + } + }; + + Promise.prototype.fail = function() { + return this.resolve(false); + }; + + Promise.prototype.then = function(callback) { + if (this.resolved === true) { + callback.apply(callback, this.data); + return; + } + this.callbacks.push(callback); + return this.end_promise = new Promise(); + }; + + return Promise; + + })(); + + window.Promise = Promise; + + + /* + s = Date.now() + log = (text) -> + console.log Date.now()-s, Array.prototype.slice.call(arguments).join(", ") + + log "Started" + + cmd = (query) -> + p = new Promise() + setTimeout ( -> + p.resolve query+" Result" + ), 100 + return p + + back = cmd("SELECT * FROM message").then (res) -> + log res + return "Return from query" + .then (res) -> + log "Back then", res + + log "Query started", back + */ + +}).call(this); + + +/* ---- lib/Prototypes.coffee ---- */ + + +(function() { + String.prototype.startsWith = function(s) { + return this.slice(0, s.length) === s; + }; + + String.prototype.endsWith = function(s) { + return s === '' || this.slice(-s.length) === s; + }; + + String.prototype.repeat = function(count) { + return new Array(count + 1).join(this); + }; + + window.isEmpty = function(obj) { + var key; + for (key in obj) { + return false; + } + return true; + }; + +}).call(this); + + +/* ---- lib/maquette.js ---- */ + + +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['exports'], factory); + } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') { + // CommonJS + factory(exports); + } else { + // Browser globals + factory(root.maquette = {}); + } +}(this, function (exports) { + 'use strict'; + ; + ; + ; + ; + var NAMESPACE_W3 = 'http://www.w3.org/'; + var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg'; + var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink'; + // Utilities + var emptyArray = []; + var extend = function (base, overrides) { + var result = {}; + Object.keys(base).forEach(function (key) { + result[key] = base[key]; + }); + if (overrides) { + Object.keys(overrides).forEach(function (key) { + result[key] = overrides[key]; + }); + } + return result; + }; + // Hyperscript helper functions + var same = function (vnode1, vnode2) { + if (vnode1.vnodeSelector !== vnode2.vnodeSelector) { + return false; + } + if (vnode1.properties && vnode2.properties) { + if (vnode1.properties.key !== vnode2.properties.key) { + return false; + } + return vnode1.properties.bind === vnode2.properties.bind; + } + return !vnode1.properties && !vnode2.properties; + }; + var toTextVNode = function (data) { + return { + vnodeSelector: '', + properties: undefined, + children: undefined, + text: data.toString(), + domNode: null + }; + }; + var appendChildren = function (parentSelector, insertions, main) { + for (var i = 0; i < insertions.length; i++) { + var item = insertions[i]; + if (Array.isArray(item)) { + appendChildren(parentSelector, item, main); + } else { + if (item !== null && item !== undefined) { + if (!item.hasOwnProperty('vnodeSelector')) { + item = toTextVNode(item); + } + main.push(item); + } + } + } + }; + // Render helper functions + var missingTransition = function () { + throw new Error('Provide a transitions object to the projectionOptions to do animations'); + }; + var DEFAULT_PROJECTION_OPTIONS = { + namespace: undefined, + eventHandlerInterceptor: undefined, + styleApplyer: function (domNode, styleName, value) { + // Provides a hook to add vendor prefixes for browsers that still need it. + domNode.style[styleName] = value; + }, + transitions: { + enter: missingTransition, + exit: missingTransition + } + }; + var applyDefaultProjectionOptions = function (projectorOptions) { + return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions); + }; + var checkStyleValue = function (styleValue) { + if (typeof styleValue !== 'string') { + throw new Error('Style values must be strings'); + } + }; + var setProperties = function (domNode, properties, projectionOptions) { + if (!properties) { + return; + } + var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor; + var propNames = Object.keys(properties); + var propCount = propNames.length; + for (var i = 0; i < propCount; i++) { + var propName = propNames[i]; + /* tslint:disable:no-var-keyword: edge case */ + var propValue = properties[propName]; + /* tslint:enable:no-var-keyword */ + if (propName === 'className') { + throw new Error('Property "className" is not supported, use "class".'); + } else if (propName === 'class') { + if (domNode.className) { + // May happen if classes is specified before class + domNode.className += ' ' + propValue; + } else { + domNode.className = propValue; + } + } else if (propName === 'classes') { + // object with string keys and boolean values + var classNames = Object.keys(propValue); + var classNameCount = classNames.length; + for (var j = 0; j < classNameCount; j++) { + var className = classNames[j]; + if (propValue[className]) { + domNode.classList.add(className); + } + } + } else if (propName === 'styles') { + // object with string keys and string (!) values + var styleNames = Object.keys(propValue); + var styleCount = styleNames.length; + for (var j = 0; j < styleCount; j++) { + var styleName = styleNames[j]; + var styleValue = propValue[styleName]; + if (styleValue) { + checkStyleValue(styleValue); + projectionOptions.styleApplyer(domNode, styleName, styleValue); + } + } + } else if (propName === 'key') { + continue; + } else if (propValue === null || propValue === undefined) { + continue; + } else { + var type = typeof propValue; + if (type === 'function') { + if (propName.lastIndexOf('on', 0) === 0) { + if (eventHandlerInterceptor) { + propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers + } + if (propName === 'oninput') { + (function () { + // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput + var oldPropValue = propValue; + propValue = function (evt) { + evt.target['oninput-value'] = evt.target.value; + // may be HTMLTextAreaElement as well + oldPropValue.apply(this, [evt]); + }; + }()); + } + domNode[propName] = propValue; + } + } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') { + if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { + domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); + } else { + domNode.setAttribute(propName, propValue); + } + } else { + domNode[propName] = propValue; + } + } + } + }; + var updateProperties = function (domNode, previousProperties, properties, projectionOptions) { + if (!properties) { + return; + } + var propertiesUpdated = false; + var propNames = Object.keys(properties); + var propCount = propNames.length; + for (var i = 0; i < propCount; i++) { + var propName = propNames[i]; + // assuming that properties will be nullified instead of missing is by design + var propValue = properties[propName]; + var previousValue = previousProperties[propName]; + if (propName === 'class') { + if (previousValue !== propValue) { + throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.'); + } + } else if (propName === 'classes') { + var classList = domNode.classList; + var classNames = Object.keys(propValue); + var classNameCount = classNames.length; + for (var j = 0; j < classNameCount; j++) { + var className = classNames[j]; + var on = !!propValue[className]; + var previousOn = !!previousValue[className]; + if (on === previousOn) { + continue; + } + propertiesUpdated = true; + if (on) { + classList.add(className); + } else { + classList.remove(className); + } + } + } else if (propName === 'styles') { + var styleNames = Object.keys(propValue); + var styleCount = styleNames.length; + for (var j = 0; j < styleCount; j++) { + var styleName = styleNames[j]; + var newStyleValue = propValue[styleName]; + var oldStyleValue = previousValue[styleName]; + if (newStyleValue === oldStyleValue) { + continue; + } + propertiesUpdated = true; + if (newStyleValue) { + checkStyleValue(newStyleValue); + projectionOptions.styleApplyer(domNode, styleName, newStyleValue); + } else { + projectionOptions.styleApplyer(domNode, styleName, ''); + } + } + } else { + if (!propValue && typeof previousValue === 'string') { + propValue = ''; + } + if (propName === 'value') { + if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) { + domNode[propName] = propValue; + // Reset the value, even if the virtual DOM did not change + domNode['oninput-value'] = undefined; + } + // else do not update the domNode, otherwise the cursor position would be changed + if (propValue !== previousValue) { + propertiesUpdated = true; + } + } else if (propValue !== previousValue) { + var type = typeof propValue; + if (type === 'function') { + throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.'); + } + if (type === 'string' && propName !== 'innerHTML') { + if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') { + domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue); + } else { + domNode.setAttribute(propName, propValue); + } + } else { + if (domNode[propName] !== propValue) { + domNode[propName] = propValue; + } + } + propertiesUpdated = true; + } + } + } + return propertiesUpdated; + }; + var findIndexOfChild = function (children, sameAs, start) { + if (sameAs.vnodeSelector !== '') { + // Never scan for text-nodes + for (var i = start; i < children.length; i++) { + if (same(children[i], sameAs)) { + return i; + } + } + } + return -1; + }; + var nodeAdded = function (vNode, transitions) { + if (vNode.properties) { + var enterAnimation = vNode.properties.enterAnimation; + if (enterAnimation) { + if (typeof enterAnimation === 'function') { + enterAnimation(vNode.domNode, vNode.properties); + } else { + transitions.enter(vNode.domNode, vNode.properties, enterAnimation); + } + } + } + }; + var nodeToRemove = function (vNode, transitions) { + var domNode = vNode.domNode; + if (vNode.properties) { + var exitAnimation = vNode.properties.exitAnimation; + if (exitAnimation) { + domNode.style.pointerEvents = 'none'; + var removeDomNode = function () { + if (domNode.parentNode) { + domNode.parentNode.removeChild(domNode); + } + }; + if (typeof exitAnimation === 'function') { + exitAnimation(domNode, removeDomNode, vNode.properties); + return; + } else { + transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode); + return; + } + } + } + if (domNode.parentNode) { + domNode.parentNode.removeChild(domNode); + } + }; + var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) { + var childNode = childNodes[indexToCheck]; + if (childNode.vnodeSelector === '') { + return; // Text nodes need not be distinguishable + } + var properties = childNode.properties; + var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined; + if (!key) { + for (var i = 0; i < childNodes.length; i++) { + if (i !== indexToCheck) { + var node = childNodes[i]; + if (same(node, childNode)) { + if (operation === 'added') { + throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.'); + } else { + throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.'); + } + } + } + } + } + }; + var createDom; + var updateDom; + var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) { + if (oldChildren === newChildren) { + return false; + } + oldChildren = oldChildren || emptyArray; + newChildren = newChildren || emptyArray; + var oldChildrenLength = oldChildren.length; + var newChildrenLength = newChildren.length; + var transitions = projectionOptions.transitions; + var oldIndex = 0; + var newIndex = 0; + var i; + var textUpdated = false; + while (newIndex < newChildrenLength) { + var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined; + var newChild = newChildren[newIndex]; + if (oldChild !== undefined && same(oldChild, newChild)) { + textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated; + oldIndex++; + } else { + var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1); + if (findOldIndex >= 0) { + // Remove preceding missing children + for (i = oldIndex; i < findOldIndex; i++) { + nodeToRemove(oldChildren[i], transitions); + checkDistinguishable(oldChildren, i, vnode, 'removed'); + } + textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated; + oldIndex = findOldIndex + 1; + } else { + // New child + createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions); + nodeAdded(newChild, transitions); + checkDistinguishable(newChildren, newIndex, vnode, 'added'); + } + } + newIndex++; + } + if (oldChildrenLength > oldIndex) { + // Remove child fragments + for (i = oldIndex; i < oldChildrenLength; i++) { + nodeToRemove(oldChildren[i], transitions); + checkDistinguishable(oldChildren, i, vnode, 'removed'); + } + } + return textUpdated; + }; + var addChildren = function (domNode, children, projectionOptions) { + if (!children) { + return; + } + for (var i = 0; i < children.length; i++) { + createDom(children[i], domNode, undefined, projectionOptions); + } + }; + var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) { + addChildren(domNode, vnode.children, projectionOptions); + // children before properties, needed for value property of . + if (vnode.text) { + domNode.textContent = vnode.text; + } + setProperties(domNode, vnode.properties, projectionOptions); + if (vnode.properties && vnode.properties.afterCreate) { + vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children); + } + }; + createDom = function (vnode, parentNode, insertBefore, projectionOptions) { + var domNode, i, c, start = 0, type, found; + var vnodeSelector = vnode.vnodeSelector; + if (vnodeSelector === '') { + domNode = vnode.domNode = document.createTextNode(vnode.text); + if (insertBefore !== undefined) { + parentNode.insertBefore(domNode, insertBefore); + } else { + parentNode.appendChild(domNode); + } + } else { + for (i = 0; i <= vnodeSelector.length; ++i) { + c = vnodeSelector.charAt(i); + if (i === vnodeSelector.length || c === '.' || c === '#') { + type = vnodeSelector.charAt(start - 1); + found = vnodeSelector.slice(start, i); + if (type === '.') { + domNode.classList.add(found); + } else if (type === '#') { + domNode.id = found; + } else { + if (found === 'svg') { + projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); + } + if (projectionOptions.namespace !== undefined) { + domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found); + } else { + domNode = vnode.domNode = document.createElement(found); + } + if (insertBefore !== undefined) { + parentNode.insertBefore(domNode, insertBefore); + } else { + parentNode.appendChild(domNode); + } + } + start = i + 1; + } + } + initPropertiesAndChildren(domNode, vnode, projectionOptions); + } + }; + updateDom = function (previous, vnode, projectionOptions) { + var domNode = previous.domNode; + var textUpdated = false; + if (previous === vnode) { + return false; // By contract, VNode objects may not be modified anymore after passing them to maquette + } + var updated = false; + if (vnode.vnodeSelector === '') { + if (vnode.text !== previous.text) { + var newVNode = document.createTextNode(vnode.text); + domNode.parentNode.replaceChild(newVNode, domNode); + vnode.domNode = newVNode; + textUpdated = true; + return textUpdated; + } + } else { + if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) { + projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG }); + } + if (previous.text !== vnode.text) { + updated = true; + if (vnode.text === undefined) { + domNode.removeChild(domNode.firstChild); // the only textnode presumably + } else { + domNode.textContent = vnode.text; + } + } + updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated; + updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated; + if (vnode.properties && vnode.properties.afterUpdate) { + vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children); + } + } + if (updated && vnode.properties && vnode.properties.updateAnimation) { + vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties); + } + vnode.domNode = previous.domNode; + return textUpdated; + }; + var createProjection = function (vnode, projectionOptions) { + return { + update: function (updatedVnode) { + if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) { + throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)'); + } + updateDom(vnode, updatedVnode, projectionOptions); + vnode = updatedVnode; + }, + domNode: vnode.domNode + }; + }; + ; + // The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'. + exports.h = function (selector) { + var properties = arguments[1]; + if (typeof selector !== 'string') { + throw new Error(); + } + var childIndex = 1; + if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') { + childIndex = 2; + } else { + // Optional properties argument was omitted + properties = undefined; + } + var text = undefined; + var children = undefined; + var argsLength = arguments.length; + // Recognize a common special case where there is only a single text node + if (argsLength === childIndex + 1) { + var onlyChild = arguments[childIndex]; + if (typeof onlyChild === 'string') { + text = onlyChild; + } else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') { + text = onlyChild[0]; + } + } + if (text === undefined) { + children = []; + for (; childIndex < arguments.length; childIndex++) { + var child = arguments[childIndex]; + if (child === null || child === undefined) { + continue; + } else if (Array.isArray(child)) { + appendChildren(selector, child, children); + } else if (child.hasOwnProperty('vnodeSelector')) { + children.push(child); + } else { + children.push(toTextVNode(child)); + } + } + } + return { + vnodeSelector: selector, + properties: properties, + children: children, + text: text === '' ? undefined : text, + domNode: null + }; + }; + /** + * Contains simple low-level utility functions to manipulate the real DOM. + */ + exports.dom = { + /** + * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in + * its [[Projection.domNode|domNode]] property. + * This is a low-level method. Users wil typically use a [[Projector]] instead. + * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] + * objects may only be rendered once. + * @param projectionOptions - Options to be used to create and update the projection. + * @returns The [[Projection]] which also contains the DOM Node that was created. + */ + create: function (vnode, projectionOptions) { + projectionOptions = applyDefaultProjectionOptions(projectionOptions); + createDom(vnode, document.createElement('div'), undefined, projectionOptions); + return createProjection(vnode, projectionOptions); + }, + /** + * Appends a new childnode to the DOM which is generated from a [[VNode]]. + * This is a low-level method. Users wil typically use a [[Projector]] instead. + * @param parentNode - The parent node for the new childNode. + * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] + * objects may only be rendered once. + * @param projectionOptions - Options to be used to create and update the [[Projection]]. + * @returns The [[Projection]] that was created. + */ + append: function (parentNode, vnode, projectionOptions) { + projectionOptions = applyDefaultProjectionOptions(projectionOptions); + createDom(vnode, parentNode, undefined, projectionOptions); + return createProjection(vnode, projectionOptions); + }, + /** + * Inserts a new DOM node which is generated from a [[VNode]]. + * This is a low-level method. Users wil typically use a [[Projector]] instead. + * @param beforeNode - The node that the DOM Node is inserted before. + * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. + * NOTE: [[VNode]] objects may only be rendered once. + * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]]. + * @returns The [[Projection]] that was created. + */ + insertBefore: function (beforeNode, vnode, projectionOptions) { + projectionOptions = applyDefaultProjectionOptions(projectionOptions); + createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions); + return createProjection(vnode, projectionOptions); + }, + /** + * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node. + * This means that the virtual DOM and the real DOM will have one overlapping element. + * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided. + * This is a low-level method. Users wil typically use a [[Projector]] instead. + * @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved. + * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects + * may only be rendered once. + * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]]. + * @returns The [[Projection]] that was created. + */ + merge: function (element, vnode, projectionOptions) { + projectionOptions = applyDefaultProjectionOptions(projectionOptions); + vnode.domNode = element; + initPropertiesAndChildren(element, vnode, projectionOptions); + return createProjection(vnode, projectionOptions); + } + }; + /** + * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees. + * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem. + * For more information, see [[CalculationCache]]. + * + * @param The type of the value that is cached. + */ + exports.createCache = function () { + var cachedInputs = undefined; + var cachedOutcome = undefined; + var result = { + invalidate: function () { + cachedOutcome = undefined; + cachedInputs = undefined; + }, + result: function (inputs, calculation) { + if (cachedInputs) { + for (var i = 0; i < inputs.length; i++) { + if (cachedInputs[i] !== inputs[i]) { + cachedOutcome = undefined; + } + } + } + if (!cachedOutcome) { + cachedOutcome = calculation(); + cachedInputs = inputs; + } + return cachedOutcome; + } + }; + return result; + }; + /** + * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects. + * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}. + * + * @param The type of source items. A database-record for instance. + * @param The type of target items. A [[Component]] for instance. + * @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number. + * @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical + * to the `callback` argument in `Array.map(callback)`. + * @param updateResult `function(source, target, index)` that updates a result to an updated source. + */ + exports.createMapping = function (getSourceKey, createResult, updateResult) { + var keys = []; + var results = []; + return { + results: results, + map: function (newSources) { + var newKeys = newSources.map(getSourceKey); + var oldTargets = results.slice(); + var oldIndex = 0; + for (var i = 0; i < newSources.length; i++) { + var source = newSources[i]; + var sourceKey = newKeys[i]; + if (sourceKey === keys[oldIndex]) { + results[i] = oldTargets[oldIndex]; + updateResult(source, oldTargets[oldIndex], i); + oldIndex++; + } else { + var found = false; + for (var j = 1; j < keys.length; j++) { + var searchIndex = (oldIndex + j) % keys.length; + if (keys[searchIndex] === sourceKey) { + results[i] = oldTargets[searchIndex]; + updateResult(newSources[i], oldTargets[searchIndex], i); + oldIndex = searchIndex + 1; + found = true; + break; + } + } + if (!found) { + results[i] = createResult(source, i); + } + } + } + results.length = newSources.length; + keys = newKeys; + } + }; + }; + /** + * Creates a [[Projector]] instance using the provided projectionOptions. + * + * For more information, see [[Projector]]. + * + * @param projectionOptions Options that influence how the DOM is rendered and updated. + */ + exports.createProjector = function (projectorOptions) { + var projector; + var projectionOptions = applyDefaultProjectionOptions(projectorOptions); + projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) { + return function () { + // intercept function calls (event handlers) to do a render afterwards. + projector.scheduleRender(); + return eventHandler.apply(properties.bind || this, arguments); + }; + }; + var renderCompleted = true; + var scheduled; + var stopped = false; + var projections = []; + var renderFunctions = []; + // matches the projections array + var doRender = function () { + scheduled = undefined; + if (!renderCompleted) { + return; // The last render threw an error, it should be logged in the browser console. + } + renderCompleted = false; + for (var i = 0; i < projections.length; i++) { + var updatedVnode = renderFunctions[i](); + projections[i].update(updatedVnode); + } + renderCompleted = true; + }; + projector = { + scheduleRender: function () { + if (!scheduled && !stopped) { + scheduled = requestAnimationFrame(doRender); + } + }, + stop: function () { + if (scheduled) { + cancelAnimationFrame(scheduled); + scheduled = undefined; + } + stopped = true; + }, + resume: function () { + stopped = false; + renderCompleted = true; + projector.scheduleRender(); + }, + append: function (parentNode, renderMaquetteFunction) { + projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions)); + renderFunctions.push(renderMaquetteFunction); + }, + insertBefore: function (beforeNode, renderMaquetteFunction) { + projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions)); + renderFunctions.push(renderMaquetteFunction); + }, + merge: function (domNode, renderMaquetteFunction) { + projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions)); + renderFunctions.push(renderMaquetteFunction); + }, + replace: function (domNode, renderMaquetteFunction) { + var vnode = renderMaquetteFunction(); + createDom(vnode, domNode.parentNode, domNode, projectionOptions); + domNode.parentNode.removeChild(domNode); + projections.push(createProjection(vnode, projectionOptions)); + renderFunctions.push(renderMaquetteFunction); + }, + detach: function (renderMaquetteFunction) { + for (var i = 0; i < renderFunctions.length; i++) { + if (renderFunctions[i] === renderMaquetteFunction) { + renderFunctions.splice(i, 1); + return projections.splice(i, 1)[0]; + } + } + throw new Error('renderMaquetteFunction was not found'); + } + }; + return projector; + }; +})); diff --git a/plugins/UiPluginManager/media/js/utils/Animation.coffee b/plugins/UiPluginManager/media/js/utils/Animation.coffee new file mode 100644 index 00000000..271b88c1 --- /dev/null +++ b/plugins/UiPluginManager/media/js/utils/Animation.coffee @@ -0,0 +1,138 @@ +class Animation + slideDown: (elem, props) -> + if elem.offsetTop > 2000 + return + + h = elem.offsetHeight + cstyle = window.getComputedStyle(elem) + margin_top = cstyle.marginTop + margin_bottom = cstyle.marginBottom + padding_top = cstyle.paddingTop + padding_bottom = cstyle.paddingBottom + transition = cstyle.transition + + elem.style.boxSizing = "border-box" + elem.style.overflow = "hidden" + elem.style.transform = "scale(0.6)" + elem.style.opacity = "0" + elem.style.height = "0px" + elem.style.marginTop = "0px" + elem.style.marginBottom = "0px" + elem.style.paddingTop = "0px" + elem.style.paddingBottom = "0px" + elem.style.transition = "none" + + setTimeout (-> + elem.className += " animate-inout" + elem.style.height = h+"px" + elem.style.transform = "scale(1)" + elem.style.opacity = "1" + elem.style.marginTop = margin_top + elem.style.marginBottom = margin_bottom + elem.style.paddingTop = padding_top + elem.style.paddingBottom = padding_bottom + ), 1 + + elem.addEventListener "transitionend", -> + elem.classList.remove("animate-inout") + elem.style.transition = elem.style.transform = elem.style.opacity = elem.style.height = null + elem.style.boxSizing = elem.style.marginTop = elem.style.marginBottom = null + elem.style.paddingTop = elem.style.paddingBottom = elem.style.overflow = null + elem.removeEventListener "transitionend", arguments.callee, false + + + slideUp: (elem, remove_func, props) -> + if elem.offsetTop > 1000 + return remove_func() + + elem.className += " animate-back" + elem.style.boxSizing = "border-box" + elem.style.height = elem.offsetHeight+"px" + elem.style.overflow = "hidden" + elem.style.transform = "scale(1)" + elem.style.opacity = "1" + elem.style.pointerEvents = "none" + setTimeout (-> + elem.style.height = "0px" + elem.style.marginTop = "0px" + elem.style.marginBottom = "0px" + elem.style.paddingTop = "0px" + elem.style.paddingBottom = "0px" + elem.style.transform = "scale(0.8)" + elem.style.borderTopWidth = "0px" + elem.style.borderBottomWidth = "0px" + elem.style.opacity = "0" + ), 1 + elem.addEventListener "transitionend", (e) -> + if e.propertyName == "opacity" or e.elapsedTime >= 0.6 + elem.removeEventListener "transitionend", arguments.callee, false + remove_func() + + + slideUpInout: (elem, remove_func, props) -> + elem.className += " animate-inout" + elem.style.boxSizing = "border-box" + elem.style.height = elem.offsetHeight+"px" + elem.style.overflow = "hidden" + elem.style.transform = "scale(1)" + elem.style.opacity = "1" + elem.style.pointerEvents = "none" + setTimeout (-> + elem.style.height = "0px" + elem.style.marginTop = "0px" + elem.style.marginBottom = "0px" + elem.style.paddingTop = "0px" + elem.style.paddingBottom = "0px" + elem.style.transform = "scale(0.8)" + elem.style.borderTopWidth = "0px" + elem.style.borderBottomWidth = "0px" + elem.style.opacity = "0" + ), 1 + elem.addEventListener "transitionend", (e) -> + if e.propertyName == "opacity" or e.elapsedTime >= 0.6 + elem.removeEventListener "transitionend", arguments.callee, false + remove_func() + + + showRight: (elem, props) -> + elem.className += " animate" + elem.style.opacity = 0 + elem.style.transform = "TranslateX(-20px) Scale(1.01)" + setTimeout (-> + elem.style.opacity = 1 + elem.style.transform = "TranslateX(0px) Scale(1)" + ), 1 + elem.addEventListener "transitionend", -> + elem.classList.remove("animate") + elem.style.transform = elem.style.opacity = null + + + show: (elem, props) -> + delay = arguments[arguments.length-2]?.delay*1000 or 1 + elem.style.opacity = 0 + setTimeout (-> + elem.className += " animate" + ), 1 + setTimeout (-> + elem.style.opacity = 1 + ), delay + elem.addEventListener "transitionend", -> + elem.classList.remove("animate") + elem.style.opacity = null + elem.removeEventListener "transitionend", arguments.callee, false + + hide: (elem, remove_func, props) -> + delay = arguments[arguments.length-2]?.delay*1000 or 1 + elem.className += " animate" + setTimeout (-> + elem.style.opacity = 0 + ), delay + elem.addEventListener "transitionend", (e) -> + if e.propertyName == "opacity" + remove_func() + + addVisibleClass: (elem, props) -> + setTimeout -> + elem.classList.add("visible") + +window.Animation = new Animation() \ No newline at end of file diff --git a/plugins/UiPluginManager/media/js/utils/Dollar.coffee b/plugins/UiPluginManager/media/js/utils/Dollar.coffee new file mode 100644 index 00000000..7f19f551 --- /dev/null +++ b/plugins/UiPluginManager/media/js/utils/Dollar.coffee @@ -0,0 +1,3 @@ +window.$ = (selector) -> + if selector.startsWith("#") + return document.getElementById(selector.replace("#", "")) diff --git a/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee b/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee new file mode 100644 index 00000000..11512d16 --- /dev/null +++ b/plugins/UiPluginManager/media/js/utils/ZeroFrame.coffee @@ -0,0 +1,85 @@ +class ZeroFrame extends Class + constructor: (url) -> + @url = url + @waiting_cb = {} + @wrapper_nonce = document.location.href.replace(/.*wrapper_nonce=([A-Za-z0-9]+).*/, "$1") + @connect() + @next_message_id = 1 + @history_state = {} + @init() + + + init: -> + @ + + + connect: -> + @target = window.parent + window.addEventListener("message", @onMessage, false) + @cmd("innerReady") + + # Save scrollTop + window.addEventListener "beforeunload", (e) => + @log "save scrollTop", window.pageYOffset + @history_state["scrollTop"] = window.pageYOffset + @cmd "wrapperReplaceState", [@history_state, null] + + # Restore scrollTop + @cmd "wrapperGetState", [], (state) => + @history_state = state if state? + @log "restore scrollTop", state, window.pageYOffset + if window.pageYOffset == 0 and state + window.scroll(window.pageXOffset, state.scrollTop) + + + onMessage: (e) => + message = e.data + cmd = message.cmd + if cmd == "response" + if @waiting_cb[message.to]? + @waiting_cb[message.to](message.result) + else + @log "Websocket callback not found:", message + else if cmd == "wrapperReady" # Wrapper inited later + @cmd("innerReady") + else if cmd == "ping" + @response message.id, "pong" + else if cmd == "wrapperOpenedWebsocket" + @onOpenWebsocket() + else if cmd == "wrapperClosedWebsocket" + @onCloseWebsocket() + else + @onRequest cmd, message.params + + + onRequest: (cmd, message) => + @log "Unknown request", message + + + response: (to, result) -> + @send {"cmd": "response", "to": to, "result": result} + + + cmd: (cmd, params={}, cb=null) -> + @send {"cmd": cmd, "params": params}, cb + + + send: (message, cb=null) -> + message.wrapper_nonce = @wrapper_nonce + message.id = @next_message_id + @next_message_id += 1 + @target.postMessage(message, "*") + if cb + @waiting_cb[message.id] = cb + + + onOpenWebsocket: => + @log "Websocket open" + + + onCloseWebsocket: => + @log "Websocket close" + + + +window.ZeroFrame = ZeroFrame diff --git a/plugins/UiPluginManager/media/plugin_manager.html b/plugins/UiPluginManager/media/plugin_manager.html new file mode 100644 index 00000000..321cbbb3 --- /dev/null +++ b/plugins/UiPluginManager/media/plugin_manager.html @@ -0,0 +1,19 @@ + + + + + Settings - ZeroNet + + + + + + +

ZeroNet plugin manager

+ +
+
+ + + + \ No newline at end of file From 7e9ab8321a34f1bd9f5ddffb14fbb069c53d1ece Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Aug 2019 01:32:55 +0200 Subject: [PATCH 025/483] Add plugin description data --- plugins/AnnounceBitTorrent/plugin_info.json | 5 +++++ plugins/AnnounceLocal/plugin_info.json | 5 +++++ plugins/AnnounceShare/plugin_info.json | 5 +++++ plugins/AnnounceZero/plugin_info.json | 5 +++++ plugins/Chart/plugin_info.json | 5 +++++ plugins/ContentFilter/plugin_info.json | 5 +++++ plugins/Cors/plugin_info.json | 5 +++++ plugins/CryptMessage/plugin_info.json | 5 +++++ plugins/FilePack/plugin_info.json | 5 +++++ plugins/PeerDb/plugin_info.json | 5 +++++ plugins/Sidebar/plugin_info.json | 5 +++++ plugins/Stats/plugin_info.json | 5 +++++ plugins/TranslateSite/plugin_info.json | 5 +++++ plugins/Trayicon/plugin_info.json | 5 +++++ plugins/UiConfig/plugin_info.json | 5 +++++ plugins/disabled-Bootstrapper/plugin_info.json | 5 +++++ plugins/disabled-Multiuser/plugin_info.json | 5 +++++ plugins/disabled-UiPassword/plugin_info.json | 5 +++++ 18 files changed, 90 insertions(+) create mode 100644 plugins/AnnounceBitTorrent/plugin_info.json create mode 100644 plugins/AnnounceLocal/plugin_info.json create mode 100644 plugins/AnnounceShare/plugin_info.json create mode 100644 plugins/AnnounceZero/plugin_info.json create mode 100644 plugins/Chart/plugin_info.json create mode 100644 plugins/ContentFilter/plugin_info.json create mode 100644 plugins/Cors/plugin_info.json create mode 100644 plugins/CryptMessage/plugin_info.json create mode 100644 plugins/FilePack/plugin_info.json create mode 100644 plugins/PeerDb/plugin_info.json create mode 100644 plugins/Sidebar/plugin_info.json create mode 100644 plugins/Stats/plugin_info.json create mode 100644 plugins/TranslateSite/plugin_info.json create mode 100644 plugins/Trayicon/plugin_info.json create mode 100644 plugins/UiConfig/plugin_info.json create mode 100644 plugins/disabled-Bootstrapper/plugin_info.json create mode 100644 plugins/disabled-Multiuser/plugin_info.json create mode 100644 plugins/disabled-UiPassword/plugin_info.json diff --git a/plugins/AnnounceBitTorrent/plugin_info.json b/plugins/AnnounceBitTorrent/plugin_info.json new file mode 100644 index 00000000..824749ee --- /dev/null +++ b/plugins/AnnounceBitTorrent/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "AnnounceBitTorrent", + "description": "Discover new peers using BitTorrent trackers.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/AnnounceLocal/plugin_info.json b/plugins/AnnounceLocal/plugin_info.json new file mode 100644 index 00000000..2908cbf1 --- /dev/null +++ b/plugins/AnnounceLocal/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "AnnounceLocal", + "description": "Discover LAN clients using UDP broadcasting.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/AnnounceShare/plugin_info.json b/plugins/AnnounceShare/plugin_info.json new file mode 100644 index 00000000..0ad07e71 --- /dev/null +++ b/plugins/AnnounceShare/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "AnnounceShare", + "description": "Share possible trackers between clients.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/AnnounceZero/plugin_info.json b/plugins/AnnounceZero/plugin_info.json new file mode 100644 index 00000000..50e7cf7f --- /dev/null +++ b/plugins/AnnounceZero/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "AnnounceZero", + "description": "Announce using ZeroNet protocol.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/Chart/plugin_info.json b/plugins/Chart/plugin_info.json new file mode 100644 index 00000000..3bdaea8a --- /dev/null +++ b/plugins/Chart/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "Chart", + "description": "Collect and provide stats of client information.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/ContentFilter/plugin_info.json b/plugins/ContentFilter/plugin_info.json new file mode 100644 index 00000000..f63bc984 --- /dev/null +++ b/plugins/ContentFilter/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "ContentFilter", + "description": "Manage site and user block list.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/Cors/plugin_info.json b/plugins/Cors/plugin_info.json new file mode 100644 index 00000000..f8af18fa --- /dev/null +++ b/plugins/Cors/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "Cors", + "description": "Cross site resource read.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/CryptMessage/plugin_info.json b/plugins/CryptMessage/plugin_info.json new file mode 100644 index 00000000..96dfdd89 --- /dev/null +++ b/plugins/CryptMessage/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "CryptMessage", + "description": "Cryptographic functions of ECIES and AES data encryption/decryption.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/FilePack/plugin_info.json b/plugins/FilePack/plugin_info.json new file mode 100644 index 00000000..42112f95 --- /dev/null +++ b/plugins/FilePack/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "FilePack", + "description": "Transparent web access for Zip and Tar.gz files.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/PeerDb/plugin_info.json b/plugins/PeerDb/plugin_info.json new file mode 100644 index 00000000..b13915ff --- /dev/null +++ b/plugins/PeerDb/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "PeerDb", + "description": "Save/restore peer list on client restart.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/Sidebar/plugin_info.json b/plugins/Sidebar/plugin_info.json new file mode 100644 index 00000000..7d6f91fc --- /dev/null +++ b/plugins/Sidebar/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "Sidebar", + "description": "Access site management sidebar and console by dragging top-right 0 button to left or down.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/Stats/plugin_info.json b/plugins/Stats/plugin_info.json new file mode 100644 index 00000000..1f401a4f --- /dev/null +++ b/plugins/Stats/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "Stats", + "description": "/Stats and /Benchmark pages.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/TranslateSite/plugin_info.json b/plugins/TranslateSite/plugin_info.json new file mode 100644 index 00000000..1d520eda --- /dev/null +++ b/plugins/TranslateSite/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "TranslateSite", + "description": "Transparent support translation of site javascript and html files.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/Trayicon/plugin_info.json b/plugins/Trayicon/plugin_info.json new file mode 100644 index 00000000..d74fa6c1 --- /dev/null +++ b/plugins/Trayicon/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "Trayicon", + "description": "Icon for system tray. (Windows only)", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/UiConfig/plugin_info.json b/plugins/UiConfig/plugin_info.json new file mode 100644 index 00000000..01e9dd31 --- /dev/null +++ b/plugins/UiConfig/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "UiConfig", + "description": "Change client settings using the web interface.", + "default": "enabled" +} \ No newline at end of file diff --git a/plugins/disabled-Bootstrapper/plugin_info.json b/plugins/disabled-Bootstrapper/plugin_info.json new file mode 100644 index 00000000..06915d4d --- /dev/null +++ b/plugins/disabled-Bootstrapper/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "Bootstrapper", + "description": "Add BitTorrent tracker server like features to your ZeroNet client.", + "default": "disabled" +} \ No newline at end of file diff --git a/plugins/disabled-Multiuser/plugin_info.json b/plugins/disabled-Multiuser/plugin_info.json new file mode 100644 index 00000000..e440ed8e --- /dev/null +++ b/plugins/disabled-Multiuser/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "MultiUser", + "description": "Cookie based multi-users support on your ZeroNet web interface.", + "default": "disabled" +} \ No newline at end of file diff --git a/plugins/disabled-UiPassword/plugin_info.json b/plugins/disabled-UiPassword/plugin_info.json new file mode 100644 index 00000000..d3649a17 --- /dev/null +++ b/plugins/disabled-UiPassword/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "UiPassword", + "description": "Password based autentication on the web interface.", + "default": "disabled" +} \ No newline at end of file From 21def8143985000df4c76bcd9f73162bcc3524ac Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Aug 2019 01:34:00 +0200 Subject: [PATCH 026/483] Fix js, css merging with absolute merged_path --- src/Debug/DebugMedia.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Debug/DebugMedia.py b/src/Debug/DebugMedia.py index a24203b9..a892dc56 100644 --- a/src/Debug/DebugMedia.py +++ b/src/Debug/DebugMedia.py @@ -45,6 +45,7 @@ def findCoffeescriptCompiler(): # Generates: all.js: merge *.js, compile coffeescript, all.css: merge *.css, vendor prefix features def merge(merged_path): + merged_path = merged_path.replace("\\", "/") merge_dir = os.path.dirname(merged_path) s = time.time() ext = merged_path.split(".")[-1] @@ -77,9 +78,10 @@ def merge(merged_path): parts = [] s_total = time.time() for file_path in findfiles(merge_dir, find_ext): - parts.append(b"\n/* ---- %s ---- */\n\n" % file_path.replace(config.data_dir, "").encode("utf8")) + file_relative_path = file_path.replace(merge_dir + "/", "") + parts.append(b"\n/* ---- %s ---- */\n\n" % file_relative_path.encode("utf8")) if file_path.endswith(".coffee"): # Compile coffee script - if file_path in changed or file_path.replace(config.data_dir, "") not in old_parts: # Only recompile if changed or its not compiled before + if file_path in changed or file_relative_path not in old_parts: # Only recompile if changed or its not compiled before if config.coffeescript_compiler is None: config.coffeescript_compiler = findCoffeescriptCompiler() if not config.coffeescript_compiler: @@ -90,7 +92,7 @@ def merge(merged_path): file_path_escaped = helper.shellquote(file_path.replace("/", os.path.sep)) if "%s" in config.coffeescript_compiler: # Replace %s with coffeescript file - command = config.coffeescript_compiler % file_path_escaped + command = config.coffeescript_compiler.replace("%s", file_path_escaped) else: # Put coffeescript file to end command = config.coffeescript_compiler + " " + file_path_escaped @@ -106,14 +108,14 @@ def merge(merged_path): parts.append(out) else: # Put error message in place of source code error = out - logging.error("%s Compile error: %s" % (file_path, error)) + logging.error("%s Compile error: %s" % (file_relative_path, error)) error_escaped = re.escape(error).replace(b"\n", b"\\n").replace(br"\\n", br"\n") parts.append( b"alert('%s compile error: %s');" % - (file_path.encode(), error_escaped) + (file_relative_path.encode(), error_escaped) ) else: # Not changed use the old_part - parts.append(old_parts[file_path.replace(config.data_dir, "")]) + parts.append(old_parts[file_relative_path]) else: # Add to parts parts.append(open(file_path, "rb").read()) From 39f318fbd5ac1d28417c78901f2374277be78509 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Aug 2019 01:34:21 +0200 Subject: [PATCH 027/483] Add plugin version information to server_info --- src/Ui/UiWebsocket.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index f964dd90..77f4ec50 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -308,6 +308,7 @@ class UiWebsocket(object): "debug": config.debug, "offline": config.offline, "plugins": PluginManager.plugin_manager.plugin_names, + "plugins_rev": PluginManager.plugin_manager.plugins_rev, "user_settings": self.user.settings } if "ADMIN" in self.site.settings["permissions"]: From 605ae75dda86240551009b8fb2d26266dbc94455 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Aug 2019 01:35:00 +0200 Subject: [PATCH 028/483] Re-compile UiConfig js, css, backdrop to bottom popup --- plugins/UiConfig/media/css/Config.css | 6 +++--- plugins/UiConfig/media/css/all.css | 11 +++++------ plugins/UiConfig/media/css/button.css | 2 +- plugins/UiConfig/media/js/all.js | 20 ++++++++++---------- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/plugins/UiConfig/media/css/Config.css b/plugins/UiConfig/media/css/Config.css index 98291d33..2211758e 100644 --- a/plugins/UiConfig/media/css/Config.css +++ b/plugins/UiConfig/media/css/Config.css @@ -1,8 +1,8 @@ body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; backface-visibility: hidden; } h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px } +h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } h2 { margin-top: 10px; } h3 { font-weight: normal } -h1 { background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } a { color: #9760F9 } a:hover { text-decoration: none } @@ -53,7 +53,7 @@ a:hover { text-decoration: none } /* Bottom */ .bottom { - width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; + width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); position: fixed; backface-visibility: hidden; box-sizing: border-box; } .bottom-content { max-width: 750px; width: 100%; margin: 0px auto; } @@ -65,4 +65,4 @@ a:hover { text-decoration: none } .animate { transition: all 0.3s ease-out !important; } .animate-back { transition: all 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important; } -.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; } \ No newline at end of file +.animate-inout { transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; } diff --git a/plugins/UiConfig/media/css/all.css b/plugins/UiConfig/media/css/all.css index 7bb0087a..2b2991d0 100644 --- a/plugins/UiConfig/media/css/all.css +++ b/plugins/UiConfig/media/css/all.css @@ -1,13 +1,12 @@ - -/* ---- plugins/UiConfig/media/css/Config.css ---- */ +/* ---- Config.css ---- */ body { background-color: #EDF2F5; font-family: Roboto, 'Segoe UI', Arial, 'Helvetica Neue'; margin: 0px; padding: 0px; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; } h1, h2, h3, h4 { font-family: 'Roboto', Arial, sans-serif; font-weight: 200; font-size: 30px; margin: 0px; padding: 0px } +h1 { background: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } h2 { margin-top: 10px; } h3 { font-weight: normal } -h1 { background: -webkit-linear-gradient(33deg,#af3bff,#0d99c9);background: -moz-linear-gradient(33deg,#af3bff,#0d99c9);background: -o-linear-gradient(33deg,#af3bff,#0d99c9);background: -ms-linear-gradient(33deg,#af3bff,#0d99c9);background: linear-gradient(33deg,#af3bff,#0d99c9); color: white; padding: 16px 30px; } a { color: #9760F9 } a:hover { text-decoration: none } @@ -58,7 +57,7 @@ a:hover { text-decoration: none } /* Bottom */ .bottom { - width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; + width: 100%; text-align: center; background-color: #ffffffde; padding: 25px; bottom: -120px; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); -webkit-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -moz-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -o-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); -ms-transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1); transition: all 0.8s cubic-bezier(0.86, 0, 0.07, 1) ; position: fixed; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; } .bottom-content { max-width: 750px; width: 100%; margin: 0px auto; } @@ -73,7 +72,7 @@ a:hover { text-decoration: none } .animate-inout { -webkit-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -moz-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -o-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; -ms-transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important; transition: all 0.6s cubic-bezier(0.77, 0, 0.175, 1) !important ; } -/* ---- plugins/UiConfig/media/css/button.css ---- */ +/* ---- button.css ---- */ /* Button */ @@ -90,7 +89,7 @@ a:hover { text-decoration: none } .button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } -/* ---- plugins/UiConfig/media/css/fonts.css ---- */ +/* ---- fonts.css ---- */ /* Base64 encoder: http://www.motobit.com/util/base64-decoder-encoder.asp */ diff --git a/plugins/UiConfig/media/css/button.css b/plugins/UiConfig/media/css/button.css index 9f46d478..f69021bf 100644 --- a/plugins/UiConfig/media/css/button.css +++ b/plugins/UiConfig/media/css/button.css @@ -9,4 +9,4 @@ color: rgba(0,0,0,0); background: #999 url(../img/loading.gif) no-repeat center center; transition: all 0.5s ease-out ; pointer-events: none; border-bottom: 2px solid #666 } -.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } \ No newline at end of file +.button.disabled { color: #DDD; background-color: #999; pointer-events: none; border-bottom: 2px solid #666 } diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index 99c3a6d8..0d5c401e 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1,5 +1,5 @@ -/* ---- plugins/UiConfig/media/js/lib/Class.coffee ---- */ +/* ---- lib/Class.coffee ---- */ (function() { @@ -56,7 +56,7 @@ }).call(this); -/* ---- plugins/UiConfig/media/js/lib/Promise.coffee ---- */ +/* ---- lib/Promise.coffee ---- */ (function() { @@ -160,7 +160,7 @@ }).call(this); -/* ---- plugins/UiConfig/media/js/lib/Prototypes.coffee ---- */ +/* ---- lib/Prototypes.coffee ---- */ (function() { @@ -187,7 +187,7 @@ }).call(this); -/* ---- plugins/UiConfig/media/js/lib/maquette.js ---- */ +/* ---- lib/maquette.js ---- */ (function (root, factory) { @@ -962,7 +962,7 @@ })); -/* ---- plugins/UiConfig/media/js/utils/Animation.coffee ---- */ +/* ---- utils/Animation.coffee ---- */ (function() { @@ -1129,7 +1129,7 @@ }).call(this); -/* ---- plugins/UiConfig/media/js/utils/Dollar.coffee ---- */ +/* ---- utils/Dollar.coffee ---- */ (function() { @@ -1142,7 +1142,7 @@ }).call(this); -/* ---- plugins/UiConfig/media/js/utils/ZeroFrame.coffee ---- */ +/* ---- utils/ZeroFrame.coffee ---- */ (function() { @@ -1274,7 +1274,7 @@ }).call(this); -/* ---- plugins/UiConfig/media/js/ConfigStorage.coffee ---- */ +/* ---- ConfigStorage.coffee ---- */ (function() { @@ -1504,7 +1504,7 @@ }).call(this); -/* ---- plugins/UiConfig/media/js/ConfigView.coffee ---- */ +/* ---- ConfigView.coffee ---- */ (function() { @@ -1708,7 +1708,7 @@ }).call(this); -/* ---- plugins/UiConfig/media/js/UiConfig.coffee ---- */ +/* ---- UiConfig.coffee ---- */ (function() { From b6e1559a80ce240aa7c130a1d314452e6d9d425f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 3 Aug 2019 01:35:37 +0200 Subject: [PATCH 029/483] Rev4163 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index ee6e939b..be705718 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4129 + self.rev = 4163 self.argv = argv self.action = None self.pending_changes = {} From 8c6400e4d68f673ba7970adadcad39c455fbcd4a Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Tue, 6 Aug 2019 14:56:45 +0200 Subject: [PATCH 030/483] Correct venv install --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b981863..2e2f6857 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,9 @@ Install Python module dependencies either: * (Option A) into a [virtual env](https://virtualenv.readthedocs.org/en/latest/) ``` - virtualenv zeronet + python3 -m venv zeronet source zeronet/bin/activate - python -m pip install -r requirements.txt + python3 -m pip install -r requirements.txt ``` * (Option B) into the system (requires root), for example, on Debian/Ubuntu: From 6cd18bbf04c98059dba12244c26831c25b0c3538 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 7 Aug 2019 14:11:30 +0200 Subject: [PATCH 031/483] Display more clean error on users.json/sites.json load error --- src/Site/SiteManager.py | 8 +++++++- src/User/UserManager.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 4af4e2d5..bb9fe308 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -33,7 +33,13 @@ class SiteManager(object): address_found = [] added = 0 # Load new adresses - for address, settings in json.load(open("%s/sites.json" % config.data_dir)).items(): + try: + json_path = "%s/sites.json" % config.data_dir + data = json.load(open(json_path)) + except Exception as err: + raise Exception("Unable to load %s: %s" % (json_path, err)) + + for address, settings in data.items(): if address not in self.sites: if os.path.isfile("%s/%s/content.json" % (config.data_dir, address)): # Root content.json exists, try load site diff --git a/src/User/UserManager.py b/src/User/UserManager.py index e1f069c0..067734a6 100644 --- a/src/User/UserManager.py +++ b/src/User/UserManager.py @@ -24,7 +24,13 @@ class UserManager(object): added = 0 s = time.time() # Load new users - for master_address, data in list(json.load(open("%s/users.json" % config.data_dir)).items()): + try: + json_path = "%s/users.json" % config.data_dir + data = json.load(open(json_path)) + except Exception as err: + raise Exception("Unable to load %s: %s" % (json_path, err)) + + for master_address, data in list(data.items()): if master_address not in self.users: user = User(master_address, data=data) self.users[master_address] = user From b9b317e213344acf03df65613b0f8c9b589c5969 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 7 Aug 2019 14:11:58 +0200 Subject: [PATCH 032/483] Remove accidently left print on plugin load --- src/Plugin/PluginManager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py index dae4f2ec..a08f0a69 100644 --- a/src/Plugin/PluginManager.py +++ b/src/Plugin/PluginManager.py @@ -127,7 +127,6 @@ class PluginManager: def loadPlugins(self): all_loaded = True s = time.time() - print(sys.path) for plugin in self.listPlugins(): self.log.debug("Loading plugin: %s (%s)" % (plugin["name"], plugin["source"])) if plugin["source"] != "builtin": From b22343f65c93357b37e5371215717f168f223eaf Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 7 Aug 2019 14:12:45 +0200 Subject: [PATCH 033/483] Support multiple trackers_file argument --- .../UiConfig/media/js/ConfigStorage.coffee | 2 +- plugins/UiConfig/media/js/all.js | 2 +- src/Config.py | 29 ++++++++++--------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index 654f0363..1dc4c8b4 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -109,7 +109,7 @@ class ConfigStorage extends Class section.items.push title: "Trackers files" key: "trackers_file" - type: "text" + type: "textarea" description: "Load additional list of torrent trackers dynamically, from a file" placeholder: "Eg.: data/trackers.json" value_pos: "fullwidth" diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index 0d5c401e..4c2f279f 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1430,7 +1430,7 @@ section.items.push({ title: "Trackers files", key: "trackers_file", - type: "text", + type: "textarea", description: "Load additional list of torrent trackers dynamically, from a file", placeholder: "Eg.: data/trackers.json", value_pos: "fullwidth" diff --git a/src/Config.py b/src/Config.py index be705718..a52dea97 100644 --- a/src/Config.py +++ b/src/Config.py @@ -250,7 +250,7 @@ class Config(object): self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port') self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip') self.parser.add_argument('--trackers', help='Bootstraping torrent trackers', default=trackers, metavar='protocol://address', nargs='*') - self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', default=False, metavar='path') + self.parser.add_argument('--trackers_file', help='Load torrent trackers dynamically from a file', metavar='path', nargs='*') self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable") self.parser.add_argument('--use_libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True) self.parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=True) @@ -296,20 +296,21 @@ class Config(object): self.trackers = self.arguments.trackers[:] - try: - if self.trackers_file.startswith("/"): # Absolute - trackers_file_path = self.trackers_file - elif self.trackers_file.startswith("{data_dir}"): # Relative to data_dir - trackers_file_path = self.trackers_file.replace("{data_dir}", self.data_dir) - else: # Relative to zeronet.py - trackers_file_path = self.start_dir + "/" + self.trackers_file + for trackers_file in self.trackers_file: + try: + if trackers_file.startswith("/"): # Absolute + trackers_file_path = trackers_file + elif trackers_file.startswith("{data_dir}"): # Relative to data_dir + trackers_file_path = trackers_file.replace("{data_dir}", self.data_dir) + else: # Relative to zeronet.py + trackers_file_path = self.start_dir + "/" + trackers_file - for line in open(trackers_file_path): - tracker = line.strip() - if "://" in tracker and tracker not in self.trackers: - self.trackers.append(tracker) - except Exception as err: - print("Error loading trackers file: %s" % err) + for line in open(trackers_file_path): + tracker = line.strip() + if "://" in tracker and tracker not in self.trackers: + self.trackers.append(tracker) + except Exception as err: + print("Error loading trackers file: %s" % err) # Find arguments specified for current action def getActionArguments(self): From b5a1310addff2583f3b98a50400dcca39a670eb7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 7 Aug 2019 14:12:53 +0200 Subject: [PATCH 034/483] Rev4165 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index a52dea97..deb8627c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4163 + self.rev = 4165 self.argv = argv self.action = None self.pending_changes = {} From f4bec3bb4de2ccd87b8a77cebf0e7d3b9aef74ba Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 14:35:04 +0200 Subject: [PATCH 035/483] Avoid starting new workers on possibly unvalaible file --- src/Worker/WorkerManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index c514fd25..236b048a 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -430,7 +430,7 @@ class WorkerManager(object): self.startFindOptional(find_more=True) else: self.startFindOptional() - elif self.tasks and not self.workers and worker.task: + elif self.tasks and not self.workers and worker.task and len(worker.task["failed"]) < 20: self.log.debug("Starting new workers... (tasks: %s)" % len(self.tasks)) self.startWorkers(reason="Removed worker") From eeaa5d21d8e96f7627116feb99a8118310ca23a6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 14:35:35 +0200 Subject: [PATCH 036/483] Start unrealible trackers on force reannounce --- src/Site/SiteAnnouncer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 2e2a353f..0c553aa8 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -90,7 +90,8 @@ class SiteAnnouncer(object): for tracker in trackers: # Start announce threads tracker_stats = global_stats[tracker] # Reduce the announce time for trackers that looks unreliable - if tracker_stats["num_error"] > 5 and tracker_stats["time_request"] > time.time() - 60 * min(30, tracker_stats["num_error"]): + time_announce_allowed = time.time() - 60 * min(30, tracker_stats["num_error"]) + if tracker_stats["num_error"] > 5 and tracker_stats["time_request"] > time_announce_allowed and not force: if config.verbose: self.site.log.debug("Tracker %s looks unreliable, announce skipped (error: %s)" % (tracker, tracker_stats["num_error"])) continue From 3696db89ab40279072a40ced8a655fc23822dc13 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 14:35:58 +0200 Subject: [PATCH 037/483] Don't increment tracker error number if no internet connection --- src/Site/SiteAnnouncer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 0c553aa8..c1a24779 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -207,10 +207,12 @@ class SiteAnnouncer(object): self.stats[tracker]["time_status"] = time.time() self.stats[tracker]["last_error"] = str(error) self.stats[tracker]["time_last_error"] = time.time() - self.stats[tracker]["num_error"] += 1 + if self.site.connection_server.has_internet: + self.stats[tracker]["num_error"] += 1 self.stats[tracker]["num_request"] += 1 global_stats[tracker]["num_request"] += 1 - global_stats[tracker]["num_error"] += 1 + if self.site.connection_server.has_internet: + global_stats[tracker]["num_error"] += 1 self.updateWebsocket(tracker="error") return False From bf10cdef632906bb7c40fab9b8952d84ad5e9709 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 14:36:33 +0200 Subject: [PATCH 038/483] Add some delay on pex error before try the next peer --- src/Site/SiteAnnouncer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index c1a24779..a25edf6b 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -274,6 +274,8 @@ class SiteAnnouncer(object): if num_added: self.site.worker_manager.onPeers() self.site.updateWebsocket(peers_added=num_added) + else: + time.sleep(0.1) if done == query_num: break self.site.log.debug("Pex result: from %s peers got %s new peers." % (done, total_added)) From 88f2b39576e39f5cf7135a438f7b8a674d0cbeb6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 14:37:19 +0200 Subject: [PATCH 039/483] Don't try to connect to onion addresses if not supported by the client --- src/Site/Site.py | 11 +++++++++-- src/Site/SiteAnnouncer.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 967a776f..c08e067e 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -823,7 +823,7 @@ class Site(object): self.log.debug("Need connections: %s, Current: %s, Total: %s" % (need, connected, len(self.peers))) if connected < need: # Need more than we have - for peer in list(self.peers.values()): + for peer in self.getRecentPeers(30): if not peer.connection or not peer.connection.connected: # No peer connection or disconnected peer.pex() # Initiate peer exchange if peer.connection and peer.connection.connected: @@ -849,6 +849,8 @@ class Site(object): continue # Not connectable if not peer.connection: continue # No connection + if peer.ip.endswith(".onion") and not self.connection_server.tor_manager.enabled: + continue # Onion not supported if peer.key in ignore: continue # The requester has this peer if time.time() - peer.connection.last_recv_time > 60 * 60 * 2: # Last message more than 2 hours ago @@ -884,8 +886,13 @@ class Site(object): # Add random peers need_more = need_num - len(found) + if not self.connection_server.tor_manager.enabled: + peers = [peer for peer in self.peers.values() if not peer.ip.endswith(".onion")] + else: + peers = list(self.peers.values()) + found_more = sorted( - list(self.peers.values())[0:need_more * 50], + peers[0:need_more * 50], key=lambda peer: peer.reputation, reverse=True )[0:need_more * 2] diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index a25edf6b..c06a0c61 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -260,7 +260,7 @@ class SiteAnnouncer(object): peers = self.site.getConnectedPeers() if len(peers) == 0: # Small number of connected peers for this site, connect to any - peers = list(self.site.peers.values()) + peers = list(self.site.getRecentPeers(20)) need_num = 10 random.shuffle(peers) From 44ef0cbe59fb160290efcc96831ec00200a1b2fb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 14:37:42 +0200 Subject: [PATCH 040/483] Always load plugins abc sorted --- src/Plugin/PluginManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugin/PluginManager.py b/src/Plugin/PluginManager.py index a08f0a69..dbafa98f 100644 --- a/src/Plugin/PluginManager.py +++ b/src/Plugin/PluginManager.py @@ -99,10 +99,10 @@ class PluginManager: def listInstalledPlugins(self, list_disabled=False): plugins = [] - for address, site_plugins in self.config.items(): + for address, site_plugins in sorted(self.config.items()): if address == "builtin": continue - for plugin_inner_path, plugin_config in site_plugins.items(): + for plugin_inner_path, plugin_config in sorted(site_plugins.items()): is_enabled = plugin_config.get("enabled", False) if not is_enabled and not list_disabled: continue From 79ba4a9d23daccf386b446a97ce214d1f3c1e326 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 14:39:02 +0200 Subject: [PATCH 041/483] Rev4167 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index deb8627c..f9d5e62b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4165 + self.rev = 4167 self.argv = argv self.action = None self.pending_changes = {} From cc21cbd1bd9411d602eb9a440889082e4c73b921 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 23:36:58 +0200 Subject: [PATCH 042/483] Use relative import in pyelliptic --- src/lib/pyelliptic/ecc.py | 6 +++--- src/lib/pyelliptic/hash.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/pyelliptic/ecc.py b/src/lib/pyelliptic/ecc.py index a70a1a5b..a45f8d78 100644 --- a/src/lib/pyelliptic/ecc.py +++ b/src/lib/pyelliptic/ecc.py @@ -12,9 +12,9 @@ pyelliptic/ecc.py from hashlib import sha512 from struct import pack, unpack -from pyelliptic.cipher import Cipher -from pyelliptic.hash import equals, hmac_sha256 -from pyelliptic.openssl import OpenSSL +from .cipher import Cipher +from .hash import equals, hmac_sha256 +from .openssl import OpenSSL class ECC(object): diff --git a/src/lib/pyelliptic/hash.py b/src/lib/pyelliptic/hash.py index fb910dd4..d6a15811 100644 --- a/src/lib/pyelliptic/hash.py +++ b/src/lib/pyelliptic/hash.py @@ -4,7 +4,7 @@ # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. -from pyelliptic.openssl import OpenSSL +from .openssl import OpenSSL # For python3 From 5da46ca29cfe35311c80a9a3cf39e286b1e9b36a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 23:37:21 +0200 Subject: [PATCH 043/483] Cleanup whitespace in pyelliptic --- src/lib/pyelliptic/openssl.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py index 02551267..7bc88fa4 100644 --- a/src/lib/pyelliptic/openssl.py +++ b/src/lib/pyelliptic/openssl.py @@ -175,7 +175,7 @@ class _OpenSSL: self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL self._lib.EC_KEY_OpenSSL.restype = ctypes.c_void_p self._lib.EC_KEY_OpenSSL.argtypes = [] - + self.EC_KEY_set_method = self._lib.EC_KEY_set_method self._lib.EC_KEY_set_method.restype = ctypes.c_int self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] @@ -250,7 +250,7 @@ class _OpenSSL: self.EVP_rc4 = self._lib.EVP_rc4 self.EVP_rc4.restype = ctypes.c_void_p self.EVP_rc4.argtypes = [] - + if self._hexversion >= 0x10100000 and not self._libreSSL: self.EVP_CIPHER_CTX_reset = self._lib.EVP_CIPHER_CTX_reset self.EVP_CIPHER_CTX_reset.restype = ctypes.c_int @@ -281,7 +281,7 @@ class _OpenSSL: self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex self.EVP_DigestInit_ex.restype = ctypes.c_int self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p] - + self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate self.EVP_DigestUpdate.restype = ctypes.c_int self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, @@ -296,7 +296,7 @@ class _OpenSSL: self.EVP_DigestFinal_ex.restype = ctypes.c_int self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - + self.ECDSA_sign = self._lib.ECDSA_sign self.ECDSA_sign.restype = ctypes.c_int self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, @@ -311,7 +311,7 @@ class _OpenSSL: self.EVP_MD_CTX_new = self._lib.EVP_MD_CTX_new self.EVP_MD_CTX_new.restype = ctypes.c_void_p self.EVP_MD_CTX_new.argtypes = [] - + self.EVP_MD_CTX_reset = self._lib.EVP_MD_CTX_reset self.EVP_MD_CTX_reset.restype = None self.EVP_MD_CTX_reset.argtypes = [ctypes.c_void_p] @@ -329,11 +329,11 @@ class _OpenSSL: self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create self.EVP_MD_CTX_create.restype = ctypes.c_void_p self.EVP_MD_CTX_create.argtypes = [] - + self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init self.EVP_MD_CTX_init.restype = None self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p] - + self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy self.EVP_MD_CTX_destroy.restype = None self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p] @@ -370,7 +370,7 @@ class _OpenSSL: except: # The above is not compatible with all versions of OSX. self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 - + self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_int, @@ -470,11 +470,11 @@ class _OpenSSL: OpenSSL random function """ buffer = self.malloc(0, size) - # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is + # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is # evidently possible that it returned an error and not-actually-random data. However, in - # tests on various operating systems, while generating hundreds of gigabytes of random + # tests on various operating systems, while generating hundreds of gigabytes of random # strings of various sizes I could not get an error to occur. Also Bitcoin doesn't check - # the return value of RAND_bytes either. + # the return value of RAND_bytes either. # Fixed in Bitmessage version 0.4.2 (in source code on 2013-10-13) while self.RAND_bytes(buffer, size) != 1: import time @@ -498,7 +498,7 @@ def loadOpenSSL(): global OpenSSL from os import path, environ from ctypes.util import find_library - + libdir = [] if getattr(sys,'frozen', None): if 'darwin' in sys.platform: From 1cfe8748937b3d3a1d6fe9da354a835351c19e27 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 23:37:43 +0200 Subject: [PATCH 044/483] Use find_library first to locate libeay32 --- src/lib/pyelliptic/openssl.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py index 7bc88fa4..bc4fe6a6 100644 --- a/src/lib/pyelliptic/openssl.py +++ b/src/lib/pyelliptic/openssl.py @@ -500,6 +500,12 @@ def loadOpenSSL(): from ctypes.util import find_library libdir = [] + + if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: + libdir.append(find_library('ssl')) + elif 'win32' in sys.platform or 'win64' in sys.platform: + libdir.append(find_library('libeay32')) + if getattr(sys,'frozen', None): if 'darwin' in sys.platform: libdir.extend([ @@ -536,10 +542,6 @@ def loadOpenSSL(): libdir.append('libssl.so') libdir.append('libcrypto.so.1.0.0') libdir.append('libssl.so.1.0.0') - if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: - libdir.append(find_library('ssl')) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append(find_library('libeay32')) for library in libdir: try: OpenSSL = _OpenSSL(library) From 30865c9d1cd6a9fed00879b27f0ab9aa2fabd1d7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 8 Aug 2019 23:37:49 +0200 Subject: [PATCH 045/483] Rev4169 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index f9d5e62b..67a44c44 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4167 + self.rev = 4169 self.argv = argv self.action = None self.pending_changes = {} From 0bbeede9751b74f593390ca0379d8133a0c2119f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 9 Aug 2019 13:17:48 +0200 Subject: [PATCH 046/483] Don't try to display bigfile limit settings if no bigfile plugin enabled --- plugins/Sidebar/SidebarPlugin.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index 46219d8a..f1a5e8e8 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -321,14 +321,15 @@ class UiWebsocketPlugin(object):
""")) - autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit)) - body.append(_(""" -
- - MB - {_[Set]} -
- """)) + if hasattr(config, "autodownload_bigfile_size_limit"): + autodownload_bigfile_size_limit = int(site.settings.get("autodownload_bigfile_size_limit", config.autodownload_bigfile_size_limit)) + body.append(_(""" +
+ + MB + {_[Set]} +
+ """)) body.append("") def sidebarRenderBadFiles(self, body, site): From bd5c2b1daa0e46919e92445d4094df6edb853cd4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 9 Aug 2019 13:18:40 +0200 Subject: [PATCH 047/483] Also try to load OpenSSL dll from Python/DDLs directory --- src/util/OpensslFindPatch.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index 81f84c63..f5b88acc 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -10,18 +10,23 @@ find_library_original = ctypes.util.find_library def getOpensslPath(): if sys.platform.startswith("win"): - lib_path = os.path.join(os.getcwd(), "tools/openssl/libeay32.dll") + lib_paths = [ + os.path.join(os.getcwd(), "tools/openssl/libeay32.dll"), + os.path.join(os.path.dirname(sys.executable), "DLLs/libcrypto-1_1-x64.dll"), + os.path.join(os.path.dirname(sys.executable), "DLLs/libcrypto-1_1.dll") + ] elif sys.platform == "cygwin": - lib_path = "/bin/cygcrypto-1.0.0.dll" + lib_paths = ["/bin/cygcrypto-1.0.0.dll"] elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX - lib_path = "../lib/libcrypto.so" + lib_paths = ["../lib/libcrypto.so"] elif os.path.isfile("/opt/lib/libcrypto.so.1.0.0"): # For optware and entware - lib_path = "/opt/lib/libcrypto.so.1.0.0" + lib_paths = ["/opt/lib/libcrypto.so.1.0.0"] else: - lib_path = "/usr/local/ssl/lib/libcrypto.so" + lib_paths = ["/usr/local/ssl/lib/libcrypto.so"] - if os.path.isfile(lib_path): - return lib_path + for lib_path in lib_paths: + if os.path.isfile(lib_path): + return lib_path if "ANDROID_APP_PATH" in os.environ: try: From e74576052029eeceff270da1f3af70d21e7459e2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 9 Aug 2019 13:18:57 +0200 Subject: [PATCH 048/483] Rev4172 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 67a44c44..2fd8b3f8 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4169 + self.rev = 4172 self.argv = argv self.action = None self.pending_changes = {} From 3f7e22497dc86416e140d29fab61c195a78c0fec Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sun, 11 Aug 2019 12:18:55 +0300 Subject: [PATCH 049/483] Fix preferring CLI argument over zeronet.conf Fix using open_browser from CLI arguments in case there are several `--open_browser` arguments, which often happens after restarts. --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 2fd8b3f8..50527fb1 100644 --- a/src/Config.py +++ b/src/Config.py @@ -420,7 +420,7 @@ class Config(object): key = section + "_" + key if key == "open_browser": # Prefer config file value over cli argument - if "--%s" % key in argv: + while "--%s" % key in argv: pos = argv.index("--open_browser") del argv[pos:pos + 2] From d610f94e7d434aaf0eac7db4d5d4501b918bccf7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 12 Aug 2019 17:56:06 +0200 Subject: [PATCH 050/483] Display TLS 1.3 support on /Stats page --- plugins/Stats/StatsPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index ad46ef7f..6e4bce16 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -76,7 +76,7 @@ class UiRequestPlugin(object): yield "Port: %s | " % main.file_server.port yield "IP Network: %s | " % main.file_server.supported_ip_types yield "Opened: %s | " % main.file_server.port_opened - yield "Crypt: %s | " % CryptConnection.manager.crypt_supported + yield "Crypt: %s, TLSv1.3: %s | " % (CryptConnection.manager.crypt_supported, CryptConnection.ssl.HAS_TLSv1_3) yield "In: %.2fMB, Out: %.2fMB | " % ( float(main.file_server.bytes_recv) / 1024 / 1024, float(main.file_server.bytes_sent) / 1024 / 1024 From 7b9b48e62de8cd3de8c9368b16ad81185b5233e9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 12 Aug 2019 17:58:23 +0200 Subject: [PATCH 051/483] Rev4175, Console and file logging disable support --- src/Config.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/Config.py b/src/Config.py index 2fd8b3f8..da4b9cea 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4172 + self.rev = 4175 self.argv = argv self.action = None self.pending_changes = {} @@ -204,7 +204,7 @@ class Config(object): # Config parameters self.parser.add_argument('--verbose', help='More detailed logging', action='store_true') self.parser.add_argument('--debug', help='Debug mode', action='store_true') - self.parser.add_argument('--silent', help='Disable logging to terminal output', action='store_true') + self.parser.add_argument('--silent', help='Only log errors to terminal output', action='store_true') self.parser.add_argument('--debug_socket', help='Debug socket connections', action='store_true') self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true') @@ -212,8 +212,10 @@ class Config(object): self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path") self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path") + self.parser.add_argument('--console_log_level', help='Level of logging to console', default="default", choices=["default", "DEBUG", "INFO", "ERROR", "off"]) + self.parser.add_argument('--log_dir', help='Path of logging directory', default=log_dir, metavar="path") - self.parser.add_argument('--log_level', help='Level of logging to file', default="DEBUG", choices=["DEBUG", "INFO", "ERROR"]) + self.parser.add_argument('--log_level', help='Level of logging to file', default="DEBUG", choices=["DEBUG", "INFO", "ERROR", "off"]) self.parser.add_argument('--log_rotate', help='Log rotate interval', default="daily", choices=["hourly", "daily", "weekly", "off"]) self.parser.add_argument('--log_rotate_backup_count', help='Log rotate backup count', default=5, type=int) @@ -540,12 +542,15 @@ class Config(object): else: format = '%(name)s %(message)s' - if self.silent: - level = logging.ERROR - elif self.debug: - level = logging.DEBUG + if self.console_log_level == "default": + if self.silent: + level = logging.ERROR + elif self.debug: + level = logging.DEBUG + else: + level = logging.INFO else: - level = logging.INFO + level = logging.getLevelName(self.console_log_level) console_logger = logging.StreamHandler() console_logger.setFormatter(logging.Formatter(format, "%H:%M:%S")) @@ -574,7 +579,13 @@ class Config(object): logging.getLogger('').setLevel(logging.getLevelName(self.log_level)) logging.getLogger('').addHandler(file_logger) - def initLogging(self, console_logging=True, file_logging=True): + def initLogging(self, console_logging=None, file_logging=None): + if console_logging == None: + console_logging = self.console_log_level != "off" + + if file_logging == None: + file_logging = self.log_level != "off" + # Create necessary files and dirs if not os.path.isdir(self.log_dir): os.mkdir(self.log_dir) From 7801937f74934553ab92d3014c73ef33c93feea4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 13 Aug 2019 21:07:26 +0200 Subject: [PATCH 052/483] Rev4176, Fix update of plugins --- src/Config.py | 2 +- update.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index 7771f6e2..c8182591 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4175 + self.rev = 4176 self.argv = argv self.action = None self.pending_changes = {} diff --git a/update.py b/update.py index 9b1c634c..0dfff297 100644 --- a/update.py +++ b/update.py @@ -62,7 +62,7 @@ def update(): continue # Keep plugin disabled/enabled status - match = re.match("plugins/([^/]+)", dest_path) + match = re.match(re.escape(source_path) + "/plugins/([^/]+)", dest_path) if match: plugin_name = match.group(1).replace("disabled-", "") if plugin_name in plugins_enabled: # Plugin was enabled @@ -112,4 +112,4 @@ def update(): if __name__ == "__main__": sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src")) # Imports relative to src - update() \ No newline at end of file + update() From 2bdd07360854bb7920c52efc28913211e4d47ab1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Aug 2019 03:05:29 +0200 Subject: [PATCH 053/483] Move resolveDomain to SiteManager for easier resolver plugins --- plugins/Zeroname/SiteManagerPlugin.py | 45 +++++---------------------- src/Site/SiteManager.py | 17 +++++++++- 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/plugins/Zeroname/SiteManagerPlugin.py b/plugins/Zeroname/SiteManagerPlugin.py index 40088f12..f03b7710 100644 --- a/plugins/Zeroname/SiteManagerPlugin.py +++ b/plugins/Zeroname/SiteManagerPlugin.py @@ -21,21 +21,13 @@ class SiteManagerPlugin(object): if not self.get(config.bit_resolver): self.need(config.bit_resolver) # Need ZeroName site - # Checks if it's a valid address - def isAddress(self, address): - return self.isBitDomain(address) or super(SiteManagerPlugin, self).isAddress(address) - - # Return: True if the address is domain - def isDomain(self, address): - return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address) - # Return: True if the address is .bit domain def isBitDomain(self, address): return re.match(r"(.*?)([A-Za-z0-9_-]+\.bit)$", address) # Resolve domain # Return: The address or None - def resolveDomain(self, domain): + def resolveBitDomain(self, domain): domain = domain.lower() if not self.site_zeroname: self.site_zeroname = self.need(config.bit_resolver) @@ -52,33 +44,10 @@ class SiteManagerPlugin(object): self.db_domains_modified = site_zeroname_modified return self.db_domains.get(domain) - # Return or create site and start download site files - # Return: Site or None if dns resolve failed - def need(self, address, *args, **kwargs): - if self.isBitDomain(address): # Its looks like a domain - address_resolved = self.resolveDomain(address) - if address_resolved: - address = address_resolved - else: - return None + # Turn domain into address + def resolveDomain(self, domain): + return self.resolveBitDomain(domain) or super(SiteManagerPlugin, self).resolveDomain(domain) - return super(SiteManagerPlugin, self).need(address, *args, **kwargs) - - # Return: Site object or None if not found - def get(self, address): - if not self.loaded: # Not loaded yet - self.load() - if self.isBitDomain(address): # Its looks like a domain - address_resolved = self.resolveDomain(address) - if address_resolved: # Domain found - site = self.sites.get(address_resolved) - if site: - site_domain = site.settings.get("domain") - if site_domain != address: - site.settings["domain"] = address - else: # Domain not found - site = self.sites.get(address) - - else: # Access by site address - site = super(SiteManagerPlugin, self).get(address) - return site + # Return: True if the address is domain + def isDomain(self, address): + return self.isBitDomain(address) or super(SiteManagerPlugin, self).isDomain(address) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index bb9fe308..8a880c9d 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -135,15 +135,30 @@ class SiteManager(object): def isDomain(self, address): return False + def resolveDomain(self, domain): + return False + # Return: Site object or None if not found def get(self, address): + if self.isDomain(address): + address_resolved = self.resolveDomain(address) + if address_resolved: + address = address_resolved + if not self.loaded: # Not loaded yet self.log.debug("Loading site: %s)..." % address) self.load() - return self.sites.get(address) + site = self.sites.get(address) + + return site # Return or create site and start download site files def need(self, address, all_file=True, settings=None): + if self.isDomain(address): + address_resolved = self.resolveDomain(address) + if address_resolved: + address = address_resolved + from .Site import Site site = self.get(address) if not site: # Site not exist yet From d93e89899b0edc41c6861078352fb0f2aaeb5e57 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Aug 2019 03:05:46 +0200 Subject: [PATCH 054/483] Fix tracker proxy PySocks import --- src/Connection/Connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 8122ec08..78de2ea4 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -129,7 +129,7 @@ class Connection(object): if config.trackers_proxy == "tor": self.sock = self.server.tor_manager.createSocket(self.ip, self.port) else: - from lib.PySocks import socks + import socks self.sock = socks.socksocket() proxy_ip, proxy_port = config.trackers_proxy.split(":") self.sock.set_proxy(socks.PROXY_TYPE_SOCKS5, proxy_ip, int(proxy_port)) From 92358bafc07a1adc57e8620a01d8c5b74257e38d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Aug 2019 03:06:13 +0200 Subject: [PATCH 055/483] Wider max notification width to allow blacklist button in same line --- src/Ui/media/Wrapper.css | 12 ++++++++---- src/Ui/media/all.css | 14 +++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index ff613e87..203ff489 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -8,7 +8,10 @@ a { color: black } #inner-iframe { width: 100%; height: 100%; position: absolute; border: 0; } /*; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out*/ #inner-iframe.back { transform: scale(0.95) translate(-300px, 0); opacity: 0.4 } -.button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; border-radius: 2px; text-decoration: none; transition: all 0.5s; background-position: left center; } +.button { + padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; + border-radius: 2px; text-decoration: none; transition: all 0.5s; background-position: left center; white-space: nowrap; +} .button:hover { background-color: #FFF400; border-bottom: 2px solid #4D4D4C; transition: none } .button:active { position: relative; top: 1px } .button:focus { outline: none } @@ -44,8 +47,9 @@ a { color: black } .notifications { position: absolute; top: 0; right: 80px; display: inline-block; z-index: 999; white-space: nowrap } .notification { - position: relative; float: right; clear: both; margin: 10px; box-sizing: border-box; overflow: hidden; backface-visibility: hidden; perspective: 1000px; padding-bottom: 5px; - color: #4F4F4F; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ + position: relative; float: right; clear: both; margin: 10px; box-sizing: border-box; overflow: hidden; backface-visibility: hidden; + perspective: 1000px; padding-bottom: 5px; color: #4F4F4F; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; + font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ } .notification-icon { display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 2; @@ -58,7 +62,7 @@ a { color: black } .notification .message-outer { display: table-row } .notification .buttons { display: table-cell; vertical-align: top; padding-top: 9px; } .notification.long .body { padding-top: 10px; padding-bottom: 10px } -.notification .message { display: table-cell; vertical-align: middle; max-width: 400px; white-space: normal; } +.notification .message { display: table-cell; vertical-align: middle; max-width: 500px; white-space: normal; } .notification.visible { max-width: 350px } diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index c1b25cdf..5ec1bc73 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -1,5 +1,5 @@ -/* ---- src/Ui/media/Wrapper.css ---- */ +/* ---- Wrapper.css ---- */ body { margin: 0; padding: 0; height: 100%; background-color: #D2CECD; overflow: hidden } @@ -12,7 +12,10 @@ a { color: black } #inner-iframe { width: 100%; height: 100%; position: absolute; border: 0; } /*; transition: all 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.8s ease-in-out*/ #inner-iframe.back { -webkit-transform: scale(0.95) translate(-300px, 0); -moz-transform: scale(0.95) translate(-300px, 0); -o-transform: scale(0.95) translate(-300px, 0); -ms-transform: scale(0.95) translate(-300px, 0); transform: scale(0.95) translate(-300px, 0) ; opacity: 0.4 } -.button { padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; text-decoration: none; -webkit-transition: all 0.5s; -moz-transition: all 0.5s; -o-transition: all 0.5s; -ms-transition: all 0.5s; transition: all 0.5s ; background-position: left center; } +.button { + padding: 5px 10px; margin-left: 10px; background-color: #FFF85F; border-bottom: 2px solid #CDBD1E; + -webkit-border-radius: 2px; -moz-border-radius: 2px; -o-border-radius: 2px; -ms-border-radius: 2px; border-radius: 2px ; text-decoration: none; -webkit-transition: all 0.5s; -moz-transition: all 0.5s; -o-transition: all 0.5s; -ms-transition: all 0.5s; transition: all 0.5s ; background-position: left center; white-space: nowrap; +} .button:hover { background-color: #FFF400; border-bottom: 2px solid #4D4D4C; -webkit-transition: none ; -moz-transition: none ; -o-transition: none ; -ms-transition: none ; transition: none } .button:active { position: relative; top: 1px } .button:focus { outline: none } @@ -48,8 +51,9 @@ a { color: black } .notifications { position: absolute; top: 0; right: 80px; display: inline-block; z-index: 999; white-space: nowrap } .notification { - position: relative; float: right; clear: both; margin: 10px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; overflow: hidden; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; -webkit-perspective: 1000px; -moz-perspective: 1000px; -o-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px ; padding-bottom: 5px; - color: #4F4F4F; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ + position: relative; float: right; clear: both; margin: 10px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; overflow: hidden; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; + -webkit-perspective: 1000px; -moz-perspective: 1000px; -o-perspective: 1000px; -ms-perspective: 1000px; perspective: 1000px ; padding-bottom: 5px; color: #4F4F4F; font-family: 'Lucida Grande', 'Segoe UI', Helvetica, Arial, sans-serif; + font-size: 14px; line-height: 20px; /*border: 1px solid rgba(210, 206, 205, 0.2)*/ } .notification-icon { display: block; width: 50px; height: 50px; position: absolute; float: left; z-index: 2; @@ -62,7 +66,7 @@ a { color: black } .notification .message-outer { display: table-row } .notification .buttons { display: table-cell; vertical-align: top; padding-top: 9px; } .notification.long .body { padding-top: 10px; padding-bottom: 10px } -.notification .message { display: table-cell; vertical-align: middle; max-width: 400px; white-space: normal; } +.notification .message { display: table-cell; vertical-align: middle; max-width: 500px; white-space: normal; } .notification.visible { max-width: 350px } From 8f491fe6e120c163cbe211bc13bc033faea32b11 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Aug 2019 03:08:40 +0200 Subject: [PATCH 056/483] Use SSLContext for connection encryption, add fake SNI, ALPN --- src/Crypt/CryptConnection.py | 56 +++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 9d705671..9768a520 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -30,6 +30,40 @@ class CryptConnectionManager: self.key_pem = config.data_dir + "/key-rsa.pem" self.log = logging.getLogger("CryptConnectionManager") + self.log.debug("Version: %s" % ssl.OPENSSL_VERSION) + + self.fakedomains = [ + "yahoo.com", "amazon.com", "live.com", "microsoft.com", "mail.ru", "csdn.net", "bing.com", + "amazon.co.jp", "office.com", "imdb.com", "msn.com", "samsung.com", "huawei.com", "ztedevices.com", + "godaddy.com", "w3.org", "gravatar.com", "creativecommons.org", "hatena.ne.jp", + "adobe.com", "opera.com", "apache.org", "rambler.ru", "one.com", "nationalgeographic.com", + "networksolutions.com", "php.net", "python.org", "phoca.cz", "debian.org", "ubuntu.com", + "nazwa.pl", "symantec.com" + ] + + def createSslContexts(self): + ciphers = "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:AES128-SHA256:AES256-SHA:" + ciphers += "!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK" + + if hasattr(ssl, "PROTOCOL_TLS"): + protocol = ssl.PROTOCOL_TLS + else: + protocol = ssl.PROTOCOL_TLSv1_2 + self.context_client = ssl.SSLContext(protocol) + self.context_client.check_hostname = False + self.context_client.verify_mode = ssl.CERT_NONE + + self.context_server = ssl.SSLContext(protocol) + self.context_server.load_cert_chain(self.cert_pem, self.key_pem) + + for ctx in (self.context_client, self.context_server): + ctx.set_ciphers(ciphers) + ctx.options |= ssl.OP_NO_COMPRESSION + try: + ctx.set_alpn_protocols(["h2", "http/1.1"]) + ctx.set_npn_protocols(["h2", "http/1.1"]) + except Exception: + pass # Select crypt that supported by both sides # Return: Name of the crypto @@ -43,15 +77,10 @@ class CryptConnectionManager: # Return: wrapped socket def wrapSocket(self, sock, crypt, server=False, cert_pin=None): if crypt == "tls-rsa": - ciphers = "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:AES128-SHA256:AES256-SHA:" - ciphers += "!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK" if server: - sock_wrapped = ssl.wrap_socket( - sock, server_side=server, keyfile=self.key_pem, - certfile=self.cert_pem, ciphers=ciphers - ) + sock_wrapped = self.context_server.wrap_socket(sock) else: - sock_wrapped = ssl.wrap_socket(sock, ciphers=ciphers) + sock_wrapped = self.context_client.wrap_socket(sock, server_hostname=random.choice(self.fakedomains)) if cert_pin: cert_hash = hashlib.sha256(sock_wrapped.getpeercert(True)).hexdigest() if cert_hash != cert_pin: @@ -85,17 +114,10 @@ class CryptConnectionManager: "/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 High Assurance Server CA", "/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA" ] - fakedomains = [ - "yahoo.com", "amazon.com", "live.com", "microsoft.com", "mail.ru", "csdn.net", "bing.com", - "amazon.co.jp", "office.com", "imdb.com", "msn.com", "samsung.com", "huawei.com", "ztedevices.com", - "godaddy.com", "w3.org", "gravatar.com", "creativecommons.org", "hatena.ne.jp", - "adobe.com", "opera.com", "apache.org", "rambler.ru", "one.com", "nationalgeographic.com", - "networksolutions.com", "php.net", "python.org", "phoca.cz", "debian.org", "ubuntu.com", - "nazwa.pl", "symantec.com" - ] - self.openssl_env['CN'] = random.choice(fakedomains) + self.openssl_env['CN'] = random.choice(self.fakedomains) if os.path.isfile(self.cert_pem) and os.path.isfile(self.key_pem): + self.createSslContexts() return True # Files already exits import subprocess @@ -162,8 +184,10 @@ class CryptConnectionManager: self.log.debug("Running: %s\n%s" % (cmd, back)) if os.path.isfile(self.cert_pem) and os.path.isfile(self.key_pem): + self.createSslContexts() return True else: self.log.error("RSA ECC SSL cert generation failed, cert or key files not exist.") + manager = CryptConnectionManager() From 429043f60ceed2e32be5f85da4db3e71327d8c39 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Aug 2019 03:09:53 +0200 Subject: [PATCH 057/483] CLI peerPing command display connection encryption info only once --- src/main.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/main.py b/src/main.py index dfc32cc9..ed813ade 100644 --- a/src/main.py +++ b/src/main.py @@ -447,30 +447,24 @@ class Actions(object): if not peer.connection: print("Error: Can't connect to peer (connection error: %s)" % peer.connection_error) return False + if "shared_ciphers" in dir(peer.connection.sock): + print("Shared ciphers:", peer.connection.sock.shared_ciphers()) + if "cipher" in dir(peer.connection.sock): + print("Cipher:", peer.connection.sock.cipher()[0]) + if "version" in dir(peer.connection.sock): + print("TLS version:", peer.connection.sock.version()) print("Connection time: %.3fs (connection error: %s)" % (time.time() - s, peer.connection_error)) for i in range(5): ping_delay = peer.ping() - if "cipher" in dir(peer.connection.sock): - cipher = peer.connection.sock.cipher()[0] - tls_version = peer.connection.sock.version() - else: - cipher = peer.connection.crypt - tls_version = None - print("Response time: %.3fs (crypt: %s %s %s)" % (ping_delay, peer.connection.crypt, cipher, tls_version)) + print("Response time: %.3fs" % ping_delay) time.sleep(1) peer.remove() print("Reconnect test...") peer = Peer(peer_ip, peer_port) for i in range(5): ping_delay = peer.ping() - if "cipher" in dir(peer.connection.sock): - cipher = peer.connection.sock.cipher()[0] - tls_version = peer.connection.sock.version() - else: - cipher = peer.connection.crypt - tls_version = None - print("Response time: %.3fs (crypt: %s %s %s)" % (ping_delay, peer.connection.crypt, cipher, tls_version)) + print("Response time: %.3fs" % ping_delay) time.sleep(1) def peerGetFile(self, peer_ip, peer_port, site, filename, benchmark=False): From 6a245a202ce3075126ce67180701e69136483164 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Aug 2019 03:19:05 +0200 Subject: [PATCH 058/483] Fix server connections encryption --- src/Crypt/CryptConnection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 9768a520..7fc64152 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -78,7 +78,7 @@ class CryptConnectionManager: def wrapSocket(self, sock, crypt, server=False, cert_pin=None): if crypt == "tls-rsa": if server: - sock_wrapped = self.context_server.wrap_socket(sock) + sock_wrapped = self.context_server.wrap_socket(sock, server_side=True) else: sock_wrapped = self.context_client.wrap_socket(sock, server_hostname=random.choice(self.fakedomains)) if cert_pin: From d63a4b39122e7613b215799b5101293cf7aabf7d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 15 Aug 2019 03:19:23 +0200 Subject: [PATCH 059/483] Rev4181 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index c8182591..64b70e9f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4176 + self.rev = 4181 self.argv = argv self.action = None self.pending_changes = {} From fcb3ac3917e0ae0e5ea75d6d6ecb7b323484f3bc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 17 Aug 2019 20:33:43 +0200 Subject: [PATCH 060/483] Only change default proxy to tor in tor always mode --- src/Tor/TorManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index fafe51eb..52efff0a 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -72,7 +72,8 @@ class TorManager(object): # Change to self-bundled Tor ports self.port = 49051 self.proxy_port = 49050 - socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", self.proxy_port) + if config.tor == "always": + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", self.proxy_port) self.enabled = True if not self.connect(): self.startTor() From 8537939d261f810024890e0e772194bbfffddefc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 17 Aug 2019 20:34:04 +0200 Subject: [PATCH 061/483] Disable UDP in proxy mode --- src/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.py b/src/main.py index ed813ade..7ffbedd5 100644 --- a/src/main.py +++ b/src/main.py @@ -83,6 +83,7 @@ if config.proxy: logging.info("Patching sockets to socks proxy: %s" % config.proxy) if config.fileserver_ip == "*": config.fileserver_ip = '127.0.0.1' # Do not accept connections anywhere but localhost + config.disable_udp = True # UDP not supported currently with proxy SocksProxy.monkeyPatch(*config.proxy.split(":")) elif config.tor == "always": from util import SocksProxy From 1d5bde01cc85efdea4c37d238885d04de0c42362 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 17 Aug 2019 20:34:21 +0200 Subject: [PATCH 062/483] Deny plugin add request in multiuser mode --- plugins/disabled-Multiuser/MultiuserPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 96354245..fc88922a 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -107,7 +107,7 @@ class UiWebsocketPlugin(object): "mergerSiteDelete", "siteSetLimit", "siteSetAutodownloadBigfileLimit", "optionalLimitSet", "optionalHelp", "optionalHelpRemove", "optionalHelpAll", "optionalFilePin", "optionalFileUnpin", "optionalFileDelete", "muteAdd", "muteRemove", "siteblockAdd", "siteblockRemove", "filterIncludeAdd", "filterIncludeRemove", - "pluginConfigSet", "pluginAdd", "pluginRemove", "pluginUpdate" + "pluginConfigSet", "pluginAdd", "pluginRemove", "pluginUpdate", "pluginAddRequest" ) if config.multiuser_no_new_sites: self.multiuser_denied_cmds += ("mergerSiteAdd", ) From 2a887870ff34f8411fd3e5f894c96cd76f35eeb5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 17 Aug 2019 20:35:00 +0200 Subject: [PATCH 063/483] Rev4185 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 64b70e9f..4346f1bd 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4181 + self.rev = 4185 self.argv = argv self.action = None self.pending_changes = {} From 7d1ca3862d511da6eb973d55834c2153c1f71cc3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 18 Aug 2019 03:02:30 +0200 Subject: [PATCH 064/483] Make missing IPv6 a warning not an error --- src/File/FileServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 5f1d9b47..91b2b103 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -108,7 +108,7 @@ class FileServer(ConnectionServer): self.log.debug("IPv6 supported on IP %s" % local_ipv6) return True except socket.error as err: - self.log.error("IPv6 not supported: %s" % err) + self.log.warning("IPv6 not supported: %s" % err) return False except Exception as err: self.log.error("IPv6 check error: %s" % err) From b871849df45cc5da9eff0ca780c0c8afe0e8a0cd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 18 Aug 2019 03:03:02 +0200 Subject: [PATCH 065/483] Add origin validation to websocket connections --- src/Ui/UiRequest.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 48599fda..c68f48a0 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -712,9 +712,19 @@ class UiRequest(object): # On websocket connection def actionWebsocket(self): ws = self.env.get("wsgi.websocket") + if ws: - wrapper_key = self.get["wrapper_key"] + # Allow only same-origin websocket requests + origin = self.env.get("HTTP_ORIGIN") + host = self.env.get("HTTP_HOST") + if origin and host: + origin_host = origin.split("://", 1)[-1] + if host != origin_host: + ws.send(json.dumps({"error": "Invalid origin: %s" % origin})) + return self.error403("Invalid origin: %s" % origin) + # Find site by wrapper_key + wrapper_key = self.get["wrapper_key"] site = None for site_check in list(self.server.sites.values()): if site_check.settings["wrapper_key"] == wrapper_key: From 18dc359cfca8e8050a0757ec100f24c407d4c9e9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 18 Aug 2019 03:03:22 +0200 Subject: [PATCH 066/483] Rev4187 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 4346f1bd..5292cebe 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4185 + self.rev = 4187 self.argv = argv self.action = None self.pending_changes = {} From 1ed40b3b8292191d8fafd8767f9ed0742b62d6ba Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 19 Aug 2019 07:09:32 +0000 Subject: [PATCH 067/483] Allow files with `..` as a name substring --- src/Content/ContentManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 12a9b638..7a1a8447 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -595,7 +595,7 @@ class ContentManager(object): return back def isValidRelativePath(self, relative_path): - if ".." in relative_path: + if ".." in relative_path.replace("\\", "/").split("/"): return False elif len(relative_path) > 255: return False From 155d8d4dfdf8f5c1bab3c5cc0f4e1be8dd049bd1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 19 Aug 2019 13:42:49 +0200 Subject: [PATCH 068/483] Rev4188, Allow only white listed values for open_browser --- src/Config.py | 2 +- src/Ui/UiWebsocket.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 5292cebe..0bded2db 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4187 + self.rev = 4188 self.argv = argv self.action = None self.pending_changes = {} diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 77f4ec50..181a306d 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1137,9 +1137,14 @@ class UiWebsocket(object): def actionConfigSet(self, to, key, value): import main if key not in config.keys_api_change_allowed: - self.response(to, {"error": "Forbidden you cannot set this config key"}) + self.response(to, {"error": "Forbidden: You cannot set this config key"}) return + if key == "open_browser": + if value not in ["default_browser", "False"]: + self.response(to, {"error": "Forbidden: Invalid value"}) + return + # Remove empty lines from lists if type(value) is list: value = [line for line in value if line] From 01ff89315bfa3a53237113fc1cb161ff9bf5f352 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 19 Aug 2019 15:30:31 +0000 Subject: [PATCH 069/483] Add GitLab CI/CD support (#2163) * Use GitLab CI/CD * Force colored tests * Get rid of an error * Mark tests as slow * Disable codecov & coveralls * Python 3.5-3.8 * Add Python 3.4 * Support both OpenSSL 1.1.0 and 1.1.1+ * Test both OpenSSL 1.1.0 and 1.1.1+ * Fix OpenSSL 1.1.1 * Fix Python 3.4 build --- .gitignore | 1 + .gitlab-ci.yml | 48 +++++++++++++++++++ plugins/disabled-Bootstrapper/Test/pytest.ini | 1 + src/Crypt/CryptBitcoin.py | 9 +++- src/Test/conftest.py | 15 +++--- src/Test/pytest.ini | 1 + 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitignore b/.gitignore index b3795821..c6cbd0a0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ .* !/.gitignore !/.travis.yml +!/.gitlab-ci.yml # Temporary files *.bak diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..b62c7b0c --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,48 @@ +stages: + - test + +.test_template: &test_template + stage: test + before_script: + - pip install --upgrade pip wheel + # Selenium and requests can't be installed without a requests hint on Python 3.4 + - pip install --upgrade requests>=2.22.0 + - pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium + - pip install --upgrade -r requirements.txt + script: + - pip list + - openssl version -a + - python -m pytest -x plugins/CryptMessage/Test --color=yes + - python -m pytest -x plugins/Bigfile/Test --color=yes + - python -m pytest -x plugins/AnnounceLocal/Test --color=yes + - python -m pytest -x plugins/OptionalManager/Test --color=yes + - python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini --color=yes + - mv plugins/disabled-Multiuser plugins/Multiuser + - python -m pytest -x plugins/Multiuser/Test --color=yes + - mv plugins/disabled-Bootstrapper plugins/Bootstrapper + - python -m pytest -x plugins/Bootstrapper/Test --color=yes + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ + +test:py3.4: + image: python:3.4.3 + <<: *test_template + +test:py3.5: + image: python:3.5.7 + <<: *test_template + +test:py3.6: + image: python:3.6.9 + <<: *test_template + +test:py3.7-openssl1.1.0: + image: python:3.7.0b5 + <<: *test_template + +test:py3.7-openssl1.1.1: + image: python:3.7.4 + <<: *test_template + +test:py3.8: + image: python:3.8.0b3 + <<: *test_template \ No newline at end of file diff --git a/plugins/disabled-Bootstrapper/Test/pytest.ini b/plugins/disabled-Bootstrapper/Test/pytest.ini index d09210d1..8ee21268 100644 --- a/plugins/disabled-Bootstrapper/Test/pytest.ini +++ b/plugins/disabled-Bootstrapper/Test/pytest.ini @@ -2,4 +2,5 @@ python_files = Test*.py addopts = -rsxX -v --durations=6 markers = + slow: mark a tests as slow. webtest: mark a test as a webtest. \ No newline at end of file diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index be0c1ba2..0d902726 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -26,9 +26,16 @@ def loadLib(lib_name): import bitcoin.core.key import bitcoin.wallet + try: + # OpenSSL 1.1.0 + ssl_version = bitcoin.core.key._ssl.SSLeay() + except AttributeError: + # OpenSSL 1.1.1+ + ssl_version = bitcoin.core.key._ssl.OpenSSL_version_num() + logging.info( "OpenSSL loaded: %s, version: %.9X in %.3fs" % - (bitcoin.core.key._ssl, bitcoin.core.key._ssl.SSLeay(), time.time() - s) + (bitcoin.core.key._ssl, ssl_version, time.time() - s) ) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 99fdae13..144256dd 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -109,13 +109,14 @@ from Debug import Debug def cleanup(): Db.dbCloseAll() for dir_path in [config.data_dir, config.data_dir + "-temp"]: - for file_name in os.listdir(dir_path): - ext = file_name.rsplit(".", 1)[-1] - if ext not in ["csr", "pem", "srl", "db", "json", "tmp"]: - continue - file_path = dir_path + "/" + file_name - if os.path.isfile(file_path): - os.unlink(file_path) + if os.path.isdir(dir_path): + for file_name in os.listdir(dir_path): + ext = file_name.rsplit(".", 1)[-1] + if ext not in ["csr", "pem", "srl", "db", "json", "tmp"]: + continue + file_path = dir_path + "/" + file_name + if os.path.isfile(file_path): + os.unlink(file_path) atexit.register(cleanup) diff --git a/src/Test/pytest.ini b/src/Test/pytest.ini index d09210d1..8ee21268 100644 --- a/src/Test/pytest.ini +++ b/src/Test/pytest.ini @@ -2,4 +2,5 @@ python_files = Test*.py addopts = -rsxX -v --durations=6 markers = + slow: mark a tests as slow. webtest: mark a test as a webtest. \ No newline at end of file From 61ba9848e5eac6c69eeb9a1b3071b45eecf97fe4 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Tue, 20 Aug 2019 08:16:35 +0000 Subject: [PATCH 070/483] Add --merge_media config option --- src/Config.py | 1 + src/Ui/UiRequest.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 0bded2db..9943053d 100644 --- a/src/Config.py +++ b/src/Config.py @@ -206,6 +206,7 @@ class Config(object): self.parser.add_argument('--debug', help='Debug mode', action='store_true') self.parser.add_argument('--silent', help='Only log errors to terminal output', action='store_true') self.parser.add_argument('--debug_socket', help='Debug socket connections', action='store_true') + self.parser.add_argument('--merge_media', help='Merge all.js and all.css', action='store_true') self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true') diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index c68f48a0..954a424f 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -551,7 +551,7 @@ class UiRequest(object): address = path_parts["address"] file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"]) - if config.debug and file_path.split("/")[-1].startswith("all."): + if (config.debug or config.merge_media) and file_path.split("/")[-1].startswith("all."): # If debugging merge *.css to all.css and *.js to all.js site = self.server.sites.get(address) if site and site.settings["own"]: @@ -607,7 +607,7 @@ class UiRequest(object): # File not in allowed path return self.error403() else: - if config.debug and match.group("inner_path").startswith("all."): + if (config.debug or config.merge_media) and match.group("inner_path").startswith("all."): # If debugging merge *.css to all.css and *.js to all.js from Debug import DebugMedia DebugMedia.merge(file_path) From 24b3651d2e99ca9e78eba06332ce370565167a2e Mon Sep 17 00:00:00 2001 From: Ivanq Date: Tue, 20 Aug 2019 10:42:01 +0000 Subject: [PATCH 071/483] Allow blob: protocol (#2166) * Allow blob: protocol * Fix quotes --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 954a424f..e34e84a5 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -259,7 +259,7 @@ class UiRequest(object): if noscript: headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src 'self'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline';" elif script_nonce and self.isScriptNonceSupported(): - headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self'; style-src 'self' 'unsafe-inline'; connect-src *; frame-src 'self'".format(script_nonce) + headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self' blob:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:".format(script_nonce) if allow_ajax: headers["Access-Control-Allow-Origin"] = "null" From e16611f15ab06216c07baa5e0ebf3214e3ae6b07 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Aug 2019 03:39:16 +0200 Subject: [PATCH 072/483] Allow websocket connection originates from earlier accepted hostnames --- src/Ui/UiRequest.py | 27 +++++++++++++++++++-------- src/Ui/UiServer.py | 3 ++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index e34e84a5..217bbd08 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -378,6 +378,16 @@ class UiRequest(object): else: return "/" + address + def getWsServerUrl(self): + if self.isProxyRequest(): + if self.env["REMOTE_ADDR"] == "127.0.0.1": # Local client, the server address also should be 127.0.0.1 + server_url = "http://127.0.0.1:%s" % self.env["SERVER_PORT"] + else: # Remote client, use SERVER_NAME as server's real address + server_url = "http://%s:%s" % (self.env["SERVER_NAME"], self.env["SERVER_PORT"]) + else: + server_url = "" + return server_url + def processQueryString(self, site, query_string): match = re.search("zeronet_peers=(.*?)(&|$)", query_string) if match: @@ -414,6 +424,9 @@ class UiRequest(object): file_url = "/" + address + "/" + inner_path root_url = "/" + address + "/" + if self.isProxyRequest(): + self.server.allowed_ws_origins.add(self.env["HTTP_HOST"]) + # Wrapper variable inits body_style = "" meta_tags = "" @@ -430,15 +443,12 @@ class UiRequest(object): inner_query_string = "?wrapper_nonce=%s" % wrapper_nonce if self.isProxyRequest(): # Its a remote proxy request - if self.env["REMOTE_ADDR"] == "127.0.0.1": # Local client, the server address also should be 127.0.0.1 - server_url = "http://127.0.0.1:%s" % self.env["SERVER_PORT"] - else: # Remote client, use SERVER_NAME as server's real address - server_url = "http://%s:%s" % (self.env["SERVER_NAME"], self.env["SERVER_PORT"]) homepage = "http://zero/" + config.homepage else: # Use relative path - server_url = "" homepage = "/" + config.homepage + server_url = self.getWsServerUrl() # Real server url for WS connections + user = self.getCurrentUser() if user: theme = user.settings.get("theme", "light") @@ -717,11 +727,12 @@ class UiRequest(object): # Allow only same-origin websocket requests origin = self.env.get("HTTP_ORIGIN") host = self.env.get("HTTP_HOST") - if origin and host: + # Allow only same-origin websocket requests + if origin: origin_host = origin.split("://", 1)[-1] - if host != origin_host: + if origin_host != host and origin_host not in self.server.allowed_ws_origins: ws.send(json.dumps({"error": "Invalid origin: %s" % origin})) - return self.error403("Invalid origin: %s" % origin) + return self.error403("Invalid origin: %s %s" % (origin, self.server.allowed_ws_origins)) # Find site by wrapper_key wrapper_key = self.get["wrapper_key"] diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 9daa90b1..e8cdd545 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -76,6 +76,7 @@ class UiServer: self.allowed_hosts.update(["localhost"]) else: self.allowed_hosts = set([]) + self.allowed_ws_origins = set() self.allow_trans_proxy = config.ui_trans_proxy self.wrapper_nonces = [] @@ -196,4 +197,4 @@ class UiServer: def updateWebsocket(self, **kwargs): for ws in self.websockets: - ws.event("serverChanged", kwargs) \ No newline at end of file + ws.event("serverChanged", kwargs) From 248fc5f015f846a868b8d1c8168842878151e66d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Aug 2019 03:39:50 +0200 Subject: [PATCH 073/483] Use re.sub to replace template variables --- src/Ui/UiRequest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 217bbd08..2a09bd3e 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -294,9 +294,12 @@ class UiRequest(object): # Renders a template def render(self, template_path, *args, **kwargs): template = open(template_path, encoding="utf8").read() - for key, val in list(kwargs.items()): - template = template.replace("{%s}" % key, "%s" % val) - return template.encode("utf8") + def renderReplacer(m): + return "%s" % kwargs.get(m.group(1), "") + + template_rendered = re.sub("{(.*?)}", renderReplacer, template) + + return template_rendered.encode("utf8") # - Actions - From 8a7ae368d8bfef54557724666579797a61bed2e1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Aug 2019 03:40:20 +0200 Subject: [PATCH 074/483] No opened services if we are in tor always mode --- src/Site/SiteAnnouncer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index c06a0c61..0d6fc9b1 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -58,7 +58,7 @@ class SiteAnnouncer(object): def getOpenedServiceTypes(self): back = [] # Type of addresses they can reach me - if config.trackers_proxy == "disable": + if config.trackers_proxy == "disable" and config.tor != "always": for ip_type, opened in list(self.site.connection_server.port_opened.items()): if opened: back.append(ip_type) From ab9fe173a830a4341cd126376f3770f981115e84 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Aug 2019 03:40:29 +0200 Subject: [PATCH 075/483] Don't use trackers proxy in tor always mode --- src/Connection/Connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/Connection.py b/src/Connection/Connection.py index 78de2ea4..22bcf29c 100644 --- a/src/Connection/Connection.py +++ b/src/Connection/Connection.py @@ -125,7 +125,7 @@ class Connection(object): self.sock = self.server.tor_manager.createSocket(self.ip, self.port) elif config.tor == "always" and helper.isPrivateIp(self.ip) and self.ip not in config.ip_local: raise Exception("Can't connect to local IPs in Tor: always mode") - elif config.trackers_proxy != "disable" and self.is_tracker_connection: + elif config.trackers_proxy != "disable" and config.tor != "always" and self.is_tracker_connection: if config.trackers_proxy == "tor": self.sock = self.server.tor_manager.createSocket(self.ip, self.port) else: From d1fb4067e7342b9fcaddb755c65ceb46b10e54db Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Aug 2019 03:40:44 +0200 Subject: [PATCH 076/483] Hide trackers proxy settings if tor always set on /Config page --- plugins/UiConfig/media/js/ConfigStorage.coffee | 4 +++- plugins/UiConfig/media/js/all.js | 14 +++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index 1dc4c8b4..83275bd6 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -123,6 +123,8 @@ class ConfigStorage extends Class {title: "Tor", value: "tor"} {title: "Disable", value: "disable"} ] + isHidden: -> + Page.values["tor"] == "always" section.items.push title: "Custom socks proxy address for trackers" @@ -154,4 +156,4 @@ class ConfigStorage extends Class @items.push(section) return section -window.ConfigStorage = ConfigStorage \ No newline at end of file +window.ConfigStorage = ConfigStorage diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index 4c2f279f..4bf524e0 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -55,7 +55,6 @@ }).call(this); - /* ---- lib/Promise.coffee ---- */ @@ -159,7 +158,6 @@ }).call(this); - /* ---- lib/Prototypes.coffee ---- */ @@ -186,7 +184,6 @@ }).call(this); - /* ---- lib/maquette.js ---- */ @@ -1128,7 +1125,6 @@ }).call(this); - /* ---- utils/Dollar.coffee ---- */ @@ -1141,7 +1137,6 @@ }).call(this); - /* ---- utils/ZeroFrame.coffee ---- */ @@ -1273,7 +1268,6 @@ }).call(this); - /* ---- ConfigStorage.coffee ---- */ @@ -1450,7 +1444,10 @@ title: "Disable", value: "disable" } - ] + ], + isHidden: function() { + return Page.values["tor"] === "always"; + } }); section.items.push({ title: "Custom socks proxy address for trackers", @@ -1707,7 +1704,6 @@ }).call(this); - /* ---- UiConfig.coffee ---- */ @@ -1942,4 +1938,4 @@ window.Page.createProjector(); -}).call(this); +}).call(this); \ No newline at end of file From 6750682e4fa3fc3c6859abf8c1306d42eb00eb46 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 23 Aug 2019 03:42:31 +0200 Subject: [PATCH 077/483] Rev4191 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9943053d..5e9a5e3b 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4188 + self.rev = 4191 self.argv = argv self.action = None self.pending_changes = {} From adffbd1973a24f139ca92bb0353e2380b8185909 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 26 Aug 2019 02:55:01 +0200 Subject: [PATCH 078/483] New function flagging decorator class to keep track permissions --- src/util/Flag.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/util/Flag.py diff --git a/src/util/Flag.py b/src/util/Flag.py new file mode 100644 index 00000000..37cfdfba --- /dev/null +++ b/src/util/Flag.py @@ -0,0 +1,22 @@ +from collections import defaultdict + + +class Flag(object): + def __init__(self): + self.valid_flags = set([ + "admin", # Only allowed to run sites with ADMIN permission + "async_run", # Action will be ran async with gevent.spawn + "no_multiuser" # Action disabled if Multiuser plugin running in open proxy mode + ]) + self.db = defaultdict(set) + + def __getattr__(self, key): + def func(f): + if key not in self.valid_flags: + raise Exception("Invalid flag: %s (valid: %s)" % (key, self.valid_flags)) + self.db[f.__name__].add(key) + return f + return func + + +flag = Flag() From ed7a3b2356b421bef6a4edd6174fca7ae48abe07 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 26 Aug 2019 03:02:30 +0200 Subject: [PATCH 079/483] Get action permissions from flag db --- src/Ui/UiWebsocket.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 181a306d..19fbf6fc 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -18,6 +18,7 @@ from Plugin import PluginManager from Translate import translate as _ from util import helper from util import SafeRe +from util.Flag import flag from Content.ContentManager import VerifyError, SignError @@ -117,9 +118,8 @@ class UiWebsocket(object): # Has permission to run the command def hasCmdPermission(self, cmd): - cmd = cmd[0].lower() + cmd[1:] - - if cmd in self.admin_commands and "ADMIN" not in self.permissions: + flags = flag.db.get(self.getCmdFuncName(cmd), ()) + if "admin" in flags and "ADMIN" not in self.permissions: return False else: return True @@ -205,6 +205,10 @@ class UiWebsocket(object): gevent.spawn(asyncErrorWatcher, func, *args, **kwargs) return wrapper + def getCmdFuncName(self, cmd): + func_name = "action" + cmd[0].upper() + cmd[1:] + return func_name + # Handle incoming messages def handleRequest(self, req): @@ -214,14 +218,14 @@ class UiWebsocket(object): if cmd == "response": # It's a response to a command return self.actionResponse(req["to"], req["result"]) - elif not self.hasCmdPermission(cmd): # Admin commands - return self.response(req["id"], {"error": "You don't have permission to run %s" % cmd}) else: # Normal command - func_name = "action" + cmd[0].upper() + cmd[1:] + func_name = self.getCmdFuncName(cmd) func = getattr(self, func_name, None) if not func: # Unknown command - self.response(req["id"], {"error": "Unknown command: %s" % cmd}) - return + return self.response(req["id"], {"error": "Unknown command: %s" % cmd}) + + if not self.hasCmdPermission(cmd): # Admin commands + return self.response(req["id"], {"error": "You don't have permission to run %s" % cmd}) # Execute in parallel if cmd in self.async_commands: From c414e6caa25cbc5a26eacd9251bec45b6c9af71d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 26 Aug 2019 03:08:57 +0200 Subject: [PATCH 080/483] Support action async call flag --- src/Ui/UiWebsocket.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 19fbf6fc..932e2df5 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -228,7 +228,8 @@ class UiWebsocket(object): return self.response(req["id"], {"error": "You don't have permission to run %s" % cmd}) # Execute in parallel - if cmd in self.async_commands: + func_flags = flag.db.get(self.getCmdFuncName(cmd), ()) + if func_flags and "async_run" in func_flags: func = self.asyncWrapper(func) # Support calling as named, unnamed parameters and raw first argument too From 376fd0d43957a31271bff12e99b0fe8951402b66 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 26 Aug 2019 03:09:48 +0200 Subject: [PATCH 081/483] Use flags instead of permission list --- src/Ui/UiWebsocket.py | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 932e2df5..bf76639a 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -24,13 +24,6 @@ from Content.ContentManager import VerifyError, SignError @PluginManager.acceptPlugins class UiWebsocket(object): - admin_commands = set([ - "sitePause", "siteResume", "siteDelete", "siteList", "siteSetLimit", "siteAdd", "siteListModifiedFiles", "siteSetSettingsValue", - "channelJoinAllsite", "serverUpdate", "serverPortcheck", "serverShutdown", "serverShowdirectory", "serverGetWrapperNonce", - "certSet", "certList", "configSet", "permissionAdd", "permissionRemove", "announcerStats", "userSetGlobalSettings" - ]) - async_commands = set(["fileGet", "fileList", "dirList", "fileNeed", "serverPortcheck", "siteListModifiedFiles"]) - def __init__(self, ws, site, server, user, request): self.ws = ws self.site = site @@ -376,6 +369,7 @@ class UiWebsocket(object): self.response(to, back) # Create a new wrapper nonce that allows to load html file + @flag.admin def actionServerGetWrapperNonce(self, to): wrapper_nonce = self.request.getWrapperNonce() self.response(to, wrapper_nonce) @@ -384,6 +378,7 @@ class UiWebsocket(object): back = self.formatAnnouncerInfo(self.site) self.response(to, back) + @flag.admin def actionAnnouncerStats(self, to): back = {} trackers = self.site.announcer.getTrackers() @@ -646,6 +641,7 @@ class UiWebsocket(object): return self.response(to, rows) # List files in directory + @flag.async_run def actionFileList(self, to, inner_path): try: return list(self.site.storage.walk(inner_path)) @@ -654,6 +650,7 @@ class UiWebsocket(object): return {"error": Debug.formatExceptionMessage(err)} # List directories in a directory + @flag.async_run def actionDirList(self, to, inner_path): try: return list(self.site.storage.list(inner_path)) @@ -679,6 +676,7 @@ class UiWebsocket(object): return self.response(to, rows) # Return file content + @flag.async_run def actionFileGet(self, to, inner_path, required=True, format="text", timeout=300): try: if required or inner_path in self.site.bad_files: @@ -698,6 +696,7 @@ class UiWebsocket(object): body = body.decode() self.response(to, body) + @flag.async_run def actionFileNeed(self, to, inner_path, timeout=300): try: with gevent.Timeout(timeout): @@ -824,6 +823,7 @@ class UiWebsocket(object): # - Admin actions - + @flag.admin def actionPermissionAdd(self, to, permission): if permission not in self.site.settings["permissions"]: self.site.settings["permissions"].append(permission) @@ -831,12 +831,14 @@ class UiWebsocket(object): self.site.updateWebsocket(permission_added=permission) self.response(to, "ok") + @flag.admin def actionPermissionRemove(self, to, permission): self.site.settings["permissions"].remove(permission) self.site.saveSettings() self.site.updateWebsocket(permission_removed=permission) self.response(to, "ok") + @flag.admin def actionPermissionDetails(self, to, permission): if permission == "ADMIN": self.response(to, _["Modify your client's configuration and access all site"] + " " + _["(Dangerous!)"] + "") @@ -848,12 +850,14 @@ class UiWebsocket(object): self.response(to, "") # Set certificate that used for authenticate user for site + @flag.admin def actionCertSet(self, to, domain): self.user.setCert(self.site.address, domain) self.site.updateWebsocket(cert_changed=domain) self.response(to, "ok") # List user's certificates + @flag.admin def actionCertList(self, to): back = [] auth_address = self.user.getAuthAddress(self.site.address) @@ -868,6 +872,7 @@ class UiWebsocket(object): return back # List all site info + @flag.admin def actionSiteList(self, to, connecting_sites=False): ret = [] SiteManager.site_manager.load() # Reload sites @@ -878,6 +883,7 @@ class UiWebsocket(object): self.response(to, ret) # Join to an event channel on all sites + @flag.admin def actionChannelJoinAllsite(self, to, channel): if channel not in self.channels: # Add channel to channels self.channels.append(channel) @@ -905,6 +911,7 @@ class UiWebsocket(object): self.response(to, {"error": "Unknown site: %s" % address}) # Pause site serving + @flag.admin def actionSitePause(self, to, address): site = self.server.sites.get(address) if site: @@ -917,6 +924,7 @@ class UiWebsocket(object): self.response(to, {"error": "Unknown site: %s" % address}) # Resume site serving + @flag.admin def actionSiteResume(self, to, address): site = self.server.sites.get(address) if site: @@ -929,6 +937,8 @@ class UiWebsocket(object): else: self.response(to, {"error": "Unknown site: %s" % address}) + @flag.admin + @flag.no_multiuser def actionSiteDelete(self, to, address): site = self.server.sites.get(address) if site: @@ -965,6 +975,7 @@ class UiWebsocket(object): self.response(to, response) return "ok" + @flag.no_multiuser def actionSiteClone(self, to, address, root_inner_path="", target_address=None, redirect=True): if not SiteManager.site_manager.isAddress(address): self.response(to, {"error": "Not a site: %s" % address}) @@ -991,6 +1002,8 @@ class UiWebsocket(object): lambda res: self.cbSiteClone(to, address, root_inner_path, target_address, redirect) ) + @flag.admin + @flag.no_multiuser def actionSiteSetLimit(self, to, size_limit): self.site.settings["size_limit"] = int(size_limit) self.site.saveSettings() @@ -998,6 +1011,7 @@ class UiWebsocket(object): self.site.updateWebsocket() self.site.download(blind_includes=True) + @flag.admin def actionSiteAdd(self, to, address): site_manager = SiteManager.site_manager if address in site_manager.sites: @@ -1008,6 +1022,8 @@ class UiWebsocket(object): else: return {"error": "Invalid address"} + @flag.admin + @flag.async_run def actionSiteListModifiedFiles(self, to, content_inner_path="content.json"): content = self.site.content_manager.contents[content_inner_path] min_mtime = content.get("modified", 0) @@ -1058,6 +1074,7 @@ class UiWebsocket(object): self.site.settings["cache"]["modified_files"] = modified_files return {"modified_files": modified_files} + @flag.admin def actionSiteSetSettingsValue(self, to, key, value): if key not in ["modified_files_notification"]: return {"error": "Can't change this key"} @@ -1078,11 +1095,14 @@ class UiWebsocket(object): settings = self.user.settings self.response(to, settings) + @flag.admin def actionUserSetGlobalSettings(self, to, settings): self.user.settings = settings self.user.save() self.response(to, "ok") + @flag.admin + @flag.no_multiuser def actionServerUpdate(self, to): def cbServerUpdate(res): self.response(to, res) @@ -1107,12 +1127,17 @@ class UiWebsocket(object): cbServerUpdate ) + @flag.admin + @flag.async_run + @flag.no_multiuser def actionServerPortcheck(self, to): import main file_server = main.file_server file_server.portCheck() self.response(to, file_server.port_opened) + @flag.admin + @flag.no_multiuser def actionServerShutdown(self, to, restart=False): import main if restart: @@ -1120,6 +1145,8 @@ class UiWebsocket(object): main.file_server.stop() main.ui_server.stop() + @flag.admin + @flag.no_multiuser def actionServerShowdirectory(self, to, directory="backup", inner_path=""): if self.request.env["REMOTE_ADDR"] != "127.0.0.1": return self.response(to, {"error": "Only clients from 127.0.0.1 allowed to run this command"}) @@ -1139,6 +1166,8 @@ class UiWebsocket(object): else: return self.response(to, {"error": "Not a directory"}) + @flag.admin + @flag.no_multiuser def actionConfigSet(self, to, key, value): import main if key not in config.keys_api_change_allowed: From 7890771faa995d52d968d6c30ffc9f6709515bae Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 26 Aug 2019 03:11:24 +0200 Subject: [PATCH 082/483] Test permissions of websocket --- src/Test/TestUiWebsocket.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/Test/TestUiWebsocket.py diff --git a/src/Test/TestUiWebsocket.py b/src/Test/TestUiWebsocket.py new file mode 100644 index 00000000..d2d23d03 --- /dev/null +++ b/src/Test/TestUiWebsocket.py @@ -0,0 +1,11 @@ +import sys +import pytest + +@pytest.mark.usefixtures("resetSettings") +class TestUiWebsocket: + def testPermission(self, ui_websocket): + res = ui_websocket.testAction("ping") + assert res == "pong" + + res = ui_websocket.testAction("certList") + assert "You don't have permission" in res["error"] From 1bd1ddf41094499d2fac350dea80f2ea56802d82 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 26 Aug 2019 03:15:29 +0200 Subject: [PATCH 083/483] Test function flagging --- src/Test/TestFlag.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/Test/TestFlag.py diff --git a/src/Test/TestFlag.py b/src/Test/TestFlag.py new file mode 100644 index 00000000..12fd8165 --- /dev/null +++ b/src/Test/TestFlag.py @@ -0,0 +1,39 @@ +import os + +import pytest + +from util.Flag import Flag + +class TestFlag: + def testFlagging(self): + flag = Flag() + @flag.admin + @flag.no_multiuser + def testFn(anything): + return anything + + assert "admin" in flag.db["testFn"] + assert "no_multiuser" in flag.db["testFn"] + + def testSubclassedFlagging(self): + flag = Flag() + class Test: + @flag.admin + @flag.no_multiuser + def testFn(anything): + return anything + + class SubTest(Test): + pass + + assert "admin" in flag.db["testFn"] + assert "no_multiuser" in flag.db["testFn"] + + def testInvalidFlag(self): + flag = Flag() + with pytest.raises(Exception) as err: + @flag.no_multiuser + @flag.unknown_flag + def testFn(anything): + return anything + assert "Invalid flag" in str(err.value) From d166a16a247f44831b0bb12e24ac32733e7c6312 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 26 Aug 2019 03:20:07 +0200 Subject: [PATCH 084/483] Use function flagging in plugins --- plugins/Bigfile/BigfilePlugin.py | 2 + plugins/Chart/ChartPlugin.py | 9 ++-- plugins/ContentFilter/ContentFilterPlugin.py | 26 ++++++----- plugins/MergerSite/MergerSitePlugin.py | 2 + plugins/Newsfeed/NewsfeedPlugin.py | 5 +- plugins/OptionalManager/UiWebsocketPlugin.py | 15 ++++-- plugins/OptionalManager/__init__.py | 3 +- plugins/Sidebar/ConsolePlugin.py | 8 +++- plugins/Sidebar/SidebarPlugin.py | 46 ++++++------------- plugins/UiConfig/UiConfigPlugin.py | 8 +--- .../UiPluginManager/UiPluginManagerPlugin.py | 17 +++---- plugins/disabled-Multiuser/MultiuserPlugin.py | 24 ++++------ 12 files changed, 75 insertions(+), 90 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 60e43693..13270ca9 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -21,6 +21,7 @@ with warnings.catch_warnings(): from util import helper from util import Msgpack +from util.Flag import flag import util from .BigfilePiecefield import BigfilePiecefield, BigfilePiecefieldPacked @@ -167,6 +168,7 @@ class UiWebsocketPlugin(object): "file_relative_path": file_relative_path } + @flag.no_multiuser def actionSiteSetAutodownloadBigfileLimit(self, to, limit): permissions = self.getPermissions(to) if "ADMIN" not in permissions: diff --git a/plugins/Chart/ChartPlugin.py b/plugins/Chart/ChartPlugin.py index ddc1e609..80a4d976 100644 --- a/plugins/Chart/ChartPlugin.py +++ b/plugins/Chart/ChartPlugin.py @@ -5,6 +5,7 @@ import gevent from Config import config from util import helper +from util.Flag import flag from Plugin import PluginManager from .ChartDb import ChartDb from .ChartCollector import ChartCollector @@ -28,10 +29,8 @@ class SiteManagerPlugin(object): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): + @flag.admin def actionChartDbQuery(self, to, query, params=None): - if not "ADMIN" in self.permissions: - return {"error": "No permission"} - if config.debug or config.verbose: s = time.time() rows = [] @@ -49,10 +48,8 @@ class UiWebsocketPlugin(object): self.log.debug("Slow query: %s (%.3fs)" % (query, time.time() - s)) return rows + @flag.admin def actionChartGetPeerLocations(self, to): - if not "ADMIN" in self.permissions: - return {"error": "No permission"} - peers = {} for site in self.server.sites.values(): peers.update(site.peers) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index 0a45ceec..f46321d4 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -7,6 +7,7 @@ import os from Plugin import PluginManager from Translate import Translate from Config import config +from util.Flag import flag from .ContentFilterStorage import ContentFilterStorage @@ -36,6 +37,7 @@ class UiWebsocketPlugin(object): filter_storage.changeDbs(auth_address, "remove") self.response(to, "ok") + @flag.no_multiuser def actionMuteAdd(self, to, auth_address, cert_user_id, reason): if "ADMIN" in self.getPermissions(to): self.cbMuteAdd(to, auth_address, cert_user_id, reason) @@ -46,12 +48,14 @@ class UiWebsocketPlugin(object): lambda res: self.cbMuteAdd(to, auth_address, cert_user_id, reason) ) + @flag.no_multiuser def cbMuteRemove(self, to, auth_address): del filter_storage.file_content["mutes"][auth_address] filter_storage.save() filter_storage.changeDbs(auth_address, "load") self.response(to, "ok") + @flag.no_multiuser def actionMuteRemove(self, to, auth_address): if "ADMIN" in self.getPermissions(to): self.cbMuteRemove(to, auth_address) @@ -63,34 +67,31 @@ class UiWebsocketPlugin(object): lambda res: self.cbMuteRemove(to, auth_address) ) + @flag.admin def actionMuteList(self, to): - if "ADMIN" in self.getPermissions(to): - self.response(to, filter_storage.file_content["mutes"]) - else: - return self.response(to, {"error": "Forbidden: Only ADMIN sites can list mutes"}) + self.response(to, filter_storage.file_content["mutes"]) # Siteblock + @flag.no_multiuser + @flag.admin def actionSiteblockAdd(self, to, site_address, reason=None): - if "ADMIN" not in self.getPermissions(to): - return self.response(to, {"error": "Forbidden: Only ADMIN sites can add to blocklist"}) filter_storage.file_content["siteblocks"][site_address] = {"date_added": time.time(), "reason": reason} filter_storage.save() self.response(to, "ok") + @flag.no_multiuser + @flag.admin def actionSiteblockRemove(self, to, site_address): - if "ADMIN" not in self.getPermissions(to): - return self.response(to, {"error": "Forbidden: Only ADMIN sites can remove from blocklist"}) del filter_storage.file_content["siteblocks"][site_address] filter_storage.save() self.response(to, "ok") + @flag.admin def actionSiteblockList(self, to): - if "ADMIN" in self.getPermissions(to): - self.response(to, filter_storage.file_content["siteblocks"]) - else: - return self.response(to, {"error": "Forbidden: Only ADMIN sites can list blocklists"}) + self.response(to, filter_storage.file_content["siteblocks"]) # Include + @flag.no_multiuser def actionFilterIncludeAdd(self, to, inner_path, description=None, address=None): if address: if "ADMIN" not in self.getPermissions(to): @@ -122,6 +123,7 @@ class UiWebsocketPlugin(object): filter_storage.includeAdd(address, inner_path, description) self.response(to, "ok") + @flag.no_multiuser def actionFilterIncludeRemove(self, to, inner_path, address=None): if address: if "ADMIN" not in self.getPermissions(to): diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index 77d83931..ac1b467b 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -7,6 +7,7 @@ from Plugin import PluginManager from Translate import Translate from util import RateLimit from util import helper +from util.Flag import flag from Debug import Debug try: import OptionalManager.UiWebsocketPlugin # To make optioanlFileInfo merger sites compatible @@ -86,6 +87,7 @@ class UiWebsocketPlugin(object): site_manager.updateMergerSites() # Delete a merged site + @flag.no_multiuser def actionMergerSiteDelete(self, to, address): site = self.server.sites.get(address) if not site: diff --git a/plugins/Newsfeed/NewsfeedPlugin.py b/plugins/Newsfeed/NewsfeedPlugin.py index f15d7447..3eb14d6c 100644 --- a/plugins/Newsfeed/NewsfeedPlugin.py +++ b/plugins/Newsfeed/NewsfeedPlugin.py @@ -5,6 +5,7 @@ from Plugin import PluginManager from Db.DbQuery import DbQuery from Debug import Debug from util import helper +from util.Flag import flag @PluginManager.registerTo("UiWebsocket") @@ -27,10 +28,8 @@ class UiWebsocketPlugin(object): feeds = self.user.sites.get(self.site.address, {}).get("follow", {}) self.response(to, feeds) + @flag.admin def actionFeedQuery(self, to, limit=10, day_limit=3): - if "ADMIN" not in self.site.settings["permissions"]: - return self.response(to, "FeedQuery not allowed") - from Site import SiteManager rows = [] stats = [] diff --git a/plugins/OptionalManager/UiWebsocketPlugin.py b/plugins/OptionalManager/UiWebsocketPlugin.py index 9c6ad742..103bbe84 100644 --- a/plugins/OptionalManager/UiWebsocketPlugin.py +++ b/plugins/OptionalManager/UiWebsocketPlugin.py @@ -8,6 +8,7 @@ import gevent from Plugin import PluginManager from Config import config from util import helper +from util.Flag import flag from Translate import Translate @@ -214,6 +215,7 @@ class UiWebsocketPlugin(object): return "ok" + @flag.no_multiuser def actionOptionalFilePin(self, to, inner_path, address=None): if type(inner_path) is not list: inner_path = [inner_path] @@ -226,6 +228,7 @@ class UiWebsocketPlugin(object): self.cmd("notification", ["done", _["Pinned %s files"] % num_file, 5000]) self.response(to, back) + @flag.no_multiuser def actionOptionalFileUnpin(self, to, inner_path, address=None): if type(inner_path) is not list: inner_path = [inner_path] @@ -238,6 +241,7 @@ class UiWebsocketPlugin(object): self.cmd("notification", ["done", _["Removed pin from %s files"] % num_file, 5000]) self.response(to, back) + @flag.no_multiuser def actionOptionalFileDelete(self, to, inner_path, address=None): if not address: address = self.site.address @@ -275,10 +279,8 @@ class UiWebsocketPlugin(object): # Limit functions + @flag.admin def actionOptionalLimitStats(self, to): - if "ADMIN" not in self.site.settings["permissions"]: - return self.response(to, "Forbidden") - back = {} back["limit"] = config.optional_limit back["used"] = self.site.content_manager.contents.db.getOptionalUsedBytes() @@ -286,9 +288,9 @@ class UiWebsocketPlugin(object): self.response(to, back) + @flag.no_multiuser + @flag.admin def actionOptionalLimitSet(self, to, limit): - if "ADMIN" not in self.site.settings["permissions"]: - return self.response(to, {"error": "Forbidden"}) config.optional_limit = re.sub(r"\.0+$", "", limit) # Remove unnecessary digits from end config.saveValue("optional_limit", limit) self.response(to, "ok") @@ -306,6 +308,7 @@ class UiWebsocketPlugin(object): self.response(to, site.settings.get("optional_help", {})) + @flag.no_multiuser def actionOptionalHelp(self, to, directory, title, address=None): if not address: address = self.site.address @@ -342,6 +345,7 @@ class UiWebsocketPlugin(object): self.response(to, dict(stats)) + @flag.no_multiuser def actionOptionalHelpRemove(self, to, directory, address=None): if not address: address = self.site.address @@ -361,6 +365,7 @@ class UiWebsocketPlugin(object): site.settings["autodownloadoptional"] = value self.response(to, value) + @flag.no_multiuser def actionOptionalHelpAll(self, to, value, address=None): if not address: address = self.site.address diff --git a/plugins/OptionalManager/__init__.py b/plugins/OptionalManager/__init__.py index 1f0ad2dd..77b8c348 100644 --- a/plugins/OptionalManager/__init__.py +++ b/plugins/OptionalManager/__init__.py @@ -1 +1,2 @@ -from . import OptionalManagerPlugin \ No newline at end of file +from . import OptionalManagerPlugin +from . import UiWebsocketPlugin diff --git a/plugins/Sidebar/ConsolePlugin.py b/plugins/Sidebar/ConsolePlugin.py index 72192aa7..30d00fee 100644 --- a/plugins/Sidebar/ConsolePlugin.py +++ b/plugins/Sidebar/ConsolePlugin.py @@ -5,6 +5,7 @@ from Plugin import PluginManager from Config import config from Debug import Debug from util import SafeRe +from util.Flag import flag class WsLogStreamer(logging.StreamHandler): @@ -37,10 +38,11 @@ class WsLogStreamer(logging.StreamHandler): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): def __init__(self, *args, **kwargs): - self.admin_commands.update(["consoleLogRead", "consoleLogStream", "consoleLogStreamRemove"]) self.log_streamers = {} return super(UiWebsocketPlugin, self).__init__(*args, **kwargs) + @flag.no_multiuser + @flag.admin def actionConsoleLogRead(self, to, filter=None, read_size=32 * 1024, limit=500): log_file_path = "%s/debug.log" % config.log_dir log_file = open(log_file_path, encoding="utf-8") @@ -74,11 +76,15 @@ class UiWebsocketPlugin(object): logging.getLogger('').addHandler(logger) return logger + @flag.no_multiuser + @flag.admin def actionConsoleLogStream(self, to, filter=None): stream_id = to self.log_streamers[stream_id] = self.addLogStreamer(stream_id, filter) self.response(to, {"stream_id": stream_id}) + @flag.no_multiuser + @flag.admin def actionConsoleLogStreamRemove(self, to, stream_id): try: self.log_streamers[stream_id].stop() diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index f1a5e8e8..c4b3a4bb 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -16,6 +16,7 @@ from Plugin import PluginManager from Debug import Debug from Translate import Translate from util import helper +from util.Flag import flag from .ZipStream import ZipStream plugin_dir = os.path.dirname(__file__) @@ -85,10 +86,6 @@ class UiRequestPlugin(object): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): - def __init__(self, *args, **kwargs): - self.async_commands.add("sidebarGetPeers") - return super(UiWebsocketPlugin, self).__init__(*args, **kwargs) - def sidebarRenderPeerStats(self, body, site): connected = len([peer for peer in list(site.peers.values()) if peer.connection and peer.connection.connected]) connectable = len([peer_id for peer_id in list(site.peers.keys()) if not peer_id.endswith(":0")]) @@ -511,11 +508,8 @@ class UiWebsocketPlugin(object): body.append("") body.append("") + @flag.admin def actionSidebarGetHtmlTag(self, to): - permissions = self.getPermissions(to) - if "ADMIN" not in permissions: - return self.response(to, "You don't have permission to run this command") - site = self.site body = [] @@ -706,11 +700,9 @@ class UiWebsocketPlugin(object): return peer_locations - + @flag.admin + @flag.async_run def actionSidebarGetPeers(self, to): - permissions = self.getPermissions(to) - if "ADMIN" not in permissions: - return self.response(to, "You don't have permission to run this command") try: peer_locations = self.getPeerLocations(self.site.peers) globe_data = [] @@ -739,53 +731,43 @@ class UiWebsocketPlugin(object): self.log.debug("sidebarGetPeers error: %s" % Debug.formatException(err)) self.response(to, {"error": str(err)}) + @flag.admin + @flag.no_multiuser def actionSiteSetOwned(self, to, owned): - permissions = self.getPermissions(to) - if "ADMIN" not in permissions: - return self.response(to, "You don't have permission to run this command") - if self.site.address == config.updatesite: return self.response(to, "You can't change the ownership of the updater site") self.site.settings["own"] = bool(owned) self.site.updateWebsocket(owned=owned) + @flag.admin + @flag.no_multiuser def actionUserSetSitePrivatekey(self, to, privatekey): - permissions = self.getPermissions(to) - if "ADMIN" not in permissions: - return self.response(to, "You don't have permission to run this command") - site_data = self.user.sites[self.site.address] site_data["privatekey"] = privatekey self.site.updateWebsocket(set_privatekey=bool(privatekey)) return "ok" + @flag.admin + @flag.no_multiuser def actionSiteSetAutodownloadoptional(self, to, owned): - permissions = self.getPermissions(to) - if "ADMIN" not in permissions: - return self.response(to, "You don't have permission to run this command") - self.site.settings["autodownloadoptional"] = bool(owned) self.site.bad_files = {} gevent.spawn(self.site.update, check_files=True) self.site.worker_manager.removeSolvedFileTasks() + @flag.no_multiuser + @flag.admin def actionDbReload(self, to): - permissions = self.getPermissions(to) - if "ADMIN" not in permissions: - return self.response(to, "You don't have permission to run this command") - self.site.storage.closeDb() self.site.storage.getDb() return self.response(to, "ok") + @flag.no_multiuser + @flag.admin def actionDbRebuild(self, to): - permissions = self.getPermissions(to) - if "ADMIN" not in permissions: - return self.response(to, "You don't have permission to run this command") - try: self.site.storage.rebuildDb() except Exception as err: diff --git a/plugins/UiConfig/UiConfigPlugin.py b/plugins/UiConfig/UiConfigPlugin.py index fc71e28b..81cfe992 100644 --- a/plugins/UiConfig/UiConfigPlugin.py +++ b/plugins/UiConfig/UiConfigPlugin.py @@ -4,6 +4,7 @@ import os from Plugin import PluginManager from Config import config from Translate import Translate +from util.Flag import flag plugin_dir = os.path.dirname(__file__) @@ -12,12 +13,6 @@ if "_" not in locals(): _ = Translate(plugin_dir + "/languages/") -@PluginManager.afterLoad -def importPluginnedClasses(): - from Ui import UiWebsocket - UiWebsocket.admin_commands.add("configList") - - @PluginManager.registerTo("UiRequest") class UiRequestPlugin(object): def actionWrapper(self, path, extra_headers=None): @@ -58,6 +53,7 @@ class UiRequestPlugin(object): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): + @flag.admin def actionConfigList(self, to): back = {} config_values = vars(config.arguments) diff --git a/plugins/UiPluginManager/UiPluginManagerPlugin.py b/plugins/UiPluginManager/UiPluginManagerPlugin.py index df6236db..1ab80f53 100644 --- a/plugins/UiPluginManager/UiPluginManagerPlugin.py +++ b/plugins/UiPluginManager/UiPluginManagerPlugin.py @@ -8,6 +8,7 @@ from Plugin import PluginManager from Config import config from Debug import Debug from Translate import Translate +from util.Flag import flag plugin_dir = os.path.dirname(__file__) @@ -16,14 +17,6 @@ if "_" not in locals(): _ = Translate(plugin_dir + "/languages/") -@PluginManager.afterLoad -def importPluginnedClasses(): - from Ui import UiWebsocket - UiWebsocket.admin_commands.update([ - "pluginList", "pluginConfigSet", "pluginAdd", "pluginRemove", "pluginUpdate" - ]) - - # Convert non-str,int,float values to str in a dict def restrictDictValues(input_dict): allowed_types = (int, str, float) @@ -73,6 +66,7 @@ class UiRequestPlugin(object): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): + @flag.admin def actionPluginList(self, to): plugins = [] for plugin in PluginManager.plugin_manager.listPlugins(list_disabled=True): @@ -107,6 +101,8 @@ class UiWebsocketPlugin(object): return {"plugins": plugins} + @flag.admin + @flag.no_multiuser def actionPluginConfigSet(self, to, source, inner_path, key, value): plugin_manager = PluginManager.plugin_manager plugins = plugin_manager.listPlugins(list_disabled=True) @@ -196,6 +192,7 @@ class UiWebsocketPlugin(object): self.response(to, "ok") + @flag.no_multiuser def actionPluginAddRequest(self, to, inner_path): self.pluginAction("add_request", self.site.address, inner_path) plugin_info = self.site.storage.loadJson(inner_path + "/plugin_info.json") @@ -208,11 +205,15 @@ class UiWebsocketPlugin(object): lambda res: self.doPluginAdd(to, inner_path, res) ) + @flag.admin + @flag.no_multiuser def actionPluginRemove(self, to, address, inner_path): self.pluginAction("remove", address, inner_path) PluginManager.plugin_manager.saveConfig() return "ok" + @flag.admin + @flag.no_multiuser def actionPluginUpdate(self, to, address, inner_path): self.pluginAction("update", address, inner_path) PluginManager.plugin_manager.saveConfig() diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index fc88922a..8a8ee8f2 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -6,6 +6,8 @@ from Config import config from Plugin import PluginManager from Crypt import CryptBitcoin from . import UserPlugin +from util.Flag import flag + # We can only import plugin host clases after the plugins are loaded @PluginManager.afterLoad @@ -101,16 +103,8 @@ class UiRequestPlugin(object): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): def __init__(self, *args, **kwargs): - self.multiuser_denied_cmds = ( - "siteDelete", "configSet", "serverShutdown", "serverUpdate", "siteClone", - "siteSetOwned", "siteSetAutodownloadoptional", "dbReload", "dbRebuild", - "mergerSiteDelete", "siteSetLimit", "siteSetAutodownloadBigfileLimit", - "optionalLimitSet", "optionalHelp", "optionalHelpRemove", "optionalHelpAll", "optionalFilePin", "optionalFileUnpin", "optionalFileDelete", - "muteAdd", "muteRemove", "siteblockAdd", "siteblockRemove", "filterIncludeAdd", "filterIncludeRemove", - "pluginConfigSet", "pluginAdd", "pluginRemove", "pluginUpdate", "pluginAddRequest" - ) if config.multiuser_no_new_sites: - self.multiuser_denied_cmds += ("mergerSiteAdd", ) + flag.no_multiuser(self.actionMergerSiteAdd) super(UiWebsocketPlugin, self).__init__(*args, **kwargs) @@ -123,18 +117,16 @@ class UiWebsocketPlugin(object): return server_info # Show current user's master seed + @flag.admin def actionUserShowMasterSeed(self, to): - if "ADMIN" not in self.site.settings["permissions"]: - return self.response(to, "Show master seed not allowed") message = "Your unique private key:" message += "
%s
" % self.user.master_seed message += "(Save it, you can access your account using this information)" self.cmd("notification", ["info", message]) # Logout user + @flag.admin def actionUserLogout(self, to): - if "ADMIN" not in self.site.settings["permissions"]: - return self.response(to, "Logout not allowed") message = "You have been logged out. Login to another account" self.cmd("notification", ["done", message, 1000000]) # 1000000 = Show ~forever :) @@ -170,8 +162,9 @@ class UiWebsocketPlugin(object): self.actionUserLoginForm(0) def hasCmdPermission(self, cmd): - cmd = cmd[0].lower() + cmd[1:] - if not config.multiuser_local and self.user.master_address not in local_master_addresses and cmd in self.multiuser_denied_cmds: + flags = flag.db.get(self.getCmdFuncName(cmd), ()) + is_public_proxy_user = not config.multiuser_local and self.user.master_address not in local_master_addresses + if is_public_proxy_user and "no_multiuser" in flags: self.cmd("notification", ["info", "This function is disabled on this proxy!"]) return False else: @@ -214,7 +207,6 @@ class UiWebsocketPlugin(object): """.replace("{master_seed}", master_seed) self.cmd("injectScript", script) - def actionPermissionAdd(self, to, permission): if permission == "NOSANDBOX": self.cmd("notification", ["info", "You can't disable sandbox on this proxy!"]) From 912c958ac02ec367f522ab1027720b397ae81a3e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 26 Aug 2019 03:21:04 +0200 Subject: [PATCH 085/483] Rev4197 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 5e9a5e3b..1edd35f1 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4191 + self.rev = 4197 self.argv = argv self.action = None self.pending_changes = {} From baa5df1d0103f8136353546d2732ebd3cb0f3a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Otr=C4=99ba?= Date: Fri, 30 Aug 2019 18:59:19 +0200 Subject: [PATCH 086/483] fixed KeyError: 'piece_size' when try to download non-optional file using '|all' --- plugins/Bigfile/BigfilePlugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 13270ca9..e3974ef6 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -753,6 +753,11 @@ class SitePlugin(object): inner_path = inner_path.replace("|all", "") file_info = self.needFileInfo(inner_path) + + # Use default function to download non-optional file + if "piece_size" not in file_info: + return super(SitePlugin, self).needFile(inner_path, *args, **kwargs) + file_size = file_info["size"] piece_size = file_info["piece_size"] From 3c4bc6ae35aae321def330750fb74bf743e20586 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Sep 2019 02:08:07 +0200 Subject: [PATCH 087/483] Always update merger sites db on content.json update --- plugins/MergerSite/MergerSitePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index ac1b467b..ae2b1484 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -295,6 +295,9 @@ class SiteStoragePlugin(object): # Also notice merger sites on a merged site file change def onUpdated(self, inner_path, file=None): + if inner_path == "content.json": + site_manager.updateMergerSites() + super(SiteStoragePlugin, self).onUpdated(inner_path, file) merged_type = merged_db.get(self.site.address) From 9ac96cdd50d6e77141005b24832423b53e9c38dc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Sep 2019 02:09:53 +0200 Subject: [PATCH 088/483] Don't leak allowed origins in error message --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 2a09bd3e..0c3ea447 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -735,7 +735,7 @@ class UiRequest(object): origin_host = origin.split("://", 1)[-1] if origin_host != host and origin_host not in self.server.allowed_ws_origins: ws.send(json.dumps({"error": "Invalid origin: %s" % origin})) - return self.error403("Invalid origin: %s %s" % (origin, self.server.allowed_ws_origins)) + return self.error403("Invalid origin: %s" % origin) # Find site by wrapper_key wrapper_key = self.get["wrapper_key"] From f999f167b1d86d9cb42a64f57cb44e920e25fb23 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Sep 2019 02:10:52 +0200 Subject: [PATCH 089/483] Offer access with ip address on invalid host error --- src/Ui/UiRequest.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 0c3ea447..42536348 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -5,6 +5,7 @@ import mimetypes import json import html import urllib +import socket import gevent @@ -83,7 +84,16 @@ class UiRequest(object): # Check if host allowed to do request if not self.isHostAllowed(self.env.get("HTTP_HOST")): - return self.error403("Invalid host: %s" % self.env.get("HTTP_HOST"), details=False) + ret_error = next(self.error403("Invalid host: %s" % self.env.get("HTTP_HOST"), details=False)) + + http_get = self.env["PATH_INFO"] + if self.env["QUERY_STRING"]: + http_get += "?{0}".format(self.env["QUERY_STRING"]) + self_host = self.env["HTTP_HOST"].split(":")[0] + self_ip = self.env["HTTP_HOST"].replace(self_host, socket.gethostbyname(self_host)) + link = "http://{0}{1}".format(self_ip, http_get) + ret_link = """

Access via ip: {0}""".format(html.escape(link)).encode("utf8") + return iter([ret_error, ret_link]) # Prepend .bit host for transparent proxy if self.server.site_manager.isDomain(self.env.get("HTTP_HOST")): From 76bc9fcddf7d71702d9530f637affec2a6077fd2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Sep 2019 14:17:35 +0200 Subject: [PATCH 090/483] Open sidebar with location hash --- plugins/Sidebar/media/Sidebar.coffee | 4 ++-- plugins/Sidebar/media/all.js | 22 +++++++++------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 827e7f65..5a4111f5 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -23,9 +23,9 @@ class Sidebar extends Class @original_set_site_info = @wrapper.setSiteInfo # We going to override this, save the original # Start in opened state for debugging - if false + if window.top.location.hash == "#ZeroNet:OpenSidebar" @startDrag() - @moved() + @moved("x") @fixbutton_targetx = @fixbutton_initx - @width @stopDrag() diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 0ec96b06..82810805 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -1,5 +1,5 @@ -/* ---- plugins/Sidebar/media/Class.coffee ---- */ +/* ---- Class.coffee ---- */ (function() { @@ -55,8 +55,7 @@ }).call(this); - -/* ---- plugins/Sidebar/media/Console.coffee ---- */ +/* ---- Console.coffee ---- */ (function() { @@ -284,8 +283,7 @@ }).call(this); - -/* ---- plugins/Sidebar/media/Menu.coffee ---- */ +/* ---- Menu.coffee ---- */ (function() { @@ -367,8 +365,7 @@ }).call(this); - -/* ---- plugins/Sidebar/media/RateLimit.coffee ---- */ +/* ---- RateLimit.coffee ---- */ (function() { @@ -396,8 +393,7 @@ }).call(this); - -/* ---- plugins/Sidebar/media/Scrollable.js ---- */ +/* ---- Scrollable.js ---- */ /* via http://jsfiddle.net/elGrecode/00dgurnn/ */ @@ -492,7 +488,7 @@ window.initScrollable = function () { return updateHeight; }; -/* ---- plugins/Sidebar/media/Sidebar.coffee ---- */ +/* ---- Sidebar.coffee ---- */ (function() { @@ -534,9 +530,9 @@ window.initScrollable = function () { this.globe = null; this.preload_html = null; this.original_set_site_info = this.wrapper.setSiteInfo; - if (false) { + if (window.top.location.hash === "#ZeroNet:OpenSidebar") { this.startDrag(); - this.moved(); + this.moved("x"); this.fixbutton_targetx = this.fixbutton_initx - this.width; this.stopDrag(); } @@ -1279,7 +1275,7 @@ window.initScrollable = function () { }).call(this); -/* ---- plugins/Sidebar/media/morphdom.js ---- */ +/* ---- morphdom.js ---- */ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.morphdom = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Mon, 2 Sep 2019 14:17:46 +0200 Subject: [PATCH 091/483] Rev4200 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 1edd35f1..56d04231 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4197 + self.rev = 4200 self.argv = argv self.action = None self.pending_changes = {} From 500c96abe21f27bf13d74e4104241ae120e55f34 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 2 Sep 2019 14:35:28 +0000 Subject: [PATCH 092/483] Fix UnicodeDecodeError when OpenSSL is not found Fixes #2180 --- src/Crypt/CryptConnection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 7fc64152..9734089e 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -137,14 +137,14 @@ class CryptConnectionManager: cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env ) - back = proc.stdout.read().strip().decode().replace("\r", "") + back = proc.stdout.read().strip().replace(b"\r", b"") proc.wait() if not (os.path.isfile(self.cacert_pem) and os.path.isfile(self.cakey_pem)): self.log.error("RSA ECC SSL CAcert generation failed, CAcert or CAkey files not exist. (%s)" % back) return False else: - self.log.debug("Result: %s" % back) + self.log.debug("Result: %s" % back.decode()) # Generate certificate key and signing request cmd_params = helper.shellquote( From 5da4537d7c0fa68cac1ec7f4371d596cf8a400b4 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 2 Sep 2019 19:34:29 +0000 Subject: [PATCH 093/483] Fix gevent.Timeout being not caught --- src/Ui/UiWebsocket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index bf76639a..ab27df49 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -701,7 +701,7 @@ class UiWebsocket(object): try: with gevent.Timeout(timeout): self.site.needFile(inner_path, priority=6) - except Exception as err: + except (Exception, gevent.Timeout) as err: return self.response(to, {"error": Debug.formatExceptionMessage(err)}) return self.response(to, "ok") From 0b04176f189cd46bf02273715e8ee0e5da1b83e3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 3 Sep 2019 12:00:25 +0200 Subject: [PATCH 094/483] Rev4203, Change console encoding to utf8 on Windows --- src/Config.py | 2 +- src/Crypt/CryptConnection.py | 8 ++++---- src/main.py | 9 +++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Config.py b/src/Config.py index 56d04231..2cc993fa 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4200 + self.rev = 4203 self.argv = argv self.action = None self.pending_changes = {} diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 9734089e..c5ee2761 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -137,14 +137,14 @@ class CryptConnectionManager: cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env ) - back = proc.stdout.read().strip().replace(b"\r", b"") + back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "") proc.wait() if not (os.path.isfile(self.cacert_pem) and os.path.isfile(self.cakey_pem)): self.log.error("RSA ECC SSL CAcert generation failed, CAcert or CAkey files not exist. (%s)" % back) return False else: - self.log.debug("Result: %s" % back.decode()) + self.log.debug("Result: %s" % back) # Generate certificate key and signing request cmd_params = helper.shellquote( @@ -160,7 +160,7 @@ class CryptConnectionManager: cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env ) - back = proc.stdout.read().strip().decode().replace("\r", "") + back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "") proc.wait() self.log.debug("Running: %s\n%s" % (cmd, back)) @@ -179,7 +179,7 @@ class CryptConnectionManager: cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=self.openssl_env ) - back = proc.stdout.read().strip().decode().replace("\r", "") + back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "") proc.wait() self.log.debug("Running: %s\n%s" % (cmd, back)) diff --git a/src/main.py b/src/main.py index 7ffbedd5..f75d2dff 100644 --- a/src/main.py +++ b/src/main.py @@ -76,6 +76,15 @@ if config.stack_size: if config.msgpack_purepython: os.environ["MSGPACK_PUREPYTHON"] = "True" +# Fix console encoding on Windows +if sys.platform.startswith("win"): + import subprocess + try: + chcp_res = subprocess.check_output("chcp 65001", shell=True).decode(errors="ignore").strip() + logging.debug("Changed console encoding to utf8: %s" % chcp_res) + except Exception as err: + logging.error("Error changing console encoding to utf8: %s" % err) + # Socket monkey patch if config.proxy: from util import SocksProxy From 743463dce90a662f3855f802a36c1a029cdecedc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Sep 2019 20:13:16 +0200 Subject: [PATCH 095/483] Execute shutdown function before running update to avoid segfault on linux --- zeronet.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/zeronet.py b/zeronet.py index 73339c3f..e9f8bc27 100755 --- a/zeronet.py +++ b/zeronet.py @@ -1,6 +1,4 @@ #!/usr/bin/env python3 - -# Included modules import os import sys @@ -35,12 +33,19 @@ def main(): if main and (main.update_after_shutdown or main.restart_after_shutdown): # Updater if main.update_after_shutdown: + print("Shutting down...") + prepareShutdown() import update + print("Updating...") update.update() - restart() - else: print("Restarting...") restart() + else: + print("Shutting down...") + prepareShutdown() + print("Restarting...") + restart() + def displayErrorMessage(err, error_log_path): import ctypes @@ -64,11 +69,12 @@ def displayErrorMessage(err, error_log_path): if res in [ID_YES, ID_NO]: subprocess.Popen(['notepad.exe', error_log_path]) +def prepareShutdown(): + import atexit + atexit._run_exitfuncs() -def restart(): + # Close log files if "main" in sys.modules: - import atexit - # Close log files logger = sys.modules["main"].logging.getLogger() for handler in logger.handlers[:]: @@ -76,10 +82,10 @@ def restart(): handler.close() logger.removeHandler(handler) - atexit._run_exitfuncs() - import time - time.sleep(1) # Wait files to close + import time + time.sleep(1) # Wait files to close +def restart(): args = sys.argv[:] sys.executable = sys.executable.replace(".pkg", "") # Frozen mac fix From 4f0613689a6874f27e1fc3b6b432def5225e83fe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Sep 2019 20:13:32 +0200 Subject: [PATCH 096/483] Formatting --- zeronet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zeronet.py b/zeronet.py index e9f8bc27..03ad1808 100755 --- a/zeronet.py +++ b/zeronet.py @@ -60,8 +60,9 @@ def displayErrorMessage(err, error_log_path): ID_CANCEL = 0x2 err_message = "%s: %s" % (type(err).__name__, err) + err_title = "Unhandled exception: %s\nReport error?" % err_message - res = ctypes.windll.user32.MessageBoxW(0, "Unhandled exception: %s\nReport error?" % err_message, "ZeroNet error", MB_YESNOCANCEL | MB_ICONEXCLAIMATION) + res = ctypes.windll.user32.MessageBoxW(0, err_title, "ZeroNet error", MB_YESNOCANCEL | MB_ICONEXCLAIMATION) if res == ID_YES: import webbrowser report_url = "https://github.com/HelloZeroNet/ZeroNet/issues/new?assignees=&labels=&template=bug-report.md&title=%s" From eab63c6af89d2e7f21a4a2f03b0a2cdfec252dca Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Sep 2019 20:15:37 +0200 Subject: [PATCH 097/483] Keep file permissions on update rename workaround --- update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/update.py b/update.py index 0dfff297..e55eaa57 100644 --- a/update.py +++ b/update.py @@ -2,6 +2,7 @@ import os import sys import json import re +import shutil def update(): @@ -94,13 +95,14 @@ def update(): num_ok += 1 except Exception as err: try: - print("Error writing: %s. Renaming old file to avoid lock on Windows..." % err) + print("Error writing: %s. Renaming old file as workaround..." % err) path_to_tmp = path_to + "-old" if os.path.isfile(path_to_tmp): os.unlink(path_to_tmp) os.rename(path_to, path_to_tmp) num_rename += 1 open(path_to, 'wb').write(data) + shutil.copymode(path_to_tmp, path_to) # Copy permissions print("Write done after rename!") num_ok += 1 except Exception as err: From 2a7d7acce0c4ce9a651b6f6d3fa9d774dcf03dc2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Sep 2019 20:15:49 +0200 Subject: [PATCH 098/483] Support updating linux bundle --- update.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/update.py b/update.py index e55eaa57..ab39a602 100644 --- a/update.py +++ b/update.py @@ -16,7 +16,10 @@ def update(): else: source_path = os.getcwd().rstrip("/") - runtime_path = os.path.dirname(sys.executable) + if config.dist_type.startswith("bundle_linux"): + runtime_path = os.path.normpath(os.path.dirname(sys.executable) + "/../..") + else: + runtime_path = os.path.dirname(sys.executable) updatesite_path = config.data_dir + "/" + config.updatesite From d3fce8ca361f3e7d1e022cdd3446cb62605aa336 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Sep 2019 20:16:32 +0200 Subject: [PATCH 099/483] Support Linux bundle OpenSSL --- src/Crypt/CryptConnection.py | 4 +++- src/util/OpensslFindPatch.py | 13 +++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index c5ee2761..866537b7 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -11,11 +11,13 @@ from util import helper class CryptConnectionManager: def __init__(self): - # OpenSSL params if sys.platform.startswith("win"): self.openssl_bin = "tools\\openssl\\openssl.exe" + elif config.dist_type.startswith("bundle_linux"): + self.openssl_bin = "../runtime/bin/openssl" else: self.openssl_bin = "openssl" + self.openssl_env = { "OPENSSL_CONF": "src/lib/openssl/openssl.cnf", "RANDFILE": config.data_dir + "/openssl-rand.tmp" diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index f5b88acc..864882e9 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -11,18 +11,19 @@ find_library_original = ctypes.util.find_library def getOpensslPath(): if sys.platform.startswith("win"): lib_paths = [ - os.path.join(os.getcwd(), "tools/openssl/libeay32.dll"), + os.path.join(os.getcwd(), "tools/openssl/libeay32.dll"), # ZeroBundle Windows os.path.join(os.path.dirname(sys.executable), "DLLs/libcrypto-1_1-x64.dll"), os.path.join(os.path.dirname(sys.executable), "DLLs/libcrypto-1_1.dll") ] elif sys.platform == "cygwin": lib_paths = ["/bin/cygcrypto-1.0.0.dll"] - elif os.path.isfile("../lib/libcrypto.so"): # ZeroBundle OSX - lib_paths = ["../lib/libcrypto.so"] - elif os.path.isfile("/opt/lib/libcrypto.so.1.0.0"): # For optware and entware - lib_paths = ["/opt/lib/libcrypto.so.1.0.0"] else: - lib_paths = ["/usr/local/ssl/lib/libcrypto.so"] + lib_paths = [ + "../runtime/lib/libcrypto.so.1.1", # ZeroBundle Linux + "../lib/libcrypto.so", # ZeroBundle OSX + "/opt/lib/libcrypto.so.1.0.0", # For optware and entware + "/usr/local/ssl/lib/libcrypto.so" + ] for lib_path in lib_paths: if os.path.isfile(lib_path): From 38e20b7c31766225285da0d59c832a3512e110a5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Sep 2019 20:16:57 +0200 Subject: [PATCH 100/483] Rev4206 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 2cc993fa..464a3144 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.0" - self.rev = 4203 + self.rev = 4206 self.argv = argv self.action = None self.pending_changes = {} From 62d278a367d6979e281d8a80f7251f11d7c2e26a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 6 Sep 2019 04:03:01 +0200 Subject: [PATCH 101/483] Version 0.7.1 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 464a3144..4927de4a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -12,7 +12,7 @@ import stat class Config(object): def __init__(self, argv): - self.version = "0.7.0" + self.version = "0.7.1" self.rev = 4206 self.argv = argv self.action = None From 55c7585334310b131e25ef427c778e69b381a6b0 Mon Sep 17 00:00:00 2001 From: krzotr Date: Sun, 8 Sep 2019 11:51:46 +0200 Subject: [PATCH 102/483] Set custom priority in FileNeed and FileGet command When you use `FileNeed` or `FileGet` command the default priority is set to `6`. You cannot change that value because is hardcoded. Now you can set priority of downloading files manually: ``` this.cmd("fileNeed", { "inner_path": inner_path + "|all", "priority": 10 }) ``` --- src/Ui/UiWebsocket.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index ab27df49..6d6559f2 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -677,11 +677,11 @@ class UiWebsocket(object): # Return file content @flag.async_run - def actionFileGet(self, to, inner_path, required=True, format="text", timeout=300): + def actionFileGet(self, to, inner_path, required=True, format="text", timeout=300, priority=6): try: if required or inner_path in self.site.bad_files: with gevent.Timeout(timeout): - self.site.needFile(inner_path, priority=6) + self.site.needFile(inner_path, priority=priority) body = self.site.storage.read(inner_path, "rb") except (Exception, gevent.Timeout) as err: self.log.error("%s fileGet error: %s" % (inner_path, Debug.formatException(err))) @@ -697,10 +697,10 @@ class UiWebsocket(object): self.response(to, body) @flag.async_run - def actionFileNeed(self, to, inner_path, timeout=300): + def actionFileNeed(self, to, inner_path, timeout=300, priority=6): try: with gevent.Timeout(timeout): - self.site.needFile(inner_path, priority=6) + self.site.needFile(inner_path, priority=priority) except (Exception, gevent.Timeout) as err: return self.response(to, {"error": Debug.formatExceptionMessage(err)}) return self.response(to, "ok") From 2de35266c4200929a37d0b8796ba30f281a18e5e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 10 Sep 2019 15:43:42 +0200 Subject: [PATCH 103/483] Rev4208, Add details on Tor connection error --- src/Config.py | 2 +- src/Tor/TorManager.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 4927de4a..756d70e2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4206 + self.rev = 4208 self.argv = argv self.action = None self.pending_changes = {} diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 52efff0a..7c3d7277 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -38,6 +38,7 @@ class TorManager(object): self.lock = RLock() self.starting = True self.connecting = True + self.status = None self.event_started = gevent.event.AsyncResult() if config.tor == "disable": @@ -64,7 +65,7 @@ class TorManager(object): self.starting = True try: if not self.connect(): - raise Exception("No connection") + raise Exception(self.status) self.log.debug("Tor proxy port %s check ok" % config.tor_proxy) except Exception as err: if sys.platform.startswith("win") and os.path.isfile(self.tor_exe): From c52da69367123ed0bbc35f29a022f8d9d5f14aac Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Tue, 10 Sep 2019 18:08:45 +0200 Subject: [PATCH 104/483] Check py3 branch build status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e2f6857..1a10c180 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=master)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) +# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) Decentralized websites using Bitcoin crypto and the BitTorrent network - https://zeronet.io From deec2e62ce6890dffe425fa3a7cb58729f58756d Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Tue, 10 Sep 2019 18:16:02 +0200 Subject: [PATCH 105/483] Add Linux bundle install method --- README.md | 50 ++++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 1a10c180..1fc87942 100644 --- a/README.md +++ b/README.md @@ -66,40 +66,26 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/ - Download [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB) - Unpack anywhere - Run `ZeroNet.exe` + +### Linux (x86-64bit) + - `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz` + - `tar xvpfz ZeroNet-py3-linux64.tar.gz` + - `cd ZeroNet-linux-dist-linux64/` + - Start with: `./ZeroNet.sh` + - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/ + + __Tip:__ Start with `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` to allow remote connections on the web interface. -### Other platforms: Install from source +### Install from source -Fetch and extract the source: - - wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz - tar xvpfz ZeroNet-py3.tar.gz - cd ZeroNet-py3 - -Install Python module dependencies either: - -* (Option A) into a [virtual env](https://virtualenv.readthedocs.org/en/latest/) - - ``` - python3 -m venv zeronet - source zeronet/bin/activate - python3 -m pip install -r requirements.txt - ``` - -* (Option B) into the system (requires root), for example, on Debian/Ubuntu: - - ``` - sudo apt-get update - sudo apt-get install python3-pip - sudo python3 -m pip install -r requirements.txt - ``` - -Start Zeronet: - - python3 zeronet.py - -Open the ZeroHello landing page in your browser by navigating to: - - http://127.0.0.1:43110/ + - `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz` + - `tar xvpfz ZeroNet-py3.tar.gz` + - `cd ZeroNet-py3` + - `sudo apt-get update` + - `sudo apt-get install python3-pip` + - `sudo python3 -m pip install -r requirements.txt` + - Start with: `python3 zeronet.py` + - Open the ZeroHello landing page in your browser by navigating to: http://127.0.0.1:43110/ ## Current limitations From 0738964e642716b2944fc435fed82b0375b0d1cd Mon Sep 17 00:00:00 2001 From: Lola Dam Date: Tue, 10 Sep 2019 18:18:21 +0200 Subject: [PATCH 106/483] Save content.json of site even if limit size is reached (#2114) * fix #2107; Still save the content.json received even if site size limit is reached but dont download files; Allow better distribution of latest version of content.json * Added test * Fix test for huge content file (now it fails) * Dont download huge content.json file and update test * Remove comments --- src/Content/ContentManager.py | 17 +++++++------ src/Site/Site.py | 13 ++++++++++ src/Test/TestSiteDownload.py | 46 ++++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 7a1a8447..c1ec533c 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -858,15 +858,16 @@ class ContentManager(object): if content.get("inner_path") and content["inner_path"] != inner_path: raise VerifyError("Wrong inner_path: %s" % content["inner_path"]) - # Check total site size limit - if site_size > site_size_limit: - if inner_path == "content.json" and self.site.settings["size"] == 0: - # First content.json download, save site size to display warning + # If our content.json file bigger than the size limit throw error + if inner_path == "content.json": + content_size_file = len(json.dumps(content, indent=1)) + if content_size_file > site_size_limit: + # Save site size to display warning self.site.settings["size"] = site_size - task = self.site.worker_manager.findTask(inner_path) - if task: # Dont try to download from other peers - self.site.worker_manager.failTask(task) - raise VerifyError("Content too large %sB > %sB, aborting task..." % (site_size, site_size_limit)) + task = self.site.worker_manager.findTask(inner_path) + if task: # Dont try to download from other peers + self.site.worker_manager.failTask(task) + raise VerifyError("Content too large %s B > %s B, aborting task..." % (site_size, site_size_limit)) # Verify valid filenames for file_relative_path in list(content.get("files", {}).keys()) + list(content.get("files_optional", {}).keys()): diff --git a/src/Site/Site.py b/src/Site/Site.py index c08e067e..af6563e3 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -179,6 +179,15 @@ class Site(object): if peer: # Update last received update from peer to prevent re-sending the same update to it peer.last_content_json_update = self.content_manager.contents[inner_path]["modified"] + # Verify size limit + if inner_path == "content.json": + site_size_limit = self.getSizeLimit() * 1024 * 1024 + content_size = len(json.dumps(self.content_manager.contents[inner_path], indent=1)) + sum([file["size"] for file in list(self.content_manager.contents[inner_path].get("files", {}).values()) if file["size"] >= 0]) # Size of new content + if site_size_limit < content_size: + # Not enought don't download anything + self.log.debug("Size limit reached (site too big please increase limit): %.2f MB > %.2f MB" % (content_size / 1024 / 1024, site_size_limit / 1024 / 1024)) + return False + # Start download files file_threads = [] if download_files: @@ -720,6 +729,10 @@ class Site(object): return self.needFile(*args, **kwargs) def isFileDownloadAllowed(self, inner_path, file_info): + # Verify space for all site + if self.settings["size"] > self.getSizeLimit() * 1024 * 1024: + return False + # Verify space for file if file_info.get("size", 0) > config.file_size_limit * 1024 * 1024: self.log.debug( "File size %s too large: %sMB > %sMB, skipping..." % diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index 1d4ba4c1..fc7ba23a 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -422,7 +422,7 @@ class TestSiteDownload: client.sites[site_temp.address] = site_temp site_temp.connection_server = client - # Connect peers + # Connect peersself, file_server, site, site_temp site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp @@ -460,3 +460,47 @@ class TestSiteDownload: assert len(file_requests) == 1 assert site_temp.storage.open("data/data.json").read() == data_new + assert site_temp.storage.open("content.json").read() == site.storage.open("content.json").read() + + # Test what happened if the content.json of the site is bigger than the site limit + def testHugeContentSiteUpdate(self, file_server, site, site_temp): + # Init source server + site.connection_server = file_server + file_server.sites[site.address] = site + + # Init client server + client = FileServer(file_server.ip, 1545) + client.sites[site_temp.address] = site_temp + site_temp.connection_server = client + + # Connect peersself, file_server, site, site_temp + site_temp.addPeer(file_server.ip, 1544) + + # Download site from site to site_temp + site_temp.download(blind_includes=True).join(timeout=5) + + # Raise limit size to 20MB on site so it can be signed + site.settings["size_limit"] = int(20 * 1024 *1024) + site.saveSettings() + + content_json = site.storage.loadJson("content.json") + content_json["description"] = "PartirUnJour" * 1024 * 1024 + site.storage.writeJson("content.json", content_json) + changed, deleted = site.content_manager.loadContent("content.json", force=True) + + # Make sure we have 2 differents content.json + assert site_temp.storage.open("content.json").read() != site.storage.open("content.json").read() + + # Generate diff + diffs = site.content_manager.getDiffs("content.json") + + # Publish with patch + site.log.info("Publish new content.json bigger than 10MB") + with Spy.Spy(FileRequest, "route") as requests: + site.content_manager.sign("content.json", privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv") + assert site.storage.getSize("content.json") > 10 * 1024 * 1024 # verify it over 10MB + site.publish(diffs=diffs) + site_temp.download(blind_includes=True).join(timeout=5) + + assert site_temp.storage.getSize("content.json") < site_temp.getSizeLimit() * 1024 * 1024 + assert site_temp.storage.open("content.json").read() != site.storage.open("content.json").read() From 448483371c06baad8b9ad73b7cc8c21c8e1a7384 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Sep 2019 00:23:36 +0200 Subject: [PATCH 107/483] Formatting --- src/Test/TestSiteDownload.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index fc7ba23a..55bdc345 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -422,7 +422,7 @@ class TestSiteDownload: client.sites[site_temp.address] = site_temp site_temp.connection_server = client - # Connect peersself, file_server, site, site_temp + # Connect peers site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp @@ -473,7 +473,7 @@ class TestSiteDownload: client.sites[site_temp.address] = site_temp site_temp.connection_server = client - # Connect peersself, file_server, site, site_temp + # Connect peers site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp From 96759e9303f15a823a8f6871e59a0e84a740264f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 12 Sep 2019 00:24:16 +0200 Subject: [PATCH 108/483] Rev4210, Fix format exception if no args --- src/Config.py | 2 +- src/Debug/Debug.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 756d70e2..e0424898 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4208 + self.rev = 4210 self.argv = argv self.action = None self.pending_changes = {} diff --git a/src/Debug/Debug.py b/src/Debug/Debug.py index 4c4099f7..90d450d0 100644 --- a/src/Debug/Debug.py +++ b/src/Debug/Debug.py @@ -15,7 +15,10 @@ class Notify(Exception): def formatExceptionMessage(err): err_type = err.__class__.__name__ - err_message = str(err.args[-1]) + if err.args: + err_message = err.args[-1] + else: + err_message = err.__str__() return "%s: %s" % (err_type, err_message) From 4293a44c93d4fef853a3df47064843f7278f3030 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 15 Sep 2019 22:08:20 +0200 Subject: [PATCH 109/483] Don't try to find OpenSSL 1.0.x --- src/util/OpensslFindPatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index 864882e9..ffc67a6c 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -45,7 +45,7 @@ def getOpensslPath(): logging.debug("OpenSSL lib not found in: %s (%s)" % (path, err)) lib_path = ( - find_library_original('ssl.so.1.0') or find_library_original('ssl') or + find_library_original('ssl.so') or find_library_original('ssl') or find_library_original('crypto') or find_library_original('libcrypto') or 'libeay32' ) From 10817aefae60b84d25228878642d840a91188340 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 15 Sep 2019 22:08:48 +0200 Subject: [PATCH 110/483] Fix pyelliptic OpenSSL 1.1 compatibility if it's also present in site-packages --- src/lib/pyelliptic/cipher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/pyelliptic/cipher.py b/src/lib/pyelliptic/cipher.py index b597cafa..54ae7a09 100644 --- a/src/lib/pyelliptic/cipher.py +++ b/src/lib/pyelliptic/cipher.py @@ -4,7 +4,7 @@ # Copyright (C) 2011 Yann GUIBET # See LICENSE for details. -from pyelliptic.openssl import OpenSSL +from .openssl import OpenSSL class Cipher: From 6f0d4a50d1b2cc4aee60f0e489dc0c0ac5d6878d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 15 Sep 2019 22:11:51 +0200 Subject: [PATCH 111/483] Add apple touch icon support for Safari --- src/Ui/UiRequest.py | 6 +++--- src/Ui/media/img/apple-touch-icon.png | Bin 0 -> 8178 bytes src/Ui/template/wrapper.html | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 src/Ui/media/img/apple-touch-icon.png diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 42536348..afaa4fec 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -119,8 +119,8 @@ class UiRequest(object): if path == "/": return self.actionIndex() - elif path == "/favicon.ico": - return self.actionFile("src/Ui/media/img/favicon.ico") + elif path in ("/favicon.ico", "/apple-touch-icon.png"): + return self.actionFile("src/Ui/media/img/%s" % path) # Internal functions elif "/ZeroNet-Internal/" in path: path = re.sub(".*?/ZeroNet-Internal/", "/", path) @@ -350,7 +350,7 @@ class UiRequest(object): return self.error403("WebSocket request not allowed to load wrapper") # No websocket if "text/html" not in self.env.get("HTTP_ACCEPT", ""): - return self.error403("Invalid Accept header to load wrapper") + return self.error403("Invalid Accept header to load wrapper: %s" % self.env.get("HTTP_ACCEPT", "")) if "prefetch" in self.env.get("HTTP_X_MOZ", "") or "prefetch" in self.env.get("HTTP_PURPOSE", ""): return self.error403("Prefetch not allowed to load wrapper") diff --git a/src/Ui/media/img/apple-touch-icon.png b/src/Ui/media/img/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..962bd31ad5a55c8067094469026a1343b5624262 GIT binary patch literal 8178 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�}X;Ioi{f=>1`F zO58k(H;25pGS#BVv}z!GYZaQ#G6_9o-Lp^a=X(;UPYmdLYBF-AwFh@>PUNzp*? zvnD+7+cC1SK6ZZoTAgO=c{Wx+?66k%D)ZNY=o__|K<}rgcmVGqel?Z?s<}h|CA8wzo z$oj=4N-E9E9D!IVOo+Ux+bpzR)j^iyrlB~k-4I?$gdR1r+4cRbQ}dtO|`E=yE2j24o!(m9?n$lv&Et>Sb&eYP;0Z*?dPdmJHEZPC%!qW zk@6s-fS4YGAA;2TFJ4KQUp&8u_+NW=4WNP@Rg7S2C$FGO((yg@sr?Fg`9`&`k%%dl z>@cwWumGkMCE{_2C=~)vB{5FnqUm>II%noQc(d*w3v)1z+E?LUUT5{nlK5A*oQcv* z@wPF~_Xau?E@=s?5d>iqn2!LTdE%xYwqORy12tP31Ues}+g5gKcvtNDuD1WU!zk#7 zGKlJAquO=Bj>w^vQ+)fVDn3yS(_)@HVuWy#S>fw% zX7H{hJWaI+-)RjgeLKVZomgD0LtI{j%g7$k?b3Yew9`h@nhKe-@j|_H*FnYbyyIkB zQgIr)XuQmx>18aZqzVfC1aOmL`{erDyF&cnOMOFVVgR$c)4aW^c7(nA>Dw@!_fKVV z0G~;!q*dk9tb~fieey~q_H9TJryn4xdTgV}RHPE7zCmo=@rdu;7zRl0WEP>s zRJp=I+7&^)ySsdgbbk&!{@Zl6(O(xl92f`6gV*7KnY_!^*Z3W(1JFfLQad_WmRG-fID?#`yMwp~3CV#@kQa zG5e`K7j;JmU!{AT8=-C@wTUV0n-2!X_j_Ba-L88UdP(z7S8~y-{cfS>f8VPlK(csW zr&qk>b@sZ~0TToxSE2~dQjozpstRvvH4{RO!SP7iv`#w7N2iAdI&S>`x01F0S8n+7 z!T+r?vpasCo%>(1$hH>!lY4Y$3^K%RHz}UEXg}J%k zg*jjRKMHmZ<{vCQ{{ITBGee4K1szXKU1xO@H^@h%gXITX3y8DFM+?Y@kIrTQ;GVvI zOo->G4TTP<4i3q0r~{9l6L6XlkUb|;j3a+U20{4n{W5>_yhal;I6K+bHPl7hg*)gY cm#x4F>{5_8A4~Bkpr-)jWmFL5QpSG&2Rle}iU0rr literal 0 HcmV?d00001 diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 96f68d62..f8b4c55d 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -6,6 +6,7 @@ + {meta_tags} From 179340774891b662f81523eff5817ede74be4413 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 15 Sep 2019 22:12:03 +0200 Subject: [PATCH 112/483] Png and svg version of the logo --- src/Ui/media/img/logo.png | Bin 0 -> 11379 bytes src/Ui/media/img/logo.svg | 1 + 2 files changed, 1 insertion(+) create mode 100644 src/Ui/media/img/logo.png create mode 100644 src/Ui/media/img/logo.svg diff --git a/src/Ui/media/img/logo.png b/src/Ui/media/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a39cfc413525b04899e8137c7e24257fb8b10879 GIT binary patch literal 11379 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 literal 0 HcmV?d00001 diff --git a/src/Ui/media/img/logo.svg b/src/Ui/media/img/logo.svg new file mode 100644 index 00000000..fa79bfc8 --- /dev/null +++ b/src/Ui/media/img/logo.svg @@ -0,0 +1 @@ + From dbcd8602c5e4c8f3aa705553ad94246c1e8f229d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 15 Sep 2019 22:12:09 +0200 Subject: [PATCH 113/483] Rev4214 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index e0424898..e81111f0 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4210 + self.rev = 4214 self.argv = argv self.action = None self.pending_changes = {} From b474677db1ff06a237206a7aa43cc52eb6ffce98 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 15 Sep 2019 22:14:20 +0200 Subject: [PATCH 114/483] Remove pyelliptic from requirements because an OpenSSL 1.1 compatible version is bundled in the lib dir --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1173d695..0b7e70dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ gevent>=1.1.0 msgpack>=0.4.4 base58 merkletools -pyelliptic==1.5.6 rsa PySocks pyasn1 From 2fbf2c77714b280888db61c89752259448b7d111 Mon Sep 17 00:00:00 2001 From: Christian Seibold Date: Wed, 18 Sep 2019 12:49:53 -0500 Subject: [PATCH 115/483] English Grammar Fix: Change "Forgot" to "Forget" in Sidebar (#2202) * Change forgot to forget English grammar fix * Change forgot to forget Fix English grammar --- plugins/Sidebar/SidebarPlugin.py | 2 +- plugins/Sidebar/media/Sidebar.coffee | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index c4b3a4bb..b15c9136 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -482,7 +482,7 @@ class UiWebsocketPlugin(object): def sidebarRenderContents(self, body, site): has_privatekey = bool(self.user.getSiteData(site.address, create=False).get("privatekey")) if has_privatekey: - tag_privatekey = _("{_[Private key saved.]} {_[Forgot]}") + tag_privatekey = _("{_[Private key saved.]} {_[Forget]}") else: tag_privatekey = _("{_[Add saved private key]}") diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index 5a4111f5..b65912d4 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -204,15 +204,15 @@ class Sidebar extends Class return true } - # Save and forgot privatekey for site signing + # Save and forget privatekey for site signing @tag.find("#privatekey-add").off("click, touchend").on "click touchend", (e) => @wrapper.displayPrompt "Enter your private key:", "password", "Save", "", (privatekey) => @wrapper.ws.cmd "userSetSitePrivatekey", [privatekey], (res) => @wrapper.notifications.add "privatekey", "done", "Private key saved for site signing", 5000 return false - @tag.find("#privatekey-forgot").off("click, touchend").on "click touchend", (e) => - @wrapper.displayConfirm "Remove saved private key for this site?", "Forgot", (res) => + @tag.find("#privatekey-forget").off("click, touchend").on "click touchend", (e) => + @wrapper.displayConfirm "Remove saved private key for this site?", "Forget", (res) => if not res return false @wrapper.ws.cmd "userSetSitePrivatekey", [""], (res) => From 93e6ec4933b8d2a8a3fea4900a2e939d3985cb4d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Sep 2019 16:32:30 +0200 Subject: [PATCH 116/483] Fix display site add prompt --- src/Ui/UiRequest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index afaa4fec..c2d4d439 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -647,6 +647,7 @@ class UiRequest(object): SiteManager.site_manager.need(post["address"]) return self.actionRedirect(post["url"]) + @helper.encodeResponse def actionSiteAddPrompt(self, path): path_parts = self.parsePath(path) if not path_parts or not self.server.site_manager.isAddress(path_parts["address"]): From d7db631b9524e2ca2e5481721d86289dcd2732a5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Sep 2019 16:33:45 +0200 Subject: [PATCH 117/483] Shut down UiServer if FileServer startup failed --- src/Connection/ConnectionServer.py | 8 +++++++- src/File/FileServer.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index f81dc5f8..f2765d58 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -46,6 +46,7 @@ class ConnectionServer(object): self.stream_server = None self.stream_server_proxy = None self.running = False + self.stopping = False self.stat_recv = defaultdict(lambda: defaultdict(int)) self.stat_sent = defaultdict(lambda: defaultdict(int)) @@ -76,6 +77,8 @@ class ConnectionServer(object): self.handleRequest = request_handler def start(self, check_connections=True): + if self.stopping: + return False self.running = True if check_connections: self.thread_checker = gevent.spawn(self.checkConnections) @@ -99,16 +102,19 @@ class ConnectionServer(object): def listen(self): if not self.running: - return False + return None + if self.stream_server_proxy: gevent.spawn(self.listenProxy) try: self.stream_server.serve_forever() except Exception as err: self.log.info("StreamServer listen error: %s" % err) + return False def stop(self): self.log.debug("Stopping") + self.stopping = True self.running = False if self.stream_server: self.stream_server.stop() diff --git a/src/File/FileServer.py b/src/File/FileServer.py index 91b2b103..a6800f8f 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -2,6 +2,7 @@ import logging import time import random import socket +import sys import gevent import gevent.pool @@ -346,7 +347,20 @@ class FileServer(ConnectionServer): # Bind and start serving sites def start(self, check_sites=True): + if self.stopping: + return False + ConnectionServer.start(self) + + try: + self.stream_server.start() + except Exception as err: + self.log.error("Error listening on: %s:%s: %s" % (self.ip, self.port, err)) + if "ui_server" in dir(sys.modules["main"]): + self.log.debug("Stopping UI Server.") + sys.modules["main"].ui_server.stop() + return False + self.sites = self.site_manager.list() if config.debug: # Auto reload FileRequest on change From d06b4abecfc6b38cfad5cf8a97a80603787b5a06 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Sep 2019 16:38:05 +0200 Subject: [PATCH 118/483] Add multiuser admin status to server info --- plugins/disabled-Multiuser/MultiuserPlugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 8a8ee8f2..0be5b59e 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -114,6 +114,8 @@ class UiWebsocketPlugin(object): server_info["multiuser"] = True if "ADMIN" in self.site.settings["permissions"]: server_info["master_address"] = self.user.master_address + is_multiuser_admin = config.multiuser_local or self.user.master_address in local_master_addresses + server_info["multiuser_admin"] = is_multiuser_admin return server_info # Show current user's master seed From f5829f60122bd1ea25a92be142820091194e136d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Sep 2019 16:38:20 +0200 Subject: [PATCH 119/483] Rev4221 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index e81111f0..5e8e98b1 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4214 + self.rev = 4221 self.argv = argv self.action = None self.pending_changes = {} From b21b885aa97c236d32a98b21ea56c3f1350681d6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 28 Sep 2019 17:01:37 +0200 Subject: [PATCH 120/483] Move site add to separate function --- src/Site/SiteManager.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 8a880c9d..2de98dce 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -152,6 +152,27 @@ class SiteManager(object): return site + def add(self, address, all_file=False, settings=None): + from .Site import Site + self.sites_changed = int(time.time()) + # Try to find site with differect case + for recover_address, recover_site in list(self.sites.items()): + if recover_address.lower() == address.lower(): + return recover_site + + if not self.isAddress(address): + return False # Not address: %s % address + self.log.debug("Added new site: %s" % address) + config.loadTrackersFile() + site = Site(address, settings=settings) + self.sites[address] = site + if not site.settings["serving"]: # Maybe it was deleted before + site.settings["serving"] = True + site.saveSettings() + if all_file: # Also download user files on first sync + site.download(check_size=True, blind_includes=True) + return site + # Return or create site and start download site files def need(self, address, all_file=True, settings=None): if self.isDomain(address): @@ -159,27 +180,9 @@ class SiteManager(object): if address_resolved: address = address_resolved - from .Site import Site site = self.get(address) if not site: # Site not exist yet - self.sites_changed = int(time.time()) - # Try to find site with differect case - for recover_address, recover_site in list(self.sites.items()): - if recover_address.lower() == address.lower(): - return recover_site - - if not self.isAddress(address): - return False # Not address: %s % address - self.log.debug("Added new site: %s" % address) - config.loadTrackersFile() - site = Site(address, settings=settings) - self.sites[address] = site - if not site.settings["serving"]: # Maybe it was deleted before - site.settings["serving"] = True - site.saveSettings() - if all_file: # Also download user files on first sync - site.download(check_size=True, blind_includes=True) - + site = self.add(address, all_file=all_file, settings=settings) return site def delete(self, address): From 43c366d2fb1860711bb08c1869a9749da600ea5f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 28 Sep 2019 17:02:27 +0200 Subject: [PATCH 121/483] Restrict blocked site addition when using mergerSiteAdd --- plugins/ContentFilter/ContentFilterPlugin.py | 7 +++++++ plugins/ContentFilter/ContentFilterStorage.py | 6 ++++++ plugins/MergerSite/MergerSitePlugin.py | 7 +++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index f46321d4..da1054b1 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -25,6 +25,13 @@ class SiteManagerPlugin(object): super(SiteManagerPlugin, self).load(*args, **kwargs) filter_storage = ContentFilterStorage(site_manager=self) + def add(self, address, *args, **kwargs): + if filter_storage.isSiteblocked(address): + details = filter_storage.getSiteblockDetails(address) + raise Exception("Site blocked: %s" % html.escape(details.get("reason", "unknown reason"))) + else: + return super(SiteManagerPlugin, self).add(address, *args, **kwargs) + @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): diff --git a/plugins/ContentFilter/ContentFilterStorage.py b/plugins/ContentFilter/ContentFilterStorage.py index 3df0b435..2215ccca 100644 --- a/plugins/ContentFilter/ContentFilterStorage.py +++ b/plugins/ContentFilter/ContentFilterStorage.py @@ -120,6 +120,12 @@ class ContentFilterStorage(object): else: return False + def getSiteblockDetails(self, address): + details = self.file_content["siteblocks"].get(address) + if not details: + details = self.include_filters["siteblocks"].get(address) + return details + # Search and remove or readd files of an user def changeDbs(self, auth_address, action): self.log.debug("Mute action %s on user %s" % (action, auth_address)) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index ae2b1484..ca2ba31e 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -79,8 +79,11 @@ class UiWebsocketPlugin(object): def cbMergerSiteAdd(self, to, addresses): added = 0 for address in addresses: - added += 1 - site_manager.need(address) + try: + site_manager.need(address) + added += 1 + except Exception as err: + self.cmd("notification", ["error", _["Adding %s failed: %s"] % (address, err)]) if added: self.cmd("notification", ["done", _["Added %s new site"] % added, 5000]) RateLimit.called(self.site.address + "-MergerSiteAdd") From 3682f0aed49323dcc22cb9cf851a2fffc44dc535 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 28 Sep 2019 17:03:43 +0200 Subject: [PATCH 122/483] Wait for db close on tests --- src/Test/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 144256dd..3c6e0870 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -207,6 +207,7 @@ def site_temp(request): site_temp.storage.deleteFiles() site_temp.content_manager.contents.db.deleteSite(site_temp) site_temp.content_manager.contents.db.close() + time.sleep(0.01) # Wait for db close db_path = "%s-temp/content.db" % config.data_dir os.unlink(db_path) del ContentDb.content_dbs[db_path] From bb436f99311efe464e9ded4601e49d453c8fe746 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 28 Sep 2019 17:17:47 +0200 Subject: [PATCH 123/483] Rev4223 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 5e8e98b1..077219ad 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4221 + self.rev = 4223 self.argv = argv self.action = None self.pending_changes = {} From fee95654fa2574201ad4c6d6019e86cbc4fceda1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=A0?= Date: Fri, 24 May 2019 18:01:22 +0200 Subject: [PATCH 124/483] Create FUNDING.yml (cherry picked from commit f08bea7f904fdac1a22fa39d57b359f68a776cfc) --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..db8c40a5 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://zeronet.io/docs/help_zeronet/donate/ From 15fca6bd12a1304a7053270b758cfed94dd78296 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:05:39 +0200 Subject: [PATCH 125/483] User selection from a list in multiuser local mode --- plugins/disabled-Multiuser/MultiuserPlugin.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index 0be5b59e..ad27b4bc 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -8,7 +8,6 @@ from Crypt import CryptBitcoin from . import UserPlugin from util.Flag import flag - # We can only import plugin host clases after the plugins are loaded @PluginManager.afterLoad def importPluginnedClasses(): @@ -144,6 +143,52 @@ class UiWebsocketPlugin(object): else: self.response(to, "User not found") + @flag.admin + def actionUserSet(self, to, master_address): + user_manager = UserManager.user_manager + user = user_manager.get(master_address) + if not user: + raise Exception("No user found") + + script = "document.cookie = 'master_address=%s;path=/;max-age=2592000;';" % master_address + script += "zeroframe.cmd('wrapperReload', ['login=done']);" + self.cmd("notification", ["done", "Successful login, reloading page..."]) + self.cmd("injectScript", script) + + self.response(to, "ok") + + @flag.admin + def actionUserSelectForm(self, to): + if not config.multiuser_local: + raise Exception("Only allowed in multiuser local mode") + user_manager = UserManager.user_manager + body = "" + "Change account:" + "" + for master_address, user in user_manager.list().items(): + is_active = self.user.master_address == master_address + if user.certs: + first_cert = next(iter(user.certs.keys())) + title = "%s@%s" % (user.certs[first_cert]["auth_user_name"], first_cert) + else: + title = user.master_address + if len(user.sites) < 2 and not is_active: # Avoid listing ad-hoc created users + continue + if is_active: + css_class = "active" + else: + css_class = "noclass" + body += "%s" % (css_class, user.master_address, title) + + script = """ + $(".notification .select.user").on("click", function() { + $(".notification .select").removeClass('active') + zeroframe.response(%s, this.title) + return false + }) + """ % self.next_message_id + + self.cmd("notification", ["ask", body], lambda master_address: self.actionUserSet(to, master_address)) + self.cmd("injectScript", script) + # Show login form def actionUserLoginForm(self, to): self.cmd("prompt", ["Login
Your private key:", "password", "Login"], self.responseUserLogin) From fe432ad843d815e68de5d0edbd091d4bbd44d6c1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:07:14 +0200 Subject: [PATCH 126/483] Open console with #ZeroNet:Console hash in url --- plugins/Sidebar/media/Console.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/media/Console.coffee b/plugins/Sidebar/media/Console.coffee index c8aace1d..44510438 100644 --- a/plugins/Sidebar/media/Console.coffee +++ b/plugins/Sidebar/media/Console.coffee @@ -11,7 +11,11 @@ class Console extends Class else handleMessageWebsocket_original(message) - if window.top.location.hash == "#console" + $(window).on "hashchange", => + if window.top.location.hash == "#ZeroNet:Console" + @open() + + if window.top.location.hash == "#ZeroNet:Console" setTimeout (=> @open()), 10 createHtmltag: -> From 284b1a4f8a2f59e30e33b3062798819c5deb604e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:07:34 +0200 Subject: [PATCH 127/483] Console filters to Warning, Error --- plugins/Sidebar/media/Console.coffee | 47 +++++++++++++++++++++++++--- plugins/Sidebar/media/Console.css | 16 ++++++++-- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/plugins/Sidebar/media/Console.coffee b/plugins/Sidebar/media/Console.coffee index 44510438..4ef9a4fd 100644 --- a/plugins/Sidebar/media/Console.coffee +++ b/plugins/Sidebar/media/Console.coffee @@ -3,6 +3,14 @@ class Console extends Class @tag = null @opened = false @filter = null + @tab_types = [ + {title: "All", filter: ""}, + {title: "Info", filter: "INFO"}, + {title: "Warning", filter: "WARNING"}, + {title: "Error", filter: "ERROR"} + ] + @read_size = 32 * 1024 + @tab_active = "" #@filter = @sidebar.wrapper.site_info.address_short handleMessageWebsocket_original = @sidebar.wrapper.handleMessageWebsocket @sidebar.wrapper.handleMessageWebsocket = (message) => @@ -24,6 +32,7 @@ class Console extends Class
+
Loading...
@@ -37,6 +46,7 @@ class Console extends Class """) @text = @container.find(".console-text") @text_elem = @text[0] + @tabs = @container.find(".console-tabs") @text.on "mousewheel", (e) => # Stop animation on manual scrolling if e.originalEvent.deltaY < 0 @@ -47,6 +57,12 @@ class Console extends Class @container.appendTo(document.body) @tag = @container.find(".console") + for tab_type in @tab_types + tab = $("", {href: "#", "data-filter": tab_type.filter}).text(tab_type.title) + if tab_type.filter == @tab_active + tab.addClass("active") + tab.on("click", @handleTabClick) + @tabs.append(tab) @container.on "mousedown touchend touchcancel", (e) => if e.target != e.currentTarget @@ -98,26 +114,31 @@ class Console extends Class loadConsoleText: => - @sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter}, (res) => + @sidebar.wrapper.ws.cmd "consoleLogRead", {filter: @filter, read_size: @read_size}, (res) => @text.html("") pos_diff = res["pos_end"] - res["pos_start"] size_read = Math.round(pos_diff/1024) size_total = Math.round(res['pos_end']/1024) + @text.append("

") @text.append("Displaying #{res.lines.length} of #{res.num_found} lines found in the last #{size_read}kB of the log file. (#{size_total}kB total)
") @addLines res.lines, false @text_elem.scrollTop = @text_elem.scrollHeight + if @stream_id + @sidebar.wrapper.ws.cmd "consoleLogStreamRemove", {stream_id: @stream_id} @sidebar.wrapper.ws.cmd "consoleLogStream", {filter: @filter}, (res) => @stream_id = res.stream_id close: => + window.top.location.hash = "" @sidebar.move_lock = "y" @sidebar.startDrag() @sidebar.stopDrag() open: => - @createHtmltag() - @sidebar.fixbutton_targety = @sidebar.page_height - @stopDragY() + @sidebar.startDrag() + @sidebar.moved("y") + @sidebar.fixbutton_targety = @sidebar.page_height - @sidebar.fixbutton_inity - 50 + @sidebar.stopDrag() onOpened: => @sidebar.onClosed() @@ -157,4 +178,20 @@ class Console extends Class if not @opened @onClosed() -window.Console = Console \ No newline at end of file + changeFilter: (filter) => + @filter = filter + if @filter == "" + @read_size = 32 * 1024 + else + @read_size = 1024 * 1024 + @loadConsoleText() + + handleTabClick: (e) => + elem = $(e.currentTarget) + @tab_active = elem.data("filter") + $("a", @tabs).removeClass("active") + elem.addClass("active") + @changeFilter(@tab_active) + return false + +window.Console = Console diff --git a/plugins/Sidebar/media/Console.css b/plugins/Sidebar/media/Console.css index dc386d70..d81253dd 100644 --- a/plugins/Sidebar/media/Console.css +++ b/plugins/Sidebar/media/Console.css @@ -1,9 +1,19 @@ .console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; } .console-container .console { background-color: #212121; height: 100vh; transform: translateY(0px); padding-top: 80px; box-sizing: border-box; } -.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; padding: 5px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;} -.console-text { overflow-y: scroll; height: 100%; color: #DDD; } - +.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;} +.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; } +.console-tabs { + background-color: #41193fad; position: relative; margin-right: 15px; backdrop-filter: blur(2px); + box-shadow: 0px 0px 45px #7d2463; background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473; +} +.console-tabs a { + margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA; + font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666; + border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5); +} +.console-tabs a:hover { color: #FFF } +.console-tabs a.active { background-color: #46223c; color: #FFF } .console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; } .console .mynode { From 1b41aa70cce15e12596daa44c8a6005e43428a1d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:07:52 +0200 Subject: [PATCH 128/483] Don't mess with console visibility on Windows --- plugins/Trayicon/TrayiconPlugin.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index 794ced77..bae1e713 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -32,16 +32,10 @@ class ActionsPlugin(object): ) self.icon = icon - if not config.debug: # Hide console if not in debug mode - notificationicon.hideConsole() - self.console = False - else: - self.console = True + self.console = False @atexit.register def hideIcon(): - if not config.debug: - notificationicon.showConsole() icon.die() ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1" From d5da404ed4ad6cf00b89df9e51197695789ef705 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:08:09 +0200 Subject: [PATCH 129/483] Log zeroname db load error --- plugins/Zeroname/SiteManagerPlugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/Zeroname/SiteManagerPlugin.py b/plugins/Zeroname/SiteManagerPlugin.py index f03b7710..9c1d1445 100644 --- a/plugins/Zeroname/SiteManagerPlugin.py +++ b/plugins/Zeroname/SiteManagerPlugin.py @@ -13,7 +13,7 @@ log = logging.getLogger("ZeronamePlugin") @PluginManager.registerTo("SiteManager") class SiteManagerPlugin(object): site_zeroname = None - db_domains = None + db_domains = {} db_domains_modified = None def load(self, *args, **kwargs): @@ -36,7 +36,11 @@ class SiteManagerPlugin(object): if not self.db_domains or self.db_domains_modified != site_zeroname_modified: self.site_zeroname.needFile("data/names.json", priority=10) s = time.time() - self.db_domains = self.site_zeroname.storage.loadJson("data/names.json") + try: + self.db_domains = self.site_zeroname.storage.loadJson("data/names.json") + except Exception as err: + log.error("Error loading names.json: %s" % err) + log.debug( "Domain db with %s entries loaded in %.3fs (modification: %s -> %s)" % (len(self.db_domains), time.time() - s, self.db_domains_modified, site_zeroname_modified) From 1f9eafa619128d7f2da21d9ffde48d0a4f24d61e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:08:32 +0200 Subject: [PATCH 130/483] Merge sidebar js, css --- plugins/Sidebar/media/all.css | 24 +++++++--- plugins/Sidebar/media/all.js | 88 +++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 16 deletions(-) diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index 7e199620..d48199a2 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -1,13 +1,23 @@ -/* ---- plugins/Sidebar/media/Console.css ---- */ +/* ---- Console.css ---- */ .console-container { width: 100%; z-index: 998; position: absolute; top: -100vh; padding-bottom: 100%; } .console-container .console { background-color: #212121; height: 100vh; -webkit-transform: translateY(0px); -moz-transform: translateY(0px); -o-transform: translateY(0px); -ms-transform: translateY(0px); transform: translateY(0px) ; padding-top: 80px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; } -.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; padding: 5px; height: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; letter-spacing: 0.5px;} -.console-text { overflow-y: scroll; height: 100%; color: #DDD; } - +.console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; letter-spacing: 0.5px;} +.console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; } +.console-tabs { + background-color: #41193fad; position: relative; margin-right: 15px; backdrop-filter: blur(2px); + -webkit-box-shadow: 0px 0px 45px #7d2463; -moz-box-shadow: 0px 0px 45px #7d2463; -o-box-shadow: 0px 0px 45px #7d2463; -ms-box-shadow: 0px 0px 45px #7d2463; box-shadow: 0px 0px 45px #7d2463 ; background: -webkit-linear-gradient(-75deg, #591a48ed, #70305e66);background: -moz-linear-gradient(-75deg, #591a48ed, #70305e66);background: -o-linear-gradient(-75deg, #591a48ed, #70305e66);background: -ms-linear-gradient(-75deg, #591a48ed, #70305e66);background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473; +} +.console-tabs a { + margin-right: 5px; padding: 5px 15px; text-decoration: none; color: #AAA; + font-size: 11px; font-family: "Consolas"; text-transform: uppercase; border: 1px solid #666; + border-bottom: 0px; display: inline-block; margin: 5px; margin-bottom: 0px; background-color: rgba(0,0,0,0.5); +} +.console-tabs a:hover { color: #FFF } +.console-tabs a.active { background-color: #46223c; color: #FFF } .console-middle {height: 0px; top: 50%; position: absolute; width: 100%; left: 50%; display: none; } .console .mynode { @@ -25,7 +35,7 @@ } -/* ---- plugins/Sidebar/media/Menu.css ---- */ +/* ---- Menu.css ---- */ .menu { @@ -48,7 +58,7 @@ .menu, .menu.visible { position: absolute; left: unset !important; right: 20px; } } -/* ---- plugins/Sidebar/media/Scrollbable.css ---- */ +/* ---- Scrollbable.css ---- */ .scrollable { @@ -97,7 +107,7 @@ } -/* ---- plugins/Sidebar/media/Sidebar.css ---- */ +/* ---- Sidebar.css ---- */ .menu { diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 82810805..b3a12a31 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -70,6 +70,8 @@ function Console(sidebar) { var handleMessageWebsocket_original; this.sidebar = sidebar; + this.handleTabClick = bind(this.handleTabClick, this); + this.changeFilter = bind(this.changeFilter, this); this.stopDragY = bind(this.stopDragY, this); this.cleanup = bind(this.cleanup, this); this.onClosed = bind(this.onClosed, this); @@ -83,6 +85,23 @@ this.tag = null; this.opened = false; this.filter = null; + this.tab_types = [ + { + title: "All", + filter: "" + }, { + title: "Info", + filter: "INFO" + }, { + title: "Warning", + filter: "WARNING" + }, { + title: "Error", + filter: "ERROR" + } + ]; + this.read_size = 32 * 1024; + this.tab_active = ""; handleMessageWebsocket_original = this.sidebar.wrapper.handleMessageWebsocket; this.sidebar.wrapper.handleMessageWebsocket = (function(_this) { return function(message) { @@ -93,7 +112,14 @@ } }; })(this); - if (window.top.location.hash === "#console") { + $(window).on("hashchange", (function(_this) { + return function() { + if (window.top.location.hash === "#ZeroNet:console") { + return _this.open(); + } + }; + })(this)); + if (window.top.location.hash === "#ZeroNet:console") { setTimeout(((function(_this) { return function() { return _this.open(); @@ -103,10 +129,12 @@ } Console.prototype.createHtmltag = function() { + var j, len, ref, tab, tab_type; if (!this.container) { - this.container = $("
\n
\n
\n
Loading...
\n
\n
\n
\n \n
\n
\n
\n
"); + this.container = $("\n
"); this.text = this.container.find(".console-text"); this.text_elem = this.text[0]; + this.tabs = this.container.find(".console-tabs"); this.text.on("mousewheel", (function(_this) { return function(e) { if (e.originalEvent.deltaY < 0) { @@ -118,6 +146,19 @@ this.text.is_bottom = true; this.container.appendTo(document.body); this.tag = this.container.find(".console"); + ref = this.tab_types; + for (j = 0, len = ref.length; j < len; j++) { + tab_type = ref[j]; + tab = $("", { + href: "#", + "data-filter": tab_type.filter + }).text(tab_type.title); + if (tab_type.filter === this.tab_active) { + tab.addClass("active"); + } + tab.on("click", this.handleTabClick); + this.tabs.append(tab); + } this.container.on("mousedown touchend touchcancel", (function(_this) { return function(e) { if (e.target !== e.currentTarget) { @@ -193,7 +234,8 @@ Console.prototype.loadConsoleText = function() { this.sidebar.wrapper.ws.cmd("consoleLogRead", { - filter: this.filter + filter: this.filter, + read_size: this.read_size }, (function(_this) { return function(res) { var pos_diff, size_read, size_total; @@ -201,11 +243,17 @@ pos_diff = res["pos_end"] - res["pos_start"]; size_read = Math.round(pos_diff / 1024); size_total = Math.round(res['pos_end'] / 1024); + _this.text.append("

"); _this.text.append("Displaying " + res.lines.length + " of " + res.num_found + " lines found in the last " + size_read + "kB of the log file. (" + size_total + "kB total)
"); _this.addLines(res.lines, false); return _this.text_elem.scrollTop = _this.text_elem.scrollHeight; }; })(this)); + if (this.stream_id) { + this.sidebar.wrapper.ws.cmd("consoleLogStreamRemove", { + stream_id: this.stream_id + }); + } return this.sidebar.wrapper.ws.cmd("consoleLogStream", { filter: this.filter }, (function(_this) { @@ -216,15 +264,17 @@ }; Console.prototype.close = function() { + window.top.location.hash = ""; this.sidebar.move_lock = "y"; this.sidebar.startDrag(); return this.sidebar.stopDrag(); }; Console.prototype.open = function() { - this.createHtmltag(); - this.sidebar.fixbutton_targety = this.sidebar.page_height; - return this.stopDragY(); + this.sidebar.startDrag(); + this.sidebar.moved("y"); + this.sidebar.fixbutton_targety = this.sidebar.page_height - this.sidebar.fixbutton_inity - 50; + return this.sidebar.stopDrag(); }; Console.prototype.onOpened = function() { @@ -275,6 +325,26 @@ } }; + Console.prototype.changeFilter = function(filter) { + this.filter = filter; + if (this.filter === "") { + this.read_size = 32 * 1024; + } else { + this.read_size = 1024 * 1024; + } + return this.loadConsoleText(); + }; + + Console.prototype.handleTabClick = function(e) { + var elem; + elem = $(e.currentTarget); + this.tab_active = elem.data("filter"); + $("a", this.tabs).removeClass("active"); + elem.addClass("active"); + this.changeFilter(this.tab_active); + return false; + }; + return Console; })(Class); @@ -283,6 +353,7 @@ }).call(this); + /* ---- Menu.coffee ---- */ @@ -530,9 +601,9 @@ window.initScrollable = function () { this.globe = null; this.preload_html = null; this.original_set_site_info = this.wrapper.setSiteInfo; - if (window.top.location.hash === "#ZeroNet:OpenSidebar") { + if (false) { this.startDrag(); - this.moved("x"); + this.moved(); this.fixbutton_targetx = this.fixbutton_initx - this.width; this.stopDrag(); } @@ -1274,7 +1345,6 @@ window.initScrollable = function () { }).call(this); - /* ---- morphdom.js ---- */ From 6eb79ba75eed6137516bfd77151b2b772cb93e90 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:08:54 +0200 Subject: [PATCH 131/483] Don't annunce site if not serving --- src/Site/Site.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index af6563e3..59b5745f 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -818,7 +818,8 @@ class Site(object): return peer def announce(self, *args, **kwargs): - self.announcer.announce(*args, **kwargs) + if self.isServing(): + self.announcer.announce(*args, **kwargs) # Keep connections to get the updates def needConnections(self, num=None, check_site_on_reconnect=False): From 119e1a9bf070738743f6ec416e5abfdf8446cf0b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:09:48 +0200 Subject: [PATCH 132/483] Simple cache decorator --- src/Test/TestCached.py | 59 ++++++++++++++++++++++++++++++++++++ src/util/Cached.py | 68 ++++++++++++++++++++++++++++++++++++++++++ src/util/__init__.py | 1 + 3 files changed, 128 insertions(+) create mode 100644 src/Test/TestCached.py create mode 100644 src/util/Cached.py diff --git a/src/Test/TestCached.py b/src/Test/TestCached.py new file mode 100644 index 00000000..088962c0 --- /dev/null +++ b/src/Test/TestCached.py @@ -0,0 +1,59 @@ +import time + +from util import Cached + + +class CachedObject: + def __init__(self): + self.num_called_add = 0 + self.num_called_multiply = 0 + self.num_called_none = 0 + + @Cached(timeout=1) + def calcAdd(self, a, b): + self.num_called_add += 1 + return a + b + + @Cached(timeout=1) + def calcMultiply(self, a, b): + self.num_called_multiply += 1 + return a * b + + @Cached(timeout=1) + def none(self): + self.num_called_none += 1 + return None + + +class TestCached: + def testNoneValue(self): + cached_object = CachedObject() + assert cached_object.none() is None + assert cached_object.none() is None + assert cached_object.num_called_none == 1 + time.sleep(2) + assert cached_object.none() is None + assert cached_object.num_called_none == 2 + + def testCall(self): + cached_object = CachedObject() + + assert cached_object.calcAdd(1, 2) == 3 + assert cached_object.calcAdd(1, 2) == 3 + assert cached_object.calcMultiply(1, 2) == 2 + assert cached_object.calcMultiply(1, 2) == 2 + assert cached_object.num_called_add == 1 + assert cached_object.num_called_multiply == 1 + + assert cached_object.calcAdd(2, 3) == 5 + assert cached_object.calcAdd(2, 3) == 5 + assert cached_object.num_called_add == 2 + + assert cached_object.calcAdd(1, 2) == 3 + assert cached_object.calcMultiply(2, 3) == 6 + assert cached_object.num_called_add == 2 + assert cached_object.num_called_multiply == 2 + + time.sleep(2) + assert cached_object.calcAdd(1, 2) == 3 + assert cached_object.num_called_add == 3 diff --git a/src/util/Cached.py b/src/util/Cached.py new file mode 100644 index 00000000..72d60dbc --- /dev/null +++ b/src/util/Cached.py @@ -0,0 +1,68 @@ +import time + + +class Cached(object): + def __init__(self, timeout): + self.cache_db = {} + self.timeout = timeout + + def __call__(self, func): + def wrapper(*args, **kwargs): + key = "%s %s" % (args, kwargs) + cached_value = None + cache_hit = False + if key in self.cache_db: + cache_hit = True + cached_value, time_cached_end = self.cache_db[key] + if time.time() > time_cached_end: + self.cleanupExpired() + cached_value = None + cache_hit = False + + if cache_hit: + return cached_value + else: + cached_value = func(*args, **kwargs) + time_cached_end = time.time() + self.timeout + self.cache_db[key] = (cached_value, time_cached_end) + return cached_value + + wrapper.emptyCache = self.emptyCache + + return wrapper + + def cleanupExpired(self): + for key in list(self.cache_db.keys()): + cached_value, time_cached_end = self.cache_db[key] + if time.time() > time_cached_end: + del(self.cache_db[key]) + + def emptyCache(self): + num = len(self.cache_db) + self.cache_db.clear() + return num + + +if __name__ == "__main__": + from gevent import monkey + monkey.patch_all() + + @Cached(timeout=2) + def calcAdd(a, b): + print("CalcAdd", a, b) + return a + b + + @Cached(timeout=1) + def calcMultiply(a, b): + print("calcMultiply", a, b) + return a * b + + for i in range(5): + print("---") + print("Emptied", calcAdd.emptyCache()) + assert calcAdd(1, 2) == 3 + print("Emptied", calcAdd.emptyCache()) + assert calcAdd(1, 2) == 3 + assert calcAdd(2, 3) == 5 + assert calcMultiply(2, 3) == 6 + time.sleep(1) diff --git a/src/util/__init__.py b/src/util/__init__.py index 7cf8ecf7..ab8a8b88 100644 --- a/src/util/__init__.py +++ b/src/util/__init__.py @@ -1,3 +1,4 @@ +from .Cached import Cached from .Event import Event from .Noparallel import Noparallel from .Pooled import Pooled From 917a2e59ce45dafcc78cc77edd617298a637d972 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:10:20 +0200 Subject: [PATCH 133/483] Fix compacting large json files --- src/util/helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/helper.py b/src/util/helper.py index 8e0f285f..317c5eed 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -49,7 +49,7 @@ def jsonDumps(data): else: return match.group(0) - content = re.sub(r"\{(\n[^,\[\{]{10,100}?)\}[, ]{0,2}\n", compact_dict, content, flags=re.DOTALL) + content = re.sub(r"\{(\n[^,\[\{]{10,100000}?)\}[, ]{0,2}\n", compact_dict, content, flags=re.DOTALL) def compact_list(match): if "\n" in match.group(0): @@ -58,7 +58,7 @@ def jsonDumps(data): else: return match.group(0) - content = re.sub(r"\[([^\[\{]{2,300}?)\][, ]{0,2}\n", compact_list, content, flags=re.DOTALL) + content = re.sub(r"\[([^\[\{]{2,100000}?)\][, ]{0,2}\n", compact_list, content, flags=re.DOTALL) # Remove end of line whitespace content = re.sub(r"(?m)[ ]+$", "", content) From 73e0aa17c4458272e24b2b97cf1e931858ad787f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:10:43 +0200 Subject: [PATCH 134/483] Don't encode byte responses --- src/util/helper.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/util/helper.py b/src/util/helper.py index 317c5eed..baaf313e 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -340,8 +340,14 @@ def encodeResponse(func): back = func(*args, **kwargs) if "__next__" in dir(back): for part in back: - yield part.encode() + if type(part) == bytes: + yield part + else: + yield part.encode() else: - yield back.encode() + if type(back) == bytes: + yield back + else: + yield back.encode() return wrapper From 29640e614c6c839d0ba6dbdccab729164bab5acb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:12:47 +0200 Subject: [PATCH 135/483] Admin API call to list server errors --- src/Ui/UiServer.py | 24 +++++++++++++++++++++--- src/Ui/UiWebsocket.py | 9 ++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index e8cdd545..4bf41b58 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -2,7 +2,6 @@ import logging import time import cgi import socket -import sys import gevent from gevent.pywsgi import WSGIServer @@ -16,6 +15,17 @@ from Debug import Debug import importlib +class LogDb(logging.StreamHandler): + def __init__(self, ui_server): + self.lines = [] + self.ui_server = ui_server + return super(LogDb, self).__init__() + + def emit(self, record): + self.ui_server.updateWebsocket(log_event=record.levelname) + self.lines.append([time.time(), record.levelname, self.format(record)]) + + # Skip websocket handler if not necessary class UiWSGIHandler(WSGIHandler): @@ -53,7 +63,6 @@ class UiWSGIHandler(WSGIHandler): class UiServer: - def __init__(self): self.ip = config.ui_ip self.port = config.ui_port @@ -86,6 +95,10 @@ class UiServer: self.sites = SiteManager.site_manager.list() self.log = logging.getLogger(__name__) + self.logdb_errors = LogDb(ui_server=self) + self.logdb_errors.setLevel(logging.getLevelName("ERROR")) + logging.getLogger('').addHandler(self.logdb_errors) + # After WebUI started def afterStarted(self): from util import Platform @@ -196,5 +209,10 @@ class UiServer: time.sleep(1) def updateWebsocket(self, **kwargs): + if kwargs: + param = {"event": list(kwargs.items())[0]} + else: + param = None + for ws in self.websockets: - ws.event("serverChanged", kwargs) + ws.event("serverChanged", param) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index 6d6559f2..9a245377 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -139,6 +139,8 @@ class UiWebsocket(object): self.cmd("setSiteInfo", site_info) elif channel == "serverChanged": server_info = self.formatServerInfo() + if len(params) > 0 and params[0]: # Extra data + server_info.update(params[0]) self.cmd("setServerInfo", server_info) elif channel == "announcerChanged": site = params[0] @@ -1101,6 +1103,11 @@ class UiWebsocket(object): self.user.save() self.response(to, "ok") + @flag.admin + @flag.no_multiuser + def actionServerErrors(self, to): + return self.server.logdb_errors.lines + @flag.admin @flag.no_multiuser def actionServerUpdate(self, to): @@ -1227,4 +1234,4 @@ class UiWebsocket(object): else: gevent.spawn(main.file_server.checkSites, check_files=False, force_port_check=True) - self.response(to, "ok") + self.response(to, "ok") \ No newline at end of file From dd493c87fa561cbbd3bca52b3455ad8db881cc58 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:13:32 +0200 Subject: [PATCH 136/483] Display WSGI errors to the browser --- src/Ui/UiServer.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 4bf41b58..d2c40f17 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -35,6 +35,16 @@ class UiWSGIHandler(WSGIHandler): self.args = args self.kwargs = kwargs + def handleError(self, err): + if config.debug: # Allow websocket errors to appear on /Debug + import main + main.DebugHook.handleError() + else: + ui_request = UiRequest(self.server, {}, self.environ, self.start_response) + block_gen = ui_request.error500("UiWSGIHandler error: %s" % Debug.formatExceptionMessage(err)) + for block in block_gen: + self.write(block) + def run_application(self): if "HTTP_UPGRADE" in self.environ: # Websocket request try: @@ -43,17 +53,13 @@ class UiWSGIHandler(WSGIHandler): ws_handler.run_application() except Exception as err: logging.error("UiWSGIHandler websocket error: %s" % Debug.formatException(err)) - if config.debug: # Allow websocket errors to appear on /Debug - import main - main.DebugHook.handleError() + self.handleError(err) else: # Standard HTTP request try: super(UiWSGIHandler, self).run_application() except Exception as err: logging.error("UiWSGIHandler error: %s" % Debug.formatException(err)) - if config.debug: # Allow websocket errors to appear on /Debug - import main - main.DebugHook.handleError() + self.handleError(err) def handle(self): # Save socket to be able to close them properly on exit From ead1b3e5f5f61fa8539555b3d93c787a7eaacc32 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:14:45 +0200 Subject: [PATCH 137/483] Log 403 as warning --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index c2d4d439..7efeef22 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -831,7 +831,7 @@ class UiRequest(object): # You are not allowed to access this def error403(self, message="", details=True): self.sendHeader(403, noscript=True) - self.log.error("Error 403: %s" % message) + self.log.warning("Error 403: %s" % message) return self.formatError("Forbidden", message, details=details) # Send file not found error From 0598bcf3321ba0ecfbcd70d09883bb6f89902e31 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:15:20 +0200 Subject: [PATCH 138/483] Fix utf8 post data parsing --- src/Ui/UiRequest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 7efeef22..7b8c7f67 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -640,7 +640,8 @@ class UiRequest(object): return self.error400() def actionSiteAdd(self): - post = dict(urllib.parse.parse_qsl(self.env["wsgi.input"].read())) + post_data = self.env["wsgi.input"].read().decode() + post = dict(urllib.parse.parse_qsl(post_data)) if post["add_nonce"] not in self.server.add_nonces: return self.error403("Add nonce error.") self.server.add_nonces.remove(post["add_nonce"]) From 9dd5c88da466c48db0f8cee74d6e6fb68393fcd5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:15:57 +0200 Subject: [PATCH 139/483] Monospace font when displaying errors --- src/Ui/UiRequest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 7b8c7f67..9f14882e 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -872,6 +872,9 @@ class UiRequest(object): """ % (title, html.escape(message), html.escape(json.dumps(details, indent=4, sort_keys=True))) else: return """ +

%s

%s

""" % (title, html.escape(message)) From 924a61309a351fa3e56309fcc75d6cb4f5e34c02 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:18:14 +0200 Subject: [PATCH 140/483] Cached isDomain / resolveDomain functions --- plugins/ContentFilter/ContentFilterPlugin.py | 4 ++-- src/Site/SiteManager.py | 17 +++++++++++++---- src/Ui/UiRequest.py | 12 +++++++++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index da1054b1..4d5d1b4c 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -198,8 +198,8 @@ class UiRequestPlugin(object): if self.server.site_manager.get(address): # Site already exists return super(UiRequestPlugin, self).actionWrapper(path, extra_headers) - if self.server.site_manager.isDomain(address): - address = self.server.site_manager.resolveDomain(address) + if self.isDomain(address): + address = self.resolveDomain(address) if address: address_sha256 = "0x" + hashlib.sha256(address.encode("utf8")).hexdigest() diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 2de98dce..9c33d8b7 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -12,6 +12,7 @@ from Content import ContentDb from Config import config from util import helper from util import RateLimit +from util import Cached @PluginManager.acceptPlugins @@ -135,13 +136,21 @@ class SiteManager(object): def isDomain(self, address): return False + @Cached(timeout=10) + def isDomainCached(self, address): + return self.isDomain(address) + def resolveDomain(self, domain): return False + @Cached(timeout=10) + def resolveDomainCached(self, domain): + return self.resolveDomain(domain) + # Return: Site object or None if not found def get(self, address): - if self.isDomain(address): - address_resolved = self.resolveDomain(address) + if self.isDomainCached(address): + address_resolved = self.resolveDomainCached(address) if address_resolved: address = address_resolved @@ -175,8 +184,8 @@ class SiteManager(object): # Return or create site and start download site files def need(self, address, all_file=True, settings=None): - if self.isDomain(address): - address_resolved = self.resolveDomain(address) + if self.isDomainCached(address): + address_resolved = self.resolveDomainCached(address) if address_resolved: address = address_resolved diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 9f14882e..2aa87af2 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -69,13 +69,19 @@ class UiRequest(object): return True if self.isProxyRequest(): # Support for chrome extension proxy - if self.server.site_manager.isDomain(host): + if self.isDomain(host): return True else: return False return False + def isDomain(self, address): + return self.server.site_manager.isDomainCached(address) + + def resolveDomain(self, domain): + return self.server.site_manager.resolveDomainCached(domain) + # Call the request handler function base on path def route(self, path): # Restict Ui access by ip @@ -96,7 +102,7 @@ class UiRequest(object): return iter([ret_error, ret_link]) # Prepend .bit host for transparent proxy - if self.server.site_manager.isDomain(self.env.get("HTTP_HOST")): + if self.isDomain(self.env.get("HTTP_HOST")): path = re.sub("^/", "/" + self.env.get("HTTP_HOST") + "/", path) path = re.sub("^http://zero[/]+", "/", path) # Remove begining http://zero/ for chrome extension path = re.sub("^http://", "/", path) # Remove begining http for chrome extension .bit access @@ -173,7 +179,7 @@ class UiRequest(object): # The request is proxied by chrome extension or a transparent proxy def isProxyRequest(self): - return self.env["PATH_INFO"].startswith("http://") or (self.server.allow_trans_proxy and self.server.site_manager.isDomain(self.env.get("HTTP_HOST"))) + return self.env["PATH_INFO"].startswith("http://") or (self.server.allow_trans_proxy and self.isDomain(self.env.get("HTTP_HOST"))) def isWebSocketRequest(self): return self.env.get("HTTP_UPGRADE") == "websocket" From 43a574225885c2d839e116b5a07ac0fb214048f9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:20:16 +0200 Subject: [PATCH 141/483] Resolve domain in parsePath function --- plugins/Zeroname/UiRequestPlugin.py | 10 ---------- src/Ui/UiRequest.py | 4 ++++ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/plugins/Zeroname/UiRequestPlugin.py b/plugins/Zeroname/UiRequestPlugin.py index b0230524..eacb0aa0 100644 --- a/plugins/Zeroname/UiRequestPlugin.py +++ b/plugins/Zeroname/UiRequestPlugin.py @@ -11,16 +11,6 @@ class UiRequestPlugin(object): self.site_manager = SiteManager.site_manager super(UiRequestPlugin, self).__init__(*args, **kwargs) - # Media request - def actionSiteMedia(self, path, **kwargs): - match = re.match(r"/media/(?P
[A-Za-z0-9-]+\.[A-Za-z0-9\.-]+)(?P/.*|$)", path) - if match: # Its a valid domain, resolve first - domain = match.group("address") - address = self.site_manager.resolveDomain(domain) - if address: - path = "/media/" + address + match.group("inner_path") - return super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) # Get the wrapper frame output - @PluginManager.registerTo("ConfigPlugin") class ConfigPlugin(object): def createArguments(self): diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 2aa87af2..fa3719e5 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -310,6 +310,7 @@ class UiRequest(object): # Renders a template def render(self, template_path, *args, **kwargs): template = open(template_path, encoding="utf8").read() + def renderReplacer(m): return "%s" % kwargs.get(m.group(1), "") @@ -559,6 +560,8 @@ class UiRequest(object): match = re.match(r"/media/(?P
[A-Za-z0-9]+[A-Za-z0-9\._-]+)(?P/.*|$)", path) if match: path_parts = match.groupdict() + if self.isDomain(path_parts["address"]): + path_parts["address"] = self.resolveDomain(path_parts["address"]) path_parts["request_address"] = path_parts["address"] # Original request address (for Merger sites) path_parts["inner_path"] = path_parts["inner_path"].lstrip("/") if not path_parts["inner_path"]: @@ -578,6 +581,7 @@ class UiRequest(object): return self.error404(path) address = path_parts["address"] + file_path = "%s/%s/%s" % (config.data_dir, address, path_parts["inner_path"]) if (config.debug or config.merge_media) and file_path.split("/")[-1].startswith("all."): From 344ad44854a6e9e1e5197854aa791a13ca1a6b3d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 6 Oct 2019 03:20:58 +0200 Subject: [PATCH 142/483] Fix reload if there is hash in the url --- src/Ui/media/Wrapper.coffee | 7 ++++--- src/Ui/media/all.js | 40 +++++++++++++++++++------------------ 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index e57a1868..74232512 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -417,11 +417,12 @@ class Wrapper @reload(message.params[0]) reload: (url_post="") -> + current_url = window.location.toString().replace(/#.*/g, "") if url_post - if window.location.toString().indexOf("?") > 0 - window.location += "&"+url_post + if current_url.indexOf("?") > 0 + window.location = current_url + "&" + url_post else - window.location += "?"+url_post + window.location = current_url + "?" + url_post else window.location.reload() diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index e41d2476..1186c7e1 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -1,12 +1,12 @@ -/* ---- src/Ui/media/lib/00-jquery.min.js ---- */ +/* ---- lib/00-jquery.min.js ---- */ /*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */ !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/\s*$/g;function Le(e,t){return N(e,"table")&&N(11!==t.nodeType?t:t.firstChild,"tr")?w(e).children("tbody")[0]||e:e}function He(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Oe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Pe(e,t){var n,r,i,o,a,s,u,l;if(1===t.nodeType){if(J.hasData(e)&&(o=J.access(e),a=J.set(t,o),l=o.events)){delete a.handle,a.events={};for(i in l)for(n=0,r=l[i].length;n1&&"string"==typeof y&&!h.checkClone&&je.test(y))return e.each(function(i){var o=e.eq(i);v&&(t[0]=y.call(this,i,o.html())),Re(o,t,n,r)});if(p&&(i=xe(t,e[0].ownerDocument,!1,e,r),o=i.firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=w.map(ye(i,"script"),He)).length;f")},clone:function(e,t,n){var r,i,o,a,s=e.cloneNode(!0),u=w.contains(e.ownerDocument,e);if(!(h.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||w.isXMLDoc(e)))for(a=ye(s),r=0,i=(o=ye(e)).length;r0&&ve(a,!u&&ye(e,"script")),s},cleanData:function(e){for(var t,n,r,i=w.event.special,o=0;void 0!==(n=e[o]);o++)if(Y(n)){if(t=n[J.expando]){if(t.events)for(r in t.events)i[r]?w.event.remove(n,r):w.removeEvent(n,r,t.handle);n[J.expando]=void 0}n[K.expando]&&(n[K.expando]=void 0)}}}),w.fn.extend({detach:function(e){return Ie(this,e,!0)},remove:function(e){return Ie(this,e)},text:function(e){return z(this,function(e){return void 0===e?w.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)})},null,e,arguments.length)},append:function(){return Re(this,arguments,function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Le(this,e).appendChild(e)})},prepend:function(){return Re(this,arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Le(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return Re(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(w.cleanData(ye(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map(function(){return w.clone(this,e,t)})},html:function(e){return z(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ae.test(e)&&!ge[(de.exec(e)||["",""])[1].toLowerCase()]){e=w.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))),u}function et(e,t,n){var r=$e(e),i=Fe(e,t,r),o="border-box"===w.css(e,"boxSizing",!1,r),a=o;if(We.test(i)){if(!n)return i;i="auto"}return a=a&&(h.boxSizingReliable()||i===e.style[t]),("auto"===i||!parseFloat(i)&&"inline"===w.css(e,"display",!1,r))&&(i=e["offset"+t[0].toUpperCase()+t.slice(1)],a=!0),(i=parseFloat(i)||0)+Ze(e,t,n||(o?"border":"content"),a,r,i)+"px"}w.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Fe(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=G(t),u=Xe.test(t),l=e.style;if(u||(t=Je(s)),a=w.cssHooks[t]||w.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];"string"==(o=typeof n)&&(i=ie.exec(n))&&i[1]&&(n=ue(e,t,i),o="number"),null!=n&&n===n&&("number"===o&&(n+=i&&i[3]||(w.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),a&&"set"in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=G(t);return Xe.test(t)||(t=Je(s)),(a=w.cssHooks[t]||w.cssHooks[s])&&"get"in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Fe(e,t,r)),"normal"===i&&t in Ve&&(i=Ve[t]),""===n||n?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),w.each(["height","width"],function(e,t){w.cssHooks[t]={get:function(e,n,r){if(n)return!ze.test(w.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?et(e,t,r):se(e,Ue,function(){return et(e,t,r)})},set:function(e,n,r){var i,o=$e(e),a="border-box"===w.css(e,"boxSizing",!1,o),s=r&&Ze(e,t,r,a,o);return a&&h.scrollboxSize()===o.position&&(s-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(o[t])-Ze(e,t,"border",!1,o)-.5)),s&&(i=ie.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=w.css(e,t)),Ke(e,n,s)}}}),w.cssHooks.marginLeft=_e(h.reliableMarginLeft,function(e,t){if(t)return(parseFloat(Fe(e,"marginLeft"))||e.getBoundingClientRect().left-se(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),w.each({margin:"",padding:"",border:"Width"},function(e,t){w.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+oe[r]+t]=o[r]||o[r-2]||o[0];return i}},"margin"!==e&&(w.cssHooks[e+t].set=Ke)}),w.fn.extend({css:function(e,t){return z(this,function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=$e(e),i=t.length;a1)}});function tt(e,t,n,r,i){return new tt.prototype.init(e,t,n,r,i)}w.Tween=tt,tt.prototype={constructor:tt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||w.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(w.cssNumber[n]?"":"px")},cur:function(){var e=tt.propHooks[this.prop];return e&&e.get?e.get(this):tt.propHooks._default.get(this)},run:function(e){var t,n=tt.propHooks[this.prop];return this.options.duration?this.pos=t=w.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):tt.propHooks._default.set(this),this}},tt.prototype.init.prototype=tt.prototype,tt.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=w.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){w.fx.step[e.prop]?w.fx.step[e.prop](e):1!==e.elem.nodeType||null==e.elem.style[w.cssProps[e.prop]]&&!w.cssHooks[e.prop]?e.elem[e.prop]=e.now:w.style(e.elem,e.prop,e.now+e.unit)}}},tt.propHooks.scrollTop=tt.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},w.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},w.fx=tt.prototype.init,w.fx.step={};var nt,rt,it=/^(?:toggle|show|hide)$/,ot=/queueHooks$/;function at(){rt&&(!1===r.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,w.fx.interval),w.fx.tick())}function st(){return e.setTimeout(function(){nt=void 0}),nt=Date.now()}function ut(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=oe[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function lt(e,t,n){for(var r,i=(pt.tweeners[t]||[]).concat(pt.tweeners["*"]),o=0,a=i.length;o1)},removeAttr:function(e){return this.each(function(){w.removeAttr(this,e)})}}),w.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return"undefined"==typeof e.getAttribute?w.prop(e,t,n):(1===o&&w.isXMLDoc(e)||(i=w.attrHooks[t.toLowerCase()]||(w.expr.match.bool.test(t)?dt:void 0)),void 0!==n?null===n?void w.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=w.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&N(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(M);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),dt={set:function(e,t,n){return!1===t?w.removeAttr(e,n):e.setAttribute(n,n),n}},w.each(w.expr.match.bool.source.match(/\w+/g),function(e,t){var n=ht[t]||w.find.attr;ht[t]=function(e,t,r){var i,o,a=t.toLowerCase();return r||(o=ht[a],ht[a]=i,i=null!=n(e,t,r)?a:null,ht[a]=o),i}});var gt=/^(?:input|select|textarea|button)$/i,yt=/^(?:a|area)$/i;w.fn.extend({prop:function(e,t){return z(this,w.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[w.propFix[e]||e]})}}),w.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return 1===o&&w.isXMLDoc(e)||(t=w.propFix[t]||t,i=w.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=w.find.attr(e,"tabindex");return t?parseInt(t,10):gt.test(e.nodeName)||yt.test(e.nodeName)&&e.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),h.optSelected||(w.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),w.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){w.propFix[this.toLowerCase()]=this});function vt(e){return(e.match(M)||[]).join(" ")}function mt(e){return e.getAttribute&&e.getAttribute("class")||""}function xt(e){return Array.isArray(e)?e:"string"==typeof e?e.match(M)||[]:[]}w.fn.extend({addClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).addClass(e.call(this,t,mt(this)))});if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])r.indexOf(" "+o+" ")<0&&(r+=o+" ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,o,a,s,u=0;if(g(e))return this.each(function(t){w(this).removeClass(e.call(this,t,mt(this)))});if(!arguments.length)return this.attr("class","");if((t=xt(e)).length)while(n=this[u++])if(i=mt(n),r=1===n.nodeType&&" "+vt(i)+" "){a=0;while(o=t[a++])while(r.indexOf(" "+o+" ")>-1)r=r.replace(" "+o+" "," ");i!==(s=vt(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):g(e)?this.each(function(n){w(this).toggleClass(e.call(this,n,mt(this),t),t)}):this.each(function(){var t,i,o,a;if(r){i=0,o=w(this),a=xt(e);while(t=a[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else void 0!==e&&"boolean"!==n||((t=mt(this))&&J.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":J.get(this,"__className__")||""))})},hasClass:function(e){var t,n,r=0;t=" "+e+" ";while(n=this[r++])if(1===n.nodeType&&(" "+vt(mt(n))+" ").indexOf(t)>-1)return!0;return!1}});var bt=/\r/g;w.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=g(e),this.each(function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,w(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=w.map(i,function(e){return null==e?"":e+""})),(t=w.valHooks[this.type]||w.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return(t=w.valHooks[i.type]||w.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(bt,""):null==n?"":n}}}),w.extend({valHooks:{option:{get:function(e){var t=w.find.attr(e,"value");return null!=t?t:vt(w.text(e))}},select:{get:function(e){var t,n,r,i=e.options,o=e.selectedIndex,a="select-one"===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),w.each(["radio","checkbox"],function(){w.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=w.inArray(w(e).val(),t)>-1}},h.checkOn||(w.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})}),h.focusin="onfocusin"in e;var wt=/^(?:focusinfocus|focusoutblur)$/,Tt=function(e){e.stopPropagation()};w.extend(w.event,{trigger:function(t,n,i,o){var a,s,u,l,c,p,d,h,v=[i||r],m=f.call(t,"type")?t.type:t,x=f.call(t,"namespace")?t.namespace.split("."):[];if(s=h=u=i=i||r,3!==i.nodeType&&8!==i.nodeType&&!wt.test(m+w.event.triggered)&&(m.indexOf(".")>-1&&(m=(x=m.split(".")).shift(),x.sort()),c=m.indexOf(":")<0&&"on"+m,t=t[w.expando]?t:new w.Event(m,"object"==typeof t&&t),t.isTrigger=o?2:3,t.namespace=x.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+x.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),n=null==n?[t]:w.makeArray(n,[t]),d=w.event.special[m]||{},o||!d.trigger||!1!==d.trigger.apply(i,n))){if(!o&&!d.noBubble&&!y(i)){for(l=d.delegateType||m,wt.test(l+m)||(s=s.parentNode);s;s=s.parentNode)v.push(s),u=s;u===(i.ownerDocument||r)&&v.push(u.defaultView||u.parentWindow||e)}a=0;while((s=v[a++])&&!t.isPropagationStopped())h=s,t.type=a>1?l:d.bindType||m,(p=(J.get(s,"events")||{})[t.type]&&J.get(s,"handle"))&&p.apply(s,n),(p=c&&s[c])&&p.apply&&Y(s)&&(t.result=p.apply(s,n),!1===t.result&&t.preventDefault());return t.type=m,o||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(v.pop(),n)||!Y(i)||c&&g(i[m])&&!y(i)&&((u=i[c])&&(i[c]=null),w.event.triggered=m,t.isPropagationStopped()&&h.addEventListener(m,Tt),i[m](),t.isPropagationStopped()&&h.removeEventListener(m,Tt),w.event.triggered=void 0,u&&(i[c]=u)),t.result}},simulate:function(e,t,n){var r=w.extend(new w.Event,n,{type:e,isSimulated:!0});w.event.trigger(r,null,t)}}),w.fn.extend({trigger:function(e,t){return this.each(function(){w.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return w.event.trigger(e,t,n,!0)}}),h.focusin||w.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){w.event.simulate(t,e.target,w.event.fix(e))};w.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=J.access(r,t);i||r.addEventListener(e,n,!0),J.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=J.access(r,t)-1;i?J.access(r,t,i):(r.removeEventListener(e,n,!0),J.remove(r,t))}}});var Ct=e.location,Et=Date.now(),kt=/\?/;w.parseXML=function(t){var n;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){n=void 0}return n&&!n.getElementsByTagName("parsererror").length||w.error("Invalid XML: "+t),n};var St=/\[\]$/,Dt=/\r?\n/g,Nt=/^(?:submit|button|image|reset|file)$/i,At=/^(?:input|select|textarea|keygen)/i;function jt(e,t,n,r){var i;if(Array.isArray(t))w.each(t,function(t,i){n||St.test(e)?r(e,i):jt(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)});else if(n||"object"!==x(t))r(e,t);else for(i in t)jt(e+"["+i+"]",t[i],n,r)}w.param=function(e,t){var n,r=[],i=function(e,t){var n=g(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(Array.isArray(e)||e.jquery&&!w.isPlainObject(e))w.each(e,function(){i(this.name,this.value)});else for(n in e)jt(n,e[n],t,i);return r.join("&")},w.fn.extend({serialize:function(){return w.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=w.prop(this,"elements");return e?w.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!w(this).is(":disabled")&&At.test(this.nodeName)&&!Nt.test(e)&&(this.checked||!pe.test(e))}).map(function(e,t){var n=w(this).val();return null==n?null:Array.isArray(n)?w.map(n,function(e){return{name:t.name,value:e.replace(Dt,"\r\n")}}):{name:t.name,value:n.replace(Dt,"\r\n")}}).get()}});var qt=/%20/g,Lt=/#.*$/,Ht=/([?&])_=[^&]*/,Ot=/^(.*?):[ \t]*([^\r\n]*)$/gm,Pt=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Mt=/^(?:GET|HEAD)$/,Rt=/^\/\//,It={},Wt={},$t="*/".concat("*"),Bt=r.createElement("a");Bt.href=Ct.href;function Ft(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(M)||[];if(g(n))while(r=o[i++])"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function _t(e,t,n,r){var i={},o=e===Wt;function a(s){var u;return i[s]=!0,w.each(e[s]||[],function(e,s){var l=s(t,n,r);return"string"!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)}),u}return a(t.dataTypes[0])||!i["*"]&&a("*")}function zt(e,t){var n,r,i=w.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&w.extend(!0,e,r),e}function Xt(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while("*"===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}function Ut(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift())if("*"===o)o=u;else if("*"!==u&&u!==o){if(!(a=l[u+" "+o]||l["* "+o]))for(i in l)if((s=i.split(" "))[1]===o&&(a=l[u+" "+s[0]]||l["* "+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}if(!0!==a)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(e){return{state:"parsererror",error:a?e:"No conversion from "+u+" to "+o}}}return{state:"success",data:t}}w.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ct.href,type:"GET",isLocal:Pt.test(Ct.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":$t,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":w.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?zt(zt(e,w.ajaxSettings),t):zt(w.ajaxSettings,e)},ajaxPrefilter:Ft(It),ajaxTransport:Ft(Wt),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var i,o,a,s,u,l,c,f,p,d,h=w.ajaxSetup({},n),g=h.context||h,y=h.context&&(g.nodeType||g.jquery)?w(g):w.event,v=w.Deferred(),m=w.Callbacks("once memory"),x=h.statusCode||{},b={},T={},C="canceled",E={readyState:0,getResponseHeader:function(e){var t;if(c){if(!s){s={};while(t=Ot.exec(a))s[t[1].toLowerCase()]=t[2]}t=s[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return c?a:null},setRequestHeader:function(e,t){return null==c&&(e=T[e.toLowerCase()]=T[e.toLowerCase()]||e,b[e]=t),this},overrideMimeType:function(e){return null==c&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(c)E.always(e[E.status]);else for(t in e)x[t]=[x[t],e[t]];return this},abort:function(e){var t=e||C;return i&&i.abort(t),k(0,t),this}};if(v.promise(E),h.url=((t||h.url||Ct.href)+"").replace(Rt,Ct.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(M)||[""],null==h.crossDomain){l=r.createElement("a");try{l.href=h.url,l.href=l.href,h.crossDomain=Bt.protocol+"//"+Bt.host!=l.protocol+"//"+l.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=w.param(h.data,h.traditional)),_t(It,h,n,E),c)return E;(f=w.event&&h.global)&&0==w.active++&&w.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Mt.test(h.type),o=h.url.replace(Lt,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(qt,"+")):(d=h.url.slice(o.length),h.data&&(h.processData||"string"==typeof h.data)&&(o+=(kt.test(o)?"&":"?")+h.data,delete h.data),!1===h.cache&&(o=o.replace(Ht,"$1"),d=(kt.test(o)?"&":"?")+"_="+Et+++d),h.url=o+d),h.ifModified&&(w.lastModified[o]&&E.setRequestHeader("If-Modified-Since",w.lastModified[o]),w.etag[o]&&E.setRequestHeader("If-None-Match",w.etag[o])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&E.setRequestHeader("Content-Type",h.contentType),E.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+$t+"; q=0.01":""):h.accepts["*"]);for(p in h.headers)E.setRequestHeader(p,h.headers[p]);if(h.beforeSend&&(!1===h.beforeSend.call(g,E,h)||c))return E.abort();if(C="abort",m.add(h.complete),E.done(h.success),E.fail(h.error),i=_t(Wt,h,n,E)){if(E.readyState=1,f&&y.trigger("ajaxSend",[E,h]),c)return E;h.async&&h.timeout>0&&(u=e.setTimeout(function(){E.abort("timeout")},h.timeout));try{c=!1,i.send(b,k)}catch(e){if(c)throw e;k(-1,e)}}else k(-1,"No Transport");function k(t,n,r,s){var l,p,d,b,T,C=n;c||(c=!0,u&&e.clearTimeout(u),i=void 0,a=s||"",E.readyState=t>0?4:0,l=t>=200&&t<300||304===t,r&&(b=Xt(h,E,r)),b=Ut(h,b,E,l),l?(h.ifModified&&((T=E.getResponseHeader("Last-Modified"))&&(w.lastModified[o]=T),(T=E.getResponseHeader("etag"))&&(w.etag[o]=T)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=b.state,p=b.data,l=!(d=b.error))):(d=C,!t&&C||(C="error",t<0&&(t=0))),E.status=t,E.statusText=(n||C)+"",l?v.resolveWith(g,[p,C,E]):v.rejectWith(g,[E,C,d]),E.statusCode(x),x=void 0,f&&y.trigger(l?"ajaxSuccess":"ajaxError",[E,h,l?p:d]),m.fireWith(g,[E,C]),f&&(y.trigger("ajaxComplete",[E,h]),--w.active||w.event.trigger("ajaxStop")))}return E},getJSON:function(e,t,n){return w.get(e,t,n,"json")},getScript:function(e,t){return w.get(e,void 0,t,"script")}}),w.each(["get","post"],function(e,t){w[t]=function(e,n,r,i){return g(n)&&(i=i||r,r=n,n=void 0),w.ajax(w.extend({url:e,type:t,dataType:i,data:n,success:r},w.isPlainObject(e)&&e))}}),w._evalUrl=function(e){return w.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},w.fn.extend({wrapAll:function(e){var t;return this[0]&&(g(e)&&(e=e.call(this[0])),t=w(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this},wrapInner:function(e){return g(e)?this.each(function(t){w(this).wrapInner(e.call(this,t))}):this.each(function(){var t=w(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=g(e);return this.each(function(n){w(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){w(this).replaceWith(this.childNodes)}),this}}),w.expr.pseudos.hidden=function(e){return!w.expr.pseudos.visible(e)},w.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},w.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Vt={0:200,1223:204},Gt=w.ajaxSettings.xhr();h.cors=!!Gt&&"withCredentials"in Gt,h.ajax=Gt=!!Gt,w.ajaxTransport(function(t){var n,r;if(h.cors||Gt&&!t.crossDomain)return{send:function(i,o){var a,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(a in t.xhrFields)s[a]=t.xhrFields[a];t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest");for(a in i)s.setRequestHeader(a,i[a]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?o(0,"error"):o(s.status,s.statusText):o(Vt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout(function(){n&&r()})},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}}),w.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),w.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return w.globalEval(e),e}}}),w.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),w.ajaxTransport("script",function(e){if(e.crossDomain){var t,n;return{send:function(i,o){t=w(" + + + +

Benchmark

+
+
+ Start benchmark + (It will take around 20 sec) +
+
+ + + + \ No newline at end of file diff --git a/plugins/Benchmark/plugin_info.json b/plugins/Benchmark/plugin_info.json new file mode 100644 index 00000000..f3f57417 --- /dev/null +++ b/plugins/Benchmark/plugin_info.json @@ -0,0 +1,5 @@ +{ + "name": "Benchmark", + "description": "Test and benchmark database and cryptographic functions related to ZeroNet.", + "default": "enabled" +} \ No newline at end of file From 331dc990863df619f8b96c9a6366af32c60eae96 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:03:27 +0100 Subject: [PATCH 204/483] Fix benchmark plugin test listing if not loaded before other plugins --- plugins/Benchmark/BenchmarkPlugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index eb3c31c0..3c588b6c 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -95,7 +95,12 @@ class ActionsPlugin: return " Done in %.3fs = %s (%.2fx)" % (taken, multipler_title, multipler) def getBenchmarkTests(self, online=False): - tests = [ + if hasattr(super(), "getBenchmarkTests"): + tests = super().getBenchmarkTests(online) + else: + tests = [] + + tests.extend([ {"func": self.testHdPrivatekey, "num": 50, "time_standard": 0.57}, {"func": self.testSign, "num": 20, "time_standard": 0.46}, {"func": self.testVerify, "kwargs": {"lib_verify": "btctools"}, "num": 20, "time_standard": 0.38}, @@ -121,7 +126,8 @@ class ActionsPlugin: {"func": self.testCryptHashlib, "kwargs": {"hash_type": "sha3_512"}, "num": 10, "time_standard": 0.65}, {"func": self.testRandom, "num": 100, "time_standard": 0.08}, - ] + ]) + if online: tests += [ {"func": self.testHttps, "num": 1, "time_standard": 2.1} From 2ad3493fb027df838c4af792817ed6caa2cac7ce Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:05:02 +0100 Subject: [PATCH 205/483] Test and benchmark of crypto function in CryptMessage plugin --- plugins/CryptMessage/CryptMessagePlugin.py | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index 64c8ac81..c3907669 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -170,3 +170,61 @@ class UserPlugin(object): publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed") site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey).decode("utf8") return site_data["encrypt_publickey_%s" % index] + + +@PluginManager.registerTo("Actions") +class ActionsPlugin: + publickey = "A3HatibU4S6eZfIQhVs2u7GLN5G9wXa9WwlkyYIfwYaj" + privatekey = "5JBiKFYBm94EUdbxtnuLi6cvNcPzcKymCUHBDf2B6aq19vvG3rL" + utf8_text = '\xc1rv\xedzt\xfbr\xf5t\xfck\xf6rf\xfar\xf3g\xe9p' + + def getBenchmarkTests(self, online=False): + if hasattr(super(), "getBenchmarkTests"): + tests = super().getBenchmarkTests(online) + else: + tests = [] + + aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) # Warm-up + tests.extend([ + {"func": self.testCryptEciesEncrypt, "kwargs": {}, "num": 100, "time_standard": 1.2}, + {"func": self.testCryptEciesDecrypt, "kwargs": {}, "num": 500, "time_standard": 1.3}, + {"func": self.testCryptAesEncrypt, "kwargs": {}, "num": 10000, "time_standard": 0.27}, + {"func": self.testCryptAesDecrypt, "kwargs": {}, "num": 10000, "time_standard": 0.25} + ]) + return tests + + def testCryptEciesEncrypt(self, num_run=1): + for i in range(num_run): + aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) + assert len(aes_key) == 32 + yield "." + + def testCryptEciesDecrypt(self, num_run=1): + aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) + for i in range(num_run): + assert len(aes_key) == 32 + ecc = CryptMessage.getEcc(self.privatekey) + assert ecc.decrypt(encrypted) == self.utf8_text.encode("utf8"), "%s != %s" % (ecc.decrypt(encrypted), self.utf8_text.encode("utf8")) + yield "." + + def testCryptAesEncrypt(self, num_run=1): + from lib import pyelliptic + + for i in range(num_run): + key = os.urandom(32) + iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') + encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) + yield "." + + def testCryptAesDecrypt(self, num_run=1): + from lib import pyelliptic + + key = os.urandom(32) + iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') + encrypted_text = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) + + for i in range(num_run): + ctx = pyelliptic.Cipher(key, iv, 0, ciphername='aes-256-cbc') + decrypted = ctx.ciphering(encrypted_text).decode("utf8") + assert decrypted == self.utf8_text + yield "." From 1c607645c76eb99646a9e7c6fdf10b7cd88eae9d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:07:51 +0100 Subject: [PATCH 206/483] Track and stop site connected greenlets on delete --- plugins/OptionalManager/ContentDbPlugin.py | 2 +- plugins/PeerDb/PeerDbPlugin.py | 6 +++--- src/Site/Site.py | 17 ++++++++++++---- src/Site/SiteAnnouncer.py | 4 ++-- src/Worker/WorkerManager.py | 15 ++++++++------ src/util/GreenletManager.py | 23 ++++++++++++++++++++++ 6 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 src/util/GreenletManager.py diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index e7945d93..fd28092b 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -61,7 +61,7 @@ class ContentDbPlugin(object): if self.need_filling: self.fillTableFileOptional(site) if not self.optional_files_loading: - gevent.spawn_later(1, self.loadFilesOptional) + site.greenlet_manager.spawnLater(1, self.loadFilesOptional) self.optional_files_loading = True def checkTables(self): diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index b4c8787b..addd1d6f 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -70,7 +70,7 @@ class ContentDbPlugin(object): def savePeers(self, site, spawn=False): if spawn: # Save peers every hour (+random some secs to not update very site at same time) - gevent.spawn_later(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True) + site.greenlet_manager.spawnLater(60 * 60 + random.randint(0, 60), self.savePeers, site, spawn=True) if not site.peers: site.log.debug("Peers not saved: No peers found") return @@ -89,8 +89,8 @@ class ContentDbPlugin(object): def initSite(self, site): super(ContentDbPlugin, self).initSite(site) - gevent.spawn_later(0.5, self.loadPeers, site) - gevent.spawn_later(60*60, self.savePeers, site, spawn=True) + site.greenlet_manager.spawnLater(0.5, self.loadPeers, site) + site.greenlet_manager.spawnLater(60*60, self.savePeers, site, spawn=True) def saveAllPeers(self): for site in list(self.sites.values()): diff --git a/src/Site/Site.py b/src/Site/Site.py index 59b5745f..807507ee 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -22,6 +22,7 @@ from .SiteStorage import SiteStorage from Crypt import CryptHash from util import helper from util import Diff +from util import GreenletManager from Plugin import PluginManager from File import FileServer from .SiteAnnouncer import SiteAnnouncer @@ -43,6 +44,7 @@ class Site(object): self.peers = {} # Key: ip:port, Value: Peer.Peer self.peers_recent = collections.deque(maxlen=100) self.peer_blacklist = SiteManager.peer_blacklist # Ignore this peers (eg. myself) + self.greenlet_manager = GreenletManager.GreenletManager() # Running greenlets self.worker_manager = WorkerManager(self) # Handle site download from other peers self.bad_files = {} # SHA check failed files, need to redownload {"inner.content": 1} (key: file, value: failed accept) self.content_updated = None # Content.js update time @@ -1026,14 +1028,21 @@ class Site(object): return self.settings.get("autodownloadoptional") def delete(self): + self.log.debug("Deleting site...") + s = time.time() self.settings["serving"] = False self.saveSettings() + num_greenlets = self.greenlet_manager.stopGreenlets("Site %s deleted" % self.address) self.worker_manager.running = False - self.worker_manager.stopWorkers() - self.storage.deleteFiles() - self.updateWebsocket(deleted=True) - self.content_manager.contents.db.deleteSite(self) + num_workers = self.worker_manager.stopWorkers() SiteManager.site_manager.delete(self.address) + self.content_manager.contents.db.deleteSite(self) + self.updateWebsocket(deleted=True) + self.storage.deleteFiles() + self.log.debug( + "Deleted site in %.3fs (greenlets: %s, workers: %s)" % + (time.time() - s, num_greenlets, num_workers) + ) # - Events - diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 0d6fc9b1..78e47731 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -95,7 +95,7 @@ class SiteAnnouncer(object): if config.verbose: self.site.log.debug("Tracker %s looks unreliable, announce skipped (error: %s)" % (tracker, tracker_stats["num_error"])) continue - thread = gevent.spawn(self.announceTracker, tracker, mode=mode) + thread = self.site.greenlet_manager.spawn(self.announceTracker, tracker, mode=mode) threads.append(thread) thread.tracker = tracker @@ -135,7 +135,7 @@ class SiteAnnouncer(object): self.site.log.error("Announce to %s trackers in %.3fs, failed" % (len(threads), time.time() - s)) if len(threads) == 1 and mode != "start": # Move to next tracker self.site.log.debug("Tracker failed, skipping to next one...") - gevent.spawn_later(1.0, self.announce, force=force, mode=mode, pex=pex) + self.site.greenlet_manager.spawnLater(1.0, self.announce, force=force, mode=mode, pex=pex) self.updateWebsocket(trackers="announced") diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 236b048a..c35b4b93 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -25,7 +25,7 @@ class WorkerManager(object): self.running = True self.time_task_added = 0 self.log = logging.getLogger("WorkerManager:%s" % self.site.address_short) - self.process_taskchecker = gevent.spawn(self.checkTasks) + self.site.greenlet_manager.spawn(self.checkTasks) def __str__(self): return "WorkerManager %s" % self.site.address_short @@ -308,7 +308,7 @@ class WorkerManager(object): if not peers: peers = self.site.getConnectablePeers() for peer in peers: - threads.append(gevent.spawn(peer.updateHashfield, force=find_more)) + threads.append(self.site.greenlet_manager.spawn(peer.updateHashfield, force=find_more)) gevent.joinall(threads, timeout=5) if time_tasks != self.time_task_added: # New task added since start @@ -340,7 +340,7 @@ class WorkerManager(object): peers = self.site.getConnectablePeers(ignore=self.asked_peers) for peer in peers: - threads.append(gevent.spawn(peer.findHashIds, list(optional_hash_ids))) + threads.append(self.site.greenlet_manager.spawn(peer.findHashIds, list(optional_hash_ids))) self.asked_peers.append(peer.key) for i in range(5): @@ -379,7 +379,7 @@ class WorkerManager(object): peers = self.site.getConnectablePeers(ignore=self.asked_peers) for peer in peers: - threads.append(gevent.spawn(peer.findHashIds, list(optional_hash_ids))) + threads.append(self.site.greenlet_manager.spawn(peer.findHashIds, list(optional_hash_ids))) self.asked_peers.append(peer.key) gevent.joinall(threads, timeout=15) @@ -397,17 +397,20 @@ class WorkerManager(object): if time_tasks != self.time_task_added: # New task added since start self.log.debug("New task since start, restarting...") - gevent.spawn_later(0.1, self.startFindOptional) + self.site.greenlet_manager.spawnLater(0.1, self.startFindOptional) else: self.log.debug("startFindOptional ended") # Stop all worker def stopWorkers(self): + num = 0 for worker in list(self.workers.values()): worker.stop() + num += 1 tasks = self.tasks[:] # Copy for task in tasks: # Mark all current task as failed self.failTask(task) + return num # Find workers by task def findWorkers(self, task): @@ -554,7 +557,7 @@ class WorkerManager(object): self.site.onFileDone(task["inner_path"]) task["evt"].set(True) if not self.tasks: - gevent.spawn(self.checkComplete) + self.site.greenlet_manager.spawn(self.checkComplete) # Mark a task failed def failTask(self, task): diff --git a/src/util/GreenletManager.py b/src/util/GreenletManager.py new file mode 100644 index 00000000..6379f97c --- /dev/null +++ b/src/util/GreenletManager.py @@ -0,0 +1,23 @@ +import gevent + + +class GreenletManager: + def __init__(self): + self.greenlets = set() + + def spawnLater(self, *args, **kwargs): + greenlet = gevent.spawn_later(*args, **kwargs) + greenlet.link(lambda greenlet: self.greenlets.remove(greenlet)) + self.greenlets.add(greenlet) + return greenlet + + def spawn(self, *args, **kwargs): + greenlet = gevent.spawn(*args, **kwargs) + greenlet.link(lambda greenlet: self.greenlets.remove(greenlet)) + self.greenlets.add(greenlet) + return greenlet + + def stopGreenlets(self, reason="Stopping greenlets"): + num = len(self.greenlets) + gevent.killall(list(self.greenlets)) + return num From 39352eb97e1507492602214252b2fa1103e4b6ad Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:08:03 +0100 Subject: [PATCH 207/483] Fix test function listing name --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 896ecbde..ce034454 100644 --- a/src/main.py +++ b/src/main.py @@ -558,7 +558,7 @@ class Actions(object): if func.__doc__: print("- %s: %s" % (test_name, func.__doc__.strip())) else: - print("- %s" % func_name) + print("- %s" % test_name) return None # Run tests From 96e7fbdca18c6887a61c6acdbd75c42e2c5c2d90 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:08:30 +0100 Subject: [PATCH 208/483] Don't try to commit if no db connection --- src/Db/Db.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Db/Db.py b/src/Db/Db.py index 7eb801de..fd6eec32 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -114,6 +114,10 @@ class Db(object): self.log.debug("Commit ignored: Progress sleeping") return False + if not self.conn: + self.log.debug("Commit ignored: No connection") + return False + try: s = time.time() self.conn.commit() From 8f27f50b34decb8e7edb76f9f42bd9ab9a6e3d58 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:09:36 +0100 Subject: [PATCH 209/483] Log SQL statements in progress as warning --- src/Db/Db.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index fd6eec32..2d1b2e66 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -124,7 +124,10 @@ class Db(object): self.log.debug("Commited in %.3fs (reason: %s)" % (time.time() - s, reason)) return True except Exception as err: - self.log.error("Commit error: %s" % err) + if "SQL statements in progress" in str(err): + self.log.warning("Commit delayed: %s (reason: %s)" % (Debug.formatException(err), reason)) + else: + self.log.error("Commit error: %s (reason: %s)" % (Debug.formatException(err), reason)) return False def insertOrUpdate(self, *args, **kwargs): From dd61429e2fd72a876e83a75934d9981ecdacca27 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:09:55 +0100 Subject: [PATCH 210/483] Handle announcer thread killing properly --- src/Site/SiteAnnouncer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 78e47731..309f2a96 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -10,6 +10,7 @@ from Plugin import PluginManager from Config import config from Debug import Debug from util import helper +from greenlet import GreenletExit import util @@ -105,7 +106,7 @@ class SiteAnnouncer(object): gevent.joinall(threads, timeout=20) # Wait for announce finish for thread in threads: - if thread.value is None: + if thread.value is None or type(thread.value) is GreenletExit: continue if thread.value is not False: if thread.value > 1.0: # Takes more than 1 second to announce From 74d7fb7835646e055d2bfc68178b950c898ba6d0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:10:42 +0100 Subject: [PATCH 211/483] Less verbose logging in site storage --- src/Site/SiteStorage.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index a93f4981..e31dad09 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -102,7 +102,7 @@ class SiteStorage(object): if self.isFile(content_inner_path): yield content_inner_path, self.getPath(content_inner_path) else: - self.log.error("[MISSING] %s" % content_inner_path) + self.log.debug("[MISSING] %s" % content_inner_path) # Data files in content.json content_inner_path_dir = helper.getDirname(content_inner_path) # Content.json dir relative to site for file_relative_path in list(content.get("files", {}).keys()) + list(content.get("files_optional", {}).keys()): @@ -113,7 +113,7 @@ class SiteStorage(object): if self.isFile(file_inner_path): yield file_inner_path, self.getPath(file_inner_path) else: - self.log.error("[MISSING] %s" % file_inner_path) + self.log.debug("[MISSING] %s" % file_inner_path) found += 1 if found % 100 == 0: time.sleep(0.001) # Context switch to avoid UI block @@ -534,7 +534,6 @@ class SiteStorage(object): path = os.path.join(root, dir) if os.path.isdir(path) and os.listdir(path) == []: os.rmdir(path) - self.log.debug("Removing %s" % path) if os.path.isdir(self.directory) and os.listdir(self.directory) == []: os.rmdir(self.directory) # Remove sites directory if empty From 57f2a43864b42dc83b1456d714145b92b27d93c8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:11:19 +0100 Subject: [PATCH 212/483] Formatting --- src/Site/SiteStorage.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index e31dad09..3e4d9c74 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -159,7 +159,11 @@ class SiteStorage(object): self.log.info("Importing data...") try: if num_total > 100: - self.site.messageWebsocket(_["Database rebuilding...
Imported {0} of {1} files (error: {2})..."].format("0000", num_total, num_error), "rebuild", 0) + self.site.messageWebsocket( + _["Database rebuilding...
Imported {0} of {1} files (error: {2})..."].format( + "0000", num_total, num_error + ), "rebuild", 0 + ) for file_inner_path, file_path in db_files: try: if self.updateDbFile(file_inner_path, file=open(file_path, "rb"), cur=cur): @@ -170,16 +174,21 @@ class SiteStorage(object): if num_imported and num_imported % 100 == 0: self.site.messageWebsocket( - _["Database rebuilding...
Imported {0} of {1} files (error: {2})..."].format(num_imported, num_total, num_error), - "rebuild", - int(float(num_imported) / num_total * 100) + _["Database rebuilding...
Imported {0} of {1} files (error: {2})..."].format( + num_imported, num_total, num_error + ), + "rebuild", int(float(num_imported) / num_total * 100) ) time.sleep(0.001) # Context switch to avoid UI block finally: cur.close() if num_total > 100: - self.site.messageWebsocket(_["Database rebuilding...
Imported {0} of {1} files (error: {2})..."].format(num_imported, num_total, num_error), "rebuild", 100) + self.site.messageWebsocket( + _["Database rebuilding...
Imported {0} of {1} files (error: {2})..."].format( + num_imported, num_total, num_error + ), "rebuild", 100 + ) self.log.info("Imported %s data file in %.3fs" % (num_imported, time.time() - s)) self.event_db_busy.set(True) # Event done, notify waiters self.event_db_busy = None # Clear event @@ -305,13 +314,14 @@ class SiteStorage(object): # Site content updated def onUpdated(self, inner_path, file=None): # Update Sql cache + should_load_to_db = inner_path.endswith(".json") or inner_path.endswith(".json.gz") if inner_path == "dbschema.json": self.has_db = self.isFile("dbschema.json") # Reopen DB to check changes if self.has_db: self.closeDb() self.getDb() - elif not config.disable_db and (inner_path.endswith(".json") or inner_path.endswith(".json.gz")) and self.has_db: # Load json file to db + elif not config.disable_db and should_load_to_db and self.has_db: # Load json file to db if config.verbose: self.log.debug("Loading json file to db: %s (file: %s)" % (inner_path, file)) try: @@ -335,7 +345,7 @@ class SiteStorage(object): path = self.getPath(inner_path) try: return os.path.getsize(path) - except: + except Exception: return 0 # File exist From cdd9dd4f6f74b5b226415f29e425ef6fbdf98c91 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:12:24 +0100 Subject: [PATCH 213/483] Fix duplicate content_db connecting --- src/Content/ContentDb.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Content/ContentDb.py b/src/Content/ContentDb.py index aeb23fe1..f284581e 100644 --- a/src/Content/ContentDb.py +++ b/src/Content/ContentDb.py @@ -1,4 +1,3 @@ -import time import os from Db.Db import Db, DbTableError @@ -12,6 +11,8 @@ class ContentDb(Db): def __init__(self, path): Db.__init__(self, {"db_name": "ContentDb", "tables": {}}, path) self.foreign_keys = True + + def init(self): try: self.schema = self.getSchema() try: @@ -25,8 +26,8 @@ class ContentDb(Db): except Exception as err: self.log.error("Error loading content.db: %s, rebuilding..." % Debug.formatException(err)) self.close() - os.unlink(path) # Remove and try again - Db.__init__(self, {"db_name": "ContentDb", "tables": {}}, path) + os.unlink(self.db_path) # Remove and try again + Db.__init__(self, {"db_name": "ContentDb", "tables": {}}, self.db_path) self.foreign_keys = True self.schema = self.getSchema() try: @@ -155,6 +156,7 @@ def getContentDb(path=None): path = "%s/content.db" % config.data_dir if path not in content_dbs: content_dbs[path] = ContentDb(path) + content_dbs[path].init() return content_dbs[path] getContentDb() # Pre-connect to default one From 8c1f64243fc0de7205bf853066fe33a2727c1abb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:14:29 +0100 Subject: [PATCH 214/483] Test CLI action parser --- src/Config.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/Config.py b/src/Config.py index 89dace4e..cc9e00bd 100644 --- a/src/Config.py +++ b/src/Config.py @@ -16,6 +16,7 @@ class Config(object): self.rev = 4260 self.argv = argv self.action = None + self.test_parser = None self.pending_changes = {} self.need_restart = False self.keys_api_change_allowed = set([ @@ -203,6 +204,8 @@ class Config(object): action = self.subparsers.add_parser("testConnection", help='Testing') action = self.subparsers.add_parser("testAnnounce", help='Testing') + self.test_parser = self.subparsers.add_parser("test", help='Run a test') + self.test_parser.add_argument('test_name', help='Test name', nargs="?") # Config parameters self.parser.add_argument('--verbose', help='More detailed logging', action='store_true') self.parser.add_argument('--debug', help='Debug mode', action='store_true') @@ -356,8 +359,17 @@ class Config(object): valid_parameters.append(arg) return valid_parameters + plugin_parameters + def getParser(self, argv): + action = self.getAction(argv) + if not action: + return self.parser + else: + return self.subparsers.choices[action] + # Parse arguments from config file and command line def parse(self, silent=False, parse_config=True): + argv = self.argv[:] # Copy command line arguments + current_parser = self.getParser(argv) if silent: # Don't display messages or quit on unknown parameter original_print_message = self.parser._print_message original_exit = self.parser.exit @@ -365,11 +377,10 @@ class Config(object): def silencer(parser, function_name): parser.exited = True return None - self.parser.exited = False - self.parser._print_message = lambda *args, **kwargs: silencer(self.parser, "_print_message") - self.parser.exit = lambda *args, **kwargs: silencer(self.parser, "exit") + current_parser.exited = False + current_parser._print_message = lambda *args, **kwargs: silencer(current_parser, "_print_message") + current_parser.exit = lambda *args, **kwargs: silencer(current_parser, "exit") - argv = self.argv[:] # Copy command line arguments self.parseCommandline(argv, silent) # Parse argv self.setAttributes() if parse_config: @@ -383,10 +394,10 @@ class Config(object): self.ip_local.append(self.fileserver_ip) if silent: # Restore original functions - if self.parser.exited and self.action == "main": # Argument parsing halted, don't start ZeroNet with main action + if current_parser.exited and self.action == "main": # Argument parsing halted, don't start ZeroNet with main action self.action = None - self.parser._print_message = original_print_message - self.parser.exit = original_exit + current_parser._print_message = original_print_message + current_parser.exit = original_exit self.loadTrackersFile() @@ -439,6 +450,16 @@ class Config(object): argv = argv[:1] + argv_extend + argv[1:] return argv + # Return command line value of given argument + def getCmdlineValue(self, key): + if key not in self.argv: + return None + argv_index = self.argv.index(key) + if argv_index == len(self.argv) - 1: # last arg, test not specified + return None + + return self.argv[argv_index + 1] + # Expose arguments as class attributes def setAttributes(self): # Set attributes from arguments @@ -457,7 +478,11 @@ class Config(object): @PluginManager.acceptPlugins class ConfigPlugin(object): def __init__(self, config): + self.argv = config.argv self.parser = config.parser + self.subparsers = config.subparsers + self.test_parser = config.test_parser + self.getCmdlineValue = config.getCmdlineValue self.createArguments() def createArguments(self): From b41a03674fdb95c4cd9ac2c206415ad23398693f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:15:00 +0100 Subject: [PATCH 215/483] New configuration options for fs write and read thread count --- src/Config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Config.py b/src/Config.py index cc9e00bd..228bcf9e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -280,6 +280,8 @@ class Config(object): self.parser.add_argument("--fix_float_decimals", help='Fix content.json modification date float precision on verification', type='bool', choices=[True, False], default=fix_float_decimals) self.parser.add_argument("--db_mode", choices=["speed", "security"], default="speed") + self.parser.add_argument('--threads_fs_read', help='Number of threads for file read operations', default=1, type=int) + self.parser.add_argument('--threads_fs_write', help='Number of threads for file write operations', default=1, type=int) self.parser.add_argument("--download_optional", choices=["manual", "auto"], default="manual") self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript, From 5d113757df93c90d2365778dba364b4ce6499847 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:15:47 +0100 Subject: [PATCH 216/483] Stop greenlets when deleting a site in test --- src/Test/conftest.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 3c6e0870..0b7e97e9 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -178,9 +178,7 @@ def site(request): site.announce = mock.MagicMock(return_value=True) # Don't try to find peers from the net def cleanup(): - site.storage.deleteFiles() - site.content_manager.contents.db.deleteSite(site) - del SiteManager.site_manager.sites["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] + site.delete() site.content_manager.contents.db.close() SiteManager.site_manager.sites.clear() db_path = "%s/content.db" % config.data_dir @@ -189,10 +187,12 @@ def site(request): gevent.killall([obj for obj in gc.get_objects() if isinstance(obj, gevent.Greenlet) and obj not in threads_before]) request.addfinalizer(cleanup) + site.greenlet_manager.stopGreenlets() site = Site("1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT") # Create new Site object to load content.json files if not SiteManager.site_manager.sites: SiteManager.site_manager.sites = {} SiteManager.site_manager.sites["1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT"] = site + site.settings["serving"] = True return site @@ -201,13 +201,12 @@ def site_temp(request): threads_before = [obj for obj in gc.get_objects() if isinstance(obj, gevent.Greenlet)] with mock.patch("Config.config.data_dir", config.data_dir + "-temp"): site_temp = Site("1TeSTvb4w2PWE81S2rEELgmX2GCCExQGT") + site_temp.settings["serving"] = True site_temp.announce = mock.MagicMock(return_value=True) # Don't try to find peers from the net def cleanup(): - site_temp.storage.deleteFiles() - site_temp.content_manager.contents.db.deleteSite(site_temp) + site_temp.delete() site_temp.content_manager.contents.db.close() - time.sleep(0.01) # Wait for db close db_path = "%s-temp/content.db" % config.data_dir os.unlink(db_path) del ContentDb.content_dbs[db_path] From 58214c0ac30b84a7b19e0278440bf28b7f4e01ce Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:16:20 +0100 Subject: [PATCH 217/483] Move file writes and reads to separate thread --- src/Site/SiteStorage.py | 23 ++++++++++++++++++++--- src/Test/TestThreadPool.py | 29 +++++++++++++++++++++++++++++ src/util/ThreadPool.py | 22 ++++++++++++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 src/Test/TestThreadPool.py create mode 100644 src/util/ThreadPool.py diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 3e4d9c74..49d9e7d1 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -14,10 +14,15 @@ from Db.Db import Db from Debug import Debug from Config import config from util import helper +from util import ThreadPool from Plugin import PluginManager from Translate import translate as _ +thread_pool_fs_read = ThreadPool.ThreadPool(config.threads_fs_read) +thread_pool_fs_write = ThreadPool.ThreadPool(config.threads_fs_write) + + @PluginManager.acceptPlugins class SiteStorage(object): def __init__(self, site, allow_create=True): @@ -44,6 +49,7 @@ class SiteStorage(object): return False # Create new databaseobject with the site's schema + @util.Noparallel() def openDb(self, close_idle=False): schema = self.getDbSchema() db_path = self.getPath(schema["db_file"]) @@ -95,6 +101,7 @@ class SiteStorage(object): return self.getDb().updateJson(path, file, cur) # Return possible db files for the site + @thread_pool_fs_read.wrap def getDbFiles(self): found = 0 for content_inner_path, content in self.site.content_manager.contents.items(): @@ -120,6 +127,7 @@ class SiteStorage(object): # Rebuild sql cache @util.Noparallel() + @thread_pool_fs_write.wrap def rebuildDb(self, delete_db=True): self.log.info("Rebuilding db...") self.has_db = self.isFile("dbschema.json") @@ -227,11 +235,12 @@ class SiteStorage(object): return open(file_path, mode, **kwargs) # Open file object + @thread_pool_fs_read.wrap def read(self, inner_path, mode="rb"): return open(self.getPath(inner_path), mode).read() - # Write content to file - def write(self, inner_path, content): + @thread_pool_fs_write.wrap + def writeThread(self, inner_path, content): file_path = self.getPath(inner_path) # Create dir if not exist file_dir = os.path.dirname(file_path) @@ -247,7 +256,10 @@ class SiteStorage(object): else: with open(file_path, "wb") as file: file.write(content) - del content + + # Write content to file + def write(self, inner_path, content): + self.writeThread(inner_path, content) self.onUpdated(inner_path) # Remove file from filesystem @@ -275,6 +287,7 @@ class SiteStorage(object): raise rename_err # List files from a directory + @thread_pool_fs_read.wrap def walk(self, dir_inner_path, ignore=None): directory = self.getPath(dir_inner_path) for root, dirs, files in os.walk(directory): @@ -307,6 +320,7 @@ class SiteStorage(object): dirs[:] = dirs_filtered # list directories in a directory + @thread_pool_fs_read.wrap def list(self, dir_inner_path): directory = self.getPath(dir_inner_path) return os.listdir(directory) @@ -331,11 +345,13 @@ class SiteStorage(object): self.closeDb() # Load and parse json file + @thread_pool_fs_read.wrap def loadJson(self, inner_path): with self.open(inner_path, "r", encoding="utf8") as file: return json.load(file) # Write formatted json file + @thread_pool_fs_write.wrap def writeJson(self, inner_path, data): # Write to disk self.write(inner_path, helper.jsonDumps(data).encode("utf8")) @@ -499,6 +515,7 @@ class SiteStorage(object): self.log.debug("Checked files in %.2fs... Found bad files: %s, Quick:%s" % (time.time() - s, len(bad_files), quick_check)) # Delete site's all file + @thread_pool_fs_write.wrap def deleteFiles(self): self.log.debug("Deleting files from content.json...") files = [] # Get filenames diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py new file mode 100644 index 00000000..b237d93a --- /dev/null +++ b/src/Test/TestThreadPool.py @@ -0,0 +1,29 @@ +import gevent + +from util import ThreadPool + + +class TestThreadPool: + def testExecutionOrder(self): + pool = ThreadPool.ThreadPool(4) + + events = [] + + @pool.wrap + def blocker(): + events.append("S") + out = 0 + for i in range(1000000): + out += 1 + events.append("D") + return out + + threads = [] + for i in range(4): + threads.append(gevent.spawn(blocker)) + gevent.joinall(threads) + + assert events == ["S"] * 4 + ["D"] * 4, events + + res = blocker() + assert res == 1000000 diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py new file mode 100644 index 00000000..8fbb12fd --- /dev/null +++ b/src/util/ThreadPool.py @@ -0,0 +1,22 @@ +import gevent.threadpool + + +class ThreadPool: + def __init__(self, max_size): + self.setMaxSize(max_size) + + def setMaxSize(self, max_size): + self.max_size = max_size + if max_size > 0: + self.pool = gevent.threadpool.ThreadPool(max_size) + else: + self.pool = None + + def wrap(self, func): + if self.pool is None: + return func + + def wrapper(*args, **kwargs): + return self.pool.apply(func, args, kwargs) + + return wrapper From 4025d753e3bd8c98109ea4ab1534989e45a5df81 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:16:44 +0100 Subject: [PATCH 218/483] Don't print errors happened in thread --- src/Debug/DebugHook.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Debug/DebugHook.py b/src/Debug/DebugHook.py index ce531df4..d100a3b8 100644 --- a/src/Debug/DebugHook.py +++ b/src/Debug/DebugHook.py @@ -74,6 +74,10 @@ else: importlib.reload(gevent) def handleGreenletError(context, type, value, tb): + if context.__class__ is tuple and context[0].__class__.__name__ == "ThreadPool": + # Exceptions in ThreadPool will be handled in the main Thread + return None + if isinstance(value, str): # Cython can raise errors where the value is a plain string # e.g., AttributeError, "_semaphore.Semaphore has no attr", From 5d34bb9062b7b880f8928ecf9212e75835eeb34c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:17:32 +0100 Subject: [PATCH 219/483] Rev4287 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 228bcf9e..aefd7348 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4260 + self.rev = 4287 self.argv = argv self.action = None self.test_parser = None From 511587dd8bab47de728ec6428a92ad8be6ed6532 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:19:14 +0100 Subject: [PATCH 220/483] Allow images from data uris --- src/Ui/UiRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 186576b9..cd23d47d 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -282,7 +282,7 @@ class UiRequest(object): if noscript: headers["Content-Security-Policy"] = "default-src 'none'; sandbox allow-top-navigation allow-forms; img-src *; font-src * data:; media-src *; style-src * 'unsafe-inline';" elif script_nonce and self.isScriptNonceSupported(): - headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self' blob:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:".format(script_nonce) + headers["Content-Security-Policy"] = "default-src 'none'; script-src 'nonce-{0}'; img-src 'self' blob: data:; style-src 'self' blob: 'unsafe-inline'; connect-src *; frame-src 'self' blob:".format(script_nonce) if allow_ajax: headers["Access-Control-Allow-Origin"] = "null" From 5aa115c88acda0baa3951ffe9c74a8ed110309ca Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 19 Nov 2019 02:25:28 +0100 Subject: [PATCH 221/483] Heavier task in thread pool test to make sure it will pass --- src/Test/TestThreadPool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py index b237d93a..6e081206 100644 --- a/src/Test/TestThreadPool.py +++ b/src/Test/TestThreadPool.py @@ -13,7 +13,7 @@ class TestThreadPool: def blocker(): events.append("S") out = 0 - for i in range(1000000): + for i in range(10000000): out += 1 events.append("D") return out @@ -23,7 +23,7 @@ class TestThreadPool: threads.append(gevent.spawn(blocker)) gevent.joinall(threads) - assert events == ["S"] * 4 + ["D"] * 4, events + assert events == ["S"] * 4 + ["D"] * 4 res = blocker() - assert res == 1000000 + assert res == 10000000 From 6262c808863df1adbbeb22431ce5b22f1c2a4e41 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 20 Nov 2019 14:06:27 +0100 Subject: [PATCH 222/483] Fix benchmark on Firefox --- plugins/Benchmark/media/benchmark.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Benchmark/media/benchmark.html b/plugins/Benchmark/media/benchmark.html index 1d63cf86..f308d8ba 100644 --- a/plugins/Benchmark/media/benchmark.html +++ b/plugins/Benchmark/media/benchmark.html @@ -39,7 +39,7 @@ function setState(elem, text) { } } formatted = formatted.replace(/(\! Error:.*)/, "
$1
"); - formatted = formatted.replace(/(\* Result:.*)/s, "
$1
"); + formatted = formatted.replace(/(\* Result:[^]*)/, "
$1
"); var is_bottom = document.body.scrollTop + document.body.clientHeight >= document.body.scrollHeight - 5; elem.innerHTML = formatted.trim(); if (is_bottom) From 6c31a3b77eb16f6567595414da3b821779179c31 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 20 Nov 2019 14:07:04 +0100 Subject: [PATCH 223/483] Change fs thread number on config interface --- .../UiConfig/media/js/ConfigStorage.coffee | 28 +++++++++++++++++++ src/Config.py | 5 ++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index 83275bd6..4d9684cc 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -149,6 +149,34 @@ class ConfigStorage extends Class {title: "Only errors", value: "ERROR"} ] + section.items.push + key: "threads_fs_read" + title: "Threads for async file system reads" + type: "select" + options: [ + {title: "Sync read", value: 0} + {title: "1 thread", value: 1} + {title: "2 threads", value: 2} + {title: "3 threads", value: 3} + {title: "4 threads", value: 4} + {title: "5 threads", value: 5} + {title: "10 threads", value: 10} + ] + + section.items.push + key: "threads_fs_write" + title: "Threads for async file system writes" + type: "select" + options: [ + {title: "Sync write", value: 0} + {title: "1 thread", value: 1} + {title: "2 threads", value: 2} + {title: "3 threads", value: 3} + {title: "4 threads", value: 4} + {title: "5 threads", value: 5} + {title: "10 threads", value: 10} + ] + createSection: (title) => section = {} section.title = title diff --git a/src/Config.py b/src/Config.py index aefd7348..585106f0 100644 --- a/src/Config.py +++ b/src/Config.py @@ -21,9 +21,10 @@ class Config(object): self.need_restart = False self.keys_api_change_allowed = set([ "tor", "fileserver_port", "language", "tor_use_bridges", "trackers_proxy", "trackers", - "trackers_file", "open_browser", "log_level", "fileserver_ip_type", "ip_external", "offline" + "trackers_file", "open_browser", "log_level", "fileserver_ip_type", "ip_external", "offline", + "threads_fs_read", "threads_fs_write" ]) - self.keys_restart_need = set(["tor", "fileserver_port", "fileserver_ip_type"]) + self.keys_restart_need = set(["tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write"]) self.start_dir = self.getStartDir() self.config_file = self.start_dir + "/zeronet.conf" From 9299e5b61421c59b0380e026e65427b1f3639504 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 20 Nov 2019 14:07:33 +0100 Subject: [PATCH 224/483] Kill greenlets with notify --- src/Site/SiteAnnouncer.py | 2 +- src/util/GreenletManager.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index 309f2a96..cfa16ab2 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -106,7 +106,7 @@ class SiteAnnouncer(object): gevent.joinall(threads, timeout=20) # Wait for announce finish for thread in threads: - if thread.value is None or type(thread.value) is GreenletExit: + if thread.value is None: continue if thread.value is not False: if thread.value > 1.0: # Takes more than 1 second to announce diff --git a/src/util/GreenletManager.py b/src/util/GreenletManager.py index 6379f97c..7245d05c 100644 --- a/src/util/GreenletManager.py +++ b/src/util/GreenletManager.py @@ -1,4 +1,5 @@ import gevent +from Debug import Debug class GreenletManager: @@ -17,7 +18,7 @@ class GreenletManager: self.greenlets.add(greenlet) return greenlet - def stopGreenlets(self, reason="Stopping greenlets"): + def stopGreenlets(self, reason="Stopping all greenlets"): num = len(self.greenlets) - gevent.killall(list(self.greenlets)) + gevent.killall(list(self.greenlets), Debug.Notify(reason), block=False) return num From a5f8a531967b53e8b3adad6c96c70596fd7e1b81 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 20 Nov 2019 14:08:02 +0100 Subject: [PATCH 225/483] Fix change detection for integers on config interface --- plugins/UiConfig/media/js/ConfigView.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UiConfig/media/js/ConfigView.coffee b/plugins/UiConfig/media/js/ConfigView.coffee index a110a17d..64b86e5e 100644 --- a/plugins/UiConfig/media/js/ConfigView.coffee +++ b/plugins/UiConfig/media/js/ConfigView.coffee @@ -118,7 +118,7 @@ class ConfigView extends Class renderValueSelect: (item) => h("select.input-select", {config_key: item.key, oninput: @handleInputChange}, item.options.map (option) => - h("option", {selected: option.value == @values[item.key], value: option.value}, option.title) + h("option", {selected: option.value.toString() == @values[item.key], value: option.value}, option.title) ) window.ConfigView = ConfigView \ No newline at end of file From d85c27e67b7ac798bbb58a93e4bbf3e11cfc7050 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 20 Nov 2019 14:08:19 +0100 Subject: [PATCH 226/483] Merge config js --- plugins/UiConfig/media/js/all.js | 62 ++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index 4bf524e0..68fe5268 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1464,7 +1464,7 @@ })(this) }); section = this.createSection("Performance"); - return section.items.push({ + section.items.push({ key: "log_level", title: "Level of logging to file", type: "select", @@ -1481,6 +1481,64 @@ } ] }); + section.items.push({ + key: "threads_fs_read", + title: "Threads for async file system reads", + type: "select", + options: [ + { + title: "Sync read", + value: 0 + }, { + title: "1 thread", + value: 1 + }, { + title: "2 threads", + value: 2 + }, { + title: "3 threads", + value: 3 + }, { + title: "4 threads", + value: 4 + }, { + title: "5 threads", + value: 5 + }, { + title: "10 threads", + value: 10 + } + ] + }); + return section.items.push({ + key: "threads_fs_write", + title: "Threads for async file system writes", + type: "select", + options: [ + { + title: "Sync write", + value: 0 + }, { + title: "1 thread", + value: 1 + }, { + title: "2 threads", + value: 2 + }, { + title: "3 threads", + value: 3 + }, { + title: "4 threads", + value: 4 + }, { + title: "5 threads", + value: 5 + }, { + title: "10 threads", + value: 10 + } + ] + }); }; ConfigStorage.prototype.createSection = function(title) { @@ -1689,7 +1747,7 @@ }, item.options.map((function(_this) { return function(option) { return h("option", { - selected: option.value === _this.values[item.key], + selected: option.value.toString() === _this.values[item.key], value: option.value }, option.title); }; From 966f393e20def207965aa5012a24e7d45d199c9c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 20 Nov 2019 14:08:49 +0100 Subject: [PATCH 227/483] Rev4290 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 585106f0..2d51fe52 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4287 + self.rev = 4290 self.argv = argv self.action = None self.test_parser = None From 89e8fd3d3ac2207087e6462b45a398d1ff00378f Mon Sep 17 00:00:00 2001 From: d9xr92 <46283343+antidepressant044@users.noreply.github.com> Date: Sat, 23 Nov 2019 16:22:36 +0400 Subject: [PATCH 228/483] potential fix for #2323 (#2324) * potential fix for #2323 * Update DbCursor.py * replaced RLock with Lock --- src/Db/DbCursor.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 91eaa313..07639130 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -1,6 +1,7 @@ import time import re import gevent +from gevent._threading import Lock from util import helper @@ -14,6 +15,7 @@ class DbCursor: self.db = db self.cursor = conn.cursor() self.logging = False + self.lock = Lock() def quoteValue(self, value): if type(value) is int: @@ -98,15 +100,19 @@ class DbCursor: query, params = self.parseQuery(query, params) s = time.time() - - if params: # Query has parameters - res = self.cursor.execute(query, params) - if self.logging: - self.db.log.debug(query + " " + str(params) + " (Done in %.4f)" % (time.time() - s)) - else: - res = self.cursor.execute(query) - if self.logging: - self.db.log.debug(query + " (Done in %.4f)" % (time.time() - s)) + + try: + self.lock.acquire(True) + if params: # Query has parameters + res = self.cursor.execute(query, params) + if self.logging: + self.db.log.debug(query + " " + str(params) + " (Done in %.4f)" % (time.time() - s)) + else: + res = self.cursor.execute(query) + if self.logging: + self.db.log.debug(query + " (Done in %.4f)" % (time.time() - s)) + finally: + self.lock.release() # Log query stats if self.db.collect_stats: From c21fe3d23a2ab0d07ff9f6db17fa8a8bdb54b6fe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:30:51 +0100 Subject: [PATCH 229/483] Prefer connecting to non-onion peers --- plugins/AnnounceZero/AnnounceZeroPlugin.py | 16 +++++++++------- src/Peer/Peer.py | 5 ++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/AnnounceZero/AnnounceZeroPlugin.py b/plugins/AnnounceZero/AnnounceZeroPlugin.py index dcaa04f0..7f31e052 100644 --- a/plugins/AnnounceZero/AnnounceZeroPlugin.py +++ b/plugins/AnnounceZero/AnnounceZeroPlugin.py @@ -21,6 +21,15 @@ def importHostClasses(): # Process result got back from tracker def processPeerRes(tracker_address, site, peers): added = 0 + + # Onion + found_onion = 0 + for packed_address in peers["onion"]: + found_onion += 1 + peer_onion, peer_port = helper.unpackOnionAddress(packed_address) + if site.addPeer(peer_onion, peer_port, source="tracker"): + added += 1 + # Ip4 found_ipv4 = 0 peers_normal = itertools.chain(peers.get("ip4", []), peers.get("ipv4", []), peers.get("ipv6", [])) @@ -29,13 +38,6 @@ def processPeerRes(tracker_address, site, peers): peer_ip, peer_port = helper.unpackAddress(packed_address) if site.addPeer(peer_ip, peer_port, source="tracker"): added += 1 - # Onion - found_onion = 0 - for packed_address in peers["onion"]: - found_onion += 1 - peer_onion, peer_port = helper.unpackOnionAddress(packed_address) - if site.addPeer(peer_onion, peer_port, source="tracker"): - added += 1 if added: site.worker_manager.onPeers() diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index b5b22436..60113294 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -130,7 +130,10 @@ class Peer(object): def found(self, source="other"): if self.reputation < 5: if source == "tracker": - self.reputation += 1 + if self.ip.endswith(".onion"): + self.reputation += 1 + else: + self.reputation += 2 elif source == "local": self.reputation += 3 From a14c36cd3e4c402efeb4388e6461dac3d9a42b3f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:31:12 +0100 Subject: [PATCH 230/483] Add peer's site to str represetntation --- src/Peer/Peer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 60113294..6fa63f60 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -115,7 +115,10 @@ class Peer(object): return self.connection def __str__(self): - return "Peer:%-12s" % self.ip + if self.site: + return "Peer:%-12s of %s" % (self.ip, self.site.address_short) + else: + return "Peer:%-12s" % self.ip def __repr__(self): return "<%s>" % self.__str__() From 6ff7fe55fca0ae9eaf3bd6aa336fedc063780d08 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:32:06 +0100 Subject: [PATCH 231/483] Make sure we use local peers if possible --- src/Peer/Peer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index 6fa63f60..b2cef0c1 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -138,7 +138,7 @@ class Peer(object): else: self.reputation += 2 elif source == "local": - self.reputation += 3 + self.reputation += 20 if source in ("tracker", "local"): self.site.peers_recent.appendleft(self) From 07633ba79db633b34e8e43bcff0bae1172bc4fce Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:33:18 +0100 Subject: [PATCH 232/483] Fix local peers dropping out from recent peers --- src/Site/Site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 807507ee..53b901e7 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -42,7 +42,7 @@ class Site(object): self.content = None # Load content.json self.peers = {} # Key: ip:port, Value: Peer.Peer - self.peers_recent = collections.deque(maxlen=100) + self.peers_recent = collections.deque(maxlen=150) self.peer_blacklist = SiteManager.peer_blacklist # Ignore this peers (eg. myself) self.greenlet_manager = GreenletManager.GreenletManager() # Running greenlets self.worker_manager = WorkerManager(self) # Handle site download from other peers From c14e722303ddba6c91da70571e3131f049755077 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:34:46 +0100 Subject: [PATCH 233/483] Fix bug that someomes blocked plugins accessing connectionserver sitelist --- src/File/FileServer.py | 2 +- src/Test/TestFileRequest.py | 2 +- src/Test/TestPeer.py | 6 +++--- src/Test/TestSiteDownload.py | 18 ++++++++++-------- src/Test/TestTor.py | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/File/FileServer.py b/src/File/FileServer.py index a6800f8f..68be3a4b 100644 --- a/src/File/FileServer.py +++ b/src/File/FileServer.py @@ -65,7 +65,7 @@ class FileServer(ConnectionServer): self.port_opened = {} - self.sites = {} + self.sites = self.site_manager.sites self.last_request = time.time() self.files_parsing = {} self.ui_server = None diff --git a/src/Test/TestFileRequest.py b/src/Test/TestFileRequest.py index 6a8d634a..3fabc271 100644 --- a/src/Test/TestFileRequest.py +++ b/src/Test/TestFileRequest.py @@ -91,7 +91,7 @@ class TestFileRequest: def testPex(self, file_server, site, site_temp): file_server.sites[site.address] = site client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client connection = client.getConnection(file_server.ip, 1544) diff --git a/src/Test/TestPeer.py b/src/Test/TestPeer.py index f7bdb6da..f57e046e 100644 --- a/src/Test/TestPeer.py +++ b/src/Test/TestPeer.py @@ -15,7 +15,7 @@ class TestPeer: def testPing(self, file_server, site, site_temp): file_server.sites[site.address] = site client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client connection = client.getConnection(file_server.ip, 1544) @@ -34,7 +34,7 @@ class TestPeer: def testDownloadFile(self, file_server, site, site_temp): file_server.sites[site.address] = site client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client connection = client.getConnection(file_server.ip, 1544) @@ -129,7 +129,7 @@ class TestPeer: def testFindHash(self, file_server, site, site_temp): file_server.sites[site.address] = site client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client # Add file_server as peer to client diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index 0d260128..34783238 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -26,7 +26,7 @@ class TestSiteDownload: # Init client server client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client site_temp.announce = mock.MagicMock(return_value=True) # Don't try to find peers from the net @@ -35,6 +35,8 @@ class TestSiteDownload: site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.storage.isFile("content.json") + # Rename non-optional file os.rename(site.storage.getPath("data/img/domain.png"), site.storage.getPath("data/img/domain-new.png")) @@ -75,7 +77,7 @@ class TestSiteDownload: # Init client server client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client site_temp.announce = mock.MagicMock(return_value=True) # Don't try to find peers from the net @@ -131,7 +133,7 @@ class TestSiteDownload: # Init client server client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client # Download normally @@ -179,7 +181,7 @@ class TestSiteDownload: # Init client server client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client # Download normally @@ -343,7 +345,7 @@ class TestSiteDownload: # Init client server client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client # Don't try to find peers from the net @@ -419,7 +421,7 @@ class TestSiteDownload: # Init client server client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client # Connect peers @@ -470,7 +472,7 @@ class TestSiteDownload: # Init client server client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client # Connect peers @@ -517,7 +519,7 @@ class TestSiteDownload: # Init client server client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client site_temp.announce = mock.MagicMock(return_value=True) # Don't try to find peers from the net diff --git a/src/Test/TestTor.py b/src/Test/TestTor.py index 63ff47f9..0252d73a 100644 --- a/src/Test/TestTor.py +++ b/src/Test/TestTor.py @@ -117,7 +117,7 @@ class TestTor: file_server.tor_manager = tor_manager client = FileServer(file_server.ip, 1545) - client.sites[site_temp.address] = site_temp + client.sites = {site_temp.address: site_temp} site_temp.connection_server = client # Add file_server as peer to client From 9a43626aa650bef34eecdccf2fc06841acbe04cc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:35:16 +0100 Subject: [PATCH 234/483] When testing don't register shutdown functions --- src/Test/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 0b7e97e9..e86c3314 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -17,6 +17,8 @@ import gevent.event from gevent import monkey monkey.patch_all(thread=False, subprocess=False) +atexit_register = atexit.register +atexit.register = lambda func: "" # Don't register shutdown functions to avoid IO error on exit def pytest_addoption(parser): parser.addoption("--slow", action='store_true', default=False, help="Also run slow tests") @@ -118,7 +120,7 @@ def cleanup(): if os.path.isfile(file_path): os.unlink(file_path) -atexit.register(cleanup) +atexit_register(cleanup) @pytest.fixture(scope="session") def resetSettings(request): From c52d47b15f30d5ec6fffb5fa0a25fa677ba5937d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:35:31 +0100 Subject: [PATCH 235/483] Don't show notifications when testing --- src/Test/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index e86c3314..5d0b6dc0 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -107,6 +107,7 @@ from util import RateLimit from Db import Db from Debug import Debug +gevent.get_hub().NOT_ERROR += (Debug.Notify,) def cleanup(): Db.dbCloseAll() From 7b210429b50e48de509ef00399604b338789c468 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:37:55 +0100 Subject: [PATCH 236/483] Multi threaded eciesDecrypt --- plugins/CryptMessage/CryptMessage.py | 15 +++++++++ plugins/CryptMessage/CryptMessagePlugin.py | 31 ++++++++++++++----- .../UiConfig/media/js/ConfigStorage.coffee | 14 +++++++++ plugins/UiConfig/media/js/all.js | 31 ++++++++++++++++++- src/Config.py | 5 +-- src/Crypt/Crypt.py | 4 +++ 6 files changed, 89 insertions(+), 11 deletions(-) create mode 100644 src/Crypt/Crypt.py diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index 6f2cbdc2..b7286e2e 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -2,6 +2,7 @@ import hashlib import base64 import lib.pybitcointools as btctools +from Crypt import Crypt ecc_cache = {} @@ -22,10 +23,24 @@ def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): mac = pyelliptic.hmac_sha256(key_m, ciphertext) return key_e, ciphertext + mac + +@Crypt.thread_pool_crypt.wrap +def eciesDecryptMulti(encrypted_datas, privatekey): + texts = [] # Decoded texts + for encrypted_data in encrypted_datas: + try: + text = eciesDecrypt(encrypted_data, privatekey).decode("utf8") + texts.append(text) + except: + texts.append(None) + return texts + + def eciesDecrypt(encrypted_data, privatekey): ecc_key = getEcc(privatekey) return ecc_key.decrypt(base64.b64decode(encrypted_data)) + def split(encrypted): iv = encrypted[0:16] ciphertext = encrypted[16 + 70:-32] diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index c3907669..45afe184 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -1,10 +1,12 @@ import base64 import os +import gevent + from Plugin import PluginManager from Crypt import CryptBitcoin, CryptHash import lib.pybitcointools as btctools - +from Config import config from . import CryptMessage @@ -44,13 +46,7 @@ class UiWebsocketPlugin(object): else: encrypted_texts = [param] - texts = [] # Decoded texts - for encrypted_text in encrypted_texts: - try: - text = CryptMessage.eciesDecrypt(encrypted_text, privatekey).decode("utf8") - texts.append(text) - except Exception as err: - texts.append(None) + texts = CryptMessage.eciesDecryptMulti(encrypted_texts, privatekey) if type(param) == list: self.response(to, texts) @@ -188,6 +184,7 @@ class ActionsPlugin: tests.extend([ {"func": self.testCryptEciesEncrypt, "kwargs": {}, "num": 100, "time_standard": 1.2}, {"func": self.testCryptEciesDecrypt, "kwargs": {}, "num": 500, "time_standard": 1.3}, + {"func": self.testCryptEciesDecryptMulti, "kwargs": {}, "num": 5, "time_standard": 0.68}, {"func": self.testCryptAesEncrypt, "kwargs": {}, "num": 10000, "time_standard": 0.27}, {"func": self.testCryptAesDecrypt, "kwargs": {}, "num": 10000, "time_standard": 0.25} ]) @@ -207,6 +204,24 @@ class ActionsPlugin: assert ecc.decrypt(encrypted) == self.utf8_text.encode("utf8"), "%s != %s" % (ecc.decrypt(encrypted), self.utf8_text.encode("utf8")) yield "." + def testCryptEciesDecryptMulti(self, num_run=1): + yield "x 100 (%s threads) " % config.threads_crypt + aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) + + threads = [] + for i in range(num_run): + assert len(aes_key) == 32 + threads.append(gevent.spawn( + CryptMessage.eciesDecryptMulti, [base64.b64encode(encrypted)] * 100, self.privatekey + )) + + for thread in threads: + res = thread.get() + assert res[0] == self.utf8_text, "%s != %s" % (res[0], self.utf8_text) + assert res[0] == res[-1], "%s != %s" % (res[0], res[-1]) + yield "." + gevent.joinall(threads) + def testCryptAesEncrypt(self, num_run=1): from lib import pyelliptic diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index 4d9684cc..642bae45 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -177,6 +177,20 @@ class ConfigStorage extends Class {title: "10 threads", value: 10} ] + section.items.push + key: "threads_crypt" + title: "Threads for cryptographic functions" + type: "select" + options: [ + {title: "Sync execution", value: 0} + {title: "1 thread", value: 1} + {title: "2 threads", value: 2} + {title: "3 threads", value: 3} + {title: "4 threads", value: 4} + {title: "5 threads", value: 5} + {title: "10 threads", value: 10} + ] + createSection: (title) => section = {} section.title = title diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index 68fe5268..b2c09e39 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1510,7 +1510,7 @@ } ] }); - return section.items.push({ + section.items.push({ key: "threads_fs_write", title: "Threads for async file system writes", type: "select", @@ -1539,6 +1539,35 @@ } ] }); + return section.items.push({ + key: "threads_crypt", + title: "Threads for cryptographic functions", + type: "select", + options: [ + { + title: "Sync execution", + value: 0 + }, { + title: "1 thread", + value: 1 + }, { + title: "2 threads", + value: 2 + }, { + title: "3 threads", + value: 3 + }, { + title: "4 threads", + value: 4 + }, { + title: "5 threads", + value: 5 + }, { + title: "10 threads", + value: 10 + } + ] + }); }; ConfigStorage.prototype.createSection = function(title) { diff --git a/src/Config.py b/src/Config.py index 2d51fe52..4c2373bd 100644 --- a/src/Config.py +++ b/src/Config.py @@ -22,9 +22,9 @@ class Config(object): self.keys_api_change_allowed = set([ "tor", "fileserver_port", "language", "tor_use_bridges", "trackers_proxy", "trackers", "trackers_file", "open_browser", "log_level", "fileserver_ip_type", "ip_external", "offline", - "threads_fs_read", "threads_fs_write" + "threads_fs_read", "threads_fs_write", "threads_crypt" ]) - self.keys_restart_need = set(["tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write"]) + self.keys_restart_need = set(["tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt"]) self.start_dir = self.getStartDir() self.config_file = self.start_dir + "/zeronet.conf" @@ -283,6 +283,7 @@ class Config(object): self.parser.add_argument("--db_mode", choices=["speed", "security"], default="speed") self.parser.add_argument('--threads_fs_read', help='Number of threads for file read operations', default=1, type=int) self.parser.add_argument('--threads_fs_write', help='Number of threads for file write operations', default=1, type=int) + self.parser.add_argument('--threads_crypt', help='Number of threads for cryptographic operations', default=2, type=int) self.parser.add_argument("--download_optional", choices=["manual", "auto"], default="manual") self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript, diff --git a/src/Crypt/Crypt.py b/src/Crypt/Crypt.py new file mode 100644 index 00000000..7d7d3659 --- /dev/null +++ b/src/Crypt/Crypt.py @@ -0,0 +1,4 @@ +from Config import config +from util import ThreadPool + +thread_pool_crypt = ThreadPool.ThreadPool(config.threads_crypt) \ No newline at end of file From 416e7d6fe0333ced4d76e15fe687bf5ee0061265 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:38:27 +0100 Subject: [PATCH 237/483] Fix too fast benchmark results statistics --- plugins/Benchmark/BenchmarkPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index 3c588b6c..73b95d22 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -181,7 +181,7 @@ class ActionsPlugin: yield self.formatResult(time_taken, time_standard) yield "\n" res[key] = "ok" - multiplers.append(time_standard / time_taken) + multiplers.append(time_standard / max(time_taken, 0.001)) except Exception as err: res[key] = err yield "Failed!\n! Error: %s\n\n" % Debug.formatException(err) @@ -193,7 +193,7 @@ class ActionsPlugin: yield " - Total: %s tests\n" % len(res) yield " - Success: %s tests\n" % len([res_key for res_key, res_val in res.items() if res_val == "ok"]) yield " - Failed: %s tests\n" % len([res_key for res_key, res_val in res.items() if res_val != "ok"]) - if multiplers: + if any(multiplers): multipler_avg = sum(multiplers) / len(multiplers) multipler_title = self.getMultiplerTitle(multipler_avg) yield " - Average speed factor: %.2fx (%s)" % (multipler_avg, multipler_title) From 756f5a1608f86e6b3dba331ed010a034b273fe92 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:38:53 +0100 Subject: [PATCH 238/483] Fix display peer found time on /Stats page --- plugins/Stats/StatsPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Stats/StatsPlugin.py b/plugins/Stats/StatsPlugin.py index ca7c8281..f164ce8f 100644 --- a/plugins/Stats/StatsPlugin.py +++ b/plugins/Stats/StatsPlugin.py @@ -222,7 +222,7 @@ class UiRequestPlugin(object): if site.content_manager.has_optional_files: yield "Optional files: %4s " % len(peer.hashfield) time_added = (time.time() - peer.time_added) / (60 * 60 * 24) - yield "(#%4s, rep: %2s, err: %s, found: %3s min, add: %.1f day) %30s -
" % (connection_id, peer.reputation, peer.connection_error, time_found, time_added, key) + yield "(#%4s, rep: %2s, err: %s, found: %.1fs min, add: %.1f day) %30s -
" % (connection_id, peer.reputation, peer.connection_error, time_found, time_added, key) yield "
" yield "" From 4f8e941e39303d74adfbffee30398b93a0572969 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:39:24 +0100 Subject: [PATCH 239/483] Fix err type logging --- src/Debug/Debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Debug/Debug.py b/src/Debug/Debug.py index 18fb2e29..efaf98be 100644 --- a/src/Debug/Debug.py +++ b/src/Debug/Debug.py @@ -49,7 +49,7 @@ def formatException(err=None, format="text"): file_title = file_name tb.append("%s line %s" % (file_title, line)) if format == "html": - return "%s: %s
%s" % (exc_type.__name__, err, " > ".join(tb)) + return "%s: %s
%s" % (repr(err), err, " > ".join(tb)) else: return "%s: %s in %s" % (exc_type.__name__, err, " > ".join(tb)) From 29346cdef5b821916eea4da2979c1f873dd0aacd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:40:52 +0100 Subject: [PATCH 240/483] Faster, async local ip discovery --- src/util/UpnpPunch.py | 69 +++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/util/UpnpPunch.py b/src/util/UpnpPunch.py index 339bcff9..4b02717a 100644 --- a/src/util/UpnpPunch.py +++ b/src/util/UpnpPunch.py @@ -17,6 +17,7 @@ import gevent logger = logging.getLogger("Upnp") + class UpnpError(Exception): pass @@ -128,33 +129,47 @@ def _parse_igd_profile(profile_xml): # add description def _get_local_ips(): + def method1(): + try: + # get local ip using UDP and a broadcast address + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + # Not using because gevents getaddrinfo doesn't like that + # using port 1 as per hobbldygoop's comment about port 0 not working on osx: + # https://github.com/sirMackk/ZeroNet/commit/fdcd15cf8df0008a2070647d4d28ffedb503fba2#commitcomment-9863928 + s.connect(('239.255.255.250', 1)) + return [s.getsockname()[0]] + except: + pass + + def method2(): + # Get ip by using UDP and a normal address (google dns ip) + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 0)) + return [s.getsockname()[0]] + except: + pass + + def method3(): + # Get ip by '' hostname . Not supported on all platforms. + try: + return socket.gethostbyname_ex('')[2] + except: + pass + + threads = [ + gevent.spawn(method1), + gevent.spawn(method2), + gevent.spawn(method3) + ] + + gevent.joinall(threads, timeout=5) + local_ips = [] - - try: - # get local ip using UDP and a broadcast address - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - # Not using because gevents getaddrinfo doesn't like that - # using port 1 as per hobbldygoop's comment about port 0 not working on osx: - # https://github.com/sirMackk/ZeroNet/commit/fdcd15cf8df0008a2070647d4d28ffedb503fba2#commitcomment-9863928 - s.connect(('239.255.255.250', 1)) - local_ips.append(s.getsockname()[0]) - except: - pass - - # Get ip by using UDP and a normal address (google dns ip) - try: - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - s.connect(('8.8.8.8', 0)) - local_ips.append(s.getsockname()[0]) - except: - pass - - # Get ip by '' hostname . Not supported on all platforms. - try: - local_ips += socket.gethostbyname_ex('')[2] - except: - pass + for thread in threads: + if thread.value: + local_ips += thread.value # Delete duplicates local_ips = list(set(local_ips)) @@ -373,8 +388,6 @@ if __name__ == "__main__": print("Success:", ask_to_open_port(15443, "ZeroNet", protos=["TCP"])) print("Done in", time.time() - s) - print("Closing port...") print("Success:", ask_to_close_port(15443, "ZeroNet", protos=["TCP"])) print("Done in", time.time() - s) - From 66a950a48149f4a8be09d75ac408174c3483d0b2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:43:28 +0100 Subject: [PATCH 241/483] New, much faster worker task sorting --- src/Content/ContentManager.py | 2 +- src/Test/TestWorkerTaskManager.py | 92 +++++++++++++++++++++++ src/Worker/Worker.py | 11 ++- src/Worker/WorkerManager.py | 38 ++++++---- src/Worker/WorkerTaskManager.py | 119 ++++++++++++++++++++++++++++++ 5 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 src/Test/TestWorkerTaskManager.py create mode 100644 src/Worker/WorkerTaskManager.py diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index a6e00def..fe2a74dd 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -870,7 +870,7 @@ class ContentManager(object): if content_size_file > site_size_limit: # Save site size to display warning self.site.settings["size"] = site_size - task = self.site.worker_manager.findTask(inner_path) + task = self.site.worker_manager.tasks.findTask(inner_path) if task: # Dont try to download from other peers self.site.worker_manager.failTask(task) raise VerifyError("Content too large %s B > %s B, aborting task..." % (site_size, site_size_limit)) diff --git a/src/Test/TestWorkerTaskManager.py b/src/Test/TestWorkerTaskManager.py new file mode 100644 index 00000000..375100c9 --- /dev/null +++ b/src/Test/TestWorkerTaskManager.py @@ -0,0 +1,92 @@ +import pytest + +from Worker import WorkerTaskManager +from . import Spy + + +class TestUiWebsocket: + def checkSort(self, tasks): # Check if it has the same order as a list sorted separately + tasks_list = list(tasks) + tasks_list.sort(key=lambda task: task["id"]) + assert tasks_list != list(tasks) + tasks_list.sort(key=lambda task: (0 - (task["priority"] - task["workers_num"] * 10), task["id"])) + assert tasks_list == list(tasks) + + def testAppendSimple(self): + tasks = WorkerTaskManager.WorkerTaskManager() + tasks.append({"id": 1, "priority": 15, "workers_num": 1, "inner_path": "file1.json"}) + tasks.append({"id": 2, "priority": 1, "workers_num": 0, "inner_path": "file2.json"}) + tasks.append({"id": 3, "priority": 8, "workers_num": 0, "inner_path": "file3.json"}) + assert [task["inner_path"] for task in tasks] == ["file3.json", "file1.json", "file2.json"] + + self.checkSort(tasks) + + def testAppendMany(self): + tasks = WorkerTaskManager.WorkerTaskManager() + for i in range(1000): + tasks.append({"id": i, "priority": i % 20, "workers_num": i % 3, "inner_path": "file%s.json" % i}) + assert tasks[0]["inner_path"] == "file39.json" + assert tasks[-1]["inner_path"] == "file980.json" + + self.checkSort(tasks) + + def testRemove(self): + tasks = WorkerTaskManager.WorkerTaskManager() + for i in range(1000): + tasks.append({"id": i, "priority": i % 20, "workers_num": i % 3, "inner_path": "file%s.json" % i}) + + i = 333 + task = {"id": i, "priority": i % 20, "workers_num": i % 3, "inner_path": "file%s.json" % i} + assert task in tasks + + tasks.remove(task) + + assert task not in tasks + + self.checkSort(tasks) + + def testModify(self): + tasks = WorkerTaskManager.WorkerTaskManager() + for i in range(1000): + tasks.append({"id": i, "priority": i % 20, "workers_num": i % 3, "inner_path": "file%s.json" % i}) + + task = tasks[333] + task["priority"] += 10 + + with pytest.raises(AssertionError): + self.checkSort(tasks) + + with Spy.Spy(tasks, "indexSlow") as calls: + tasks.updateItem(task) + assert len(calls) == 1 + + assert task in tasks + + self.checkSort(tasks) + + # Check reorder optimization + + with Spy.Spy(tasks, "indexSlow") as calls: + tasks.updateItem(task, "priority", task["priority"] + 10) + assert len(calls) == 0 + + self.checkSort(tasks) + + def testIn(self): + tasks = WorkerTaskManager.WorkerTaskManager() + + i = 1 + task = {"id": i, "priority": i % 20, "workers_num": i % 3, "inner_path": "file%s.json" % i} + + assert task not in tasks + + + def testFindTask(self): + tasks = WorkerTaskManager.WorkerTaskManager() + for i in range(1000): + tasks.append({"id": i, "priority": i % 20, "workers_num": i % 3, "inner_path": "file%s.json" % i}) + + assert tasks.findTask("file999.json") + assert not tasks.findTask("file-unknown.json") + tasks.remove(tasks.findTask("file999.json")) + assert not tasks.findTask("file999.json") diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 89c4ccf6..fb6ce443 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -80,7 +80,8 @@ class Worker(object): self.task = task site = task["site"] - task["workers_num"] += 1 + self.manager.addTaskWorker(task, self) + error_message = "Unknown error" try: buff = self.peer.getFile(site.address, task["inner_path"], task["size"]) @@ -114,6 +115,7 @@ class Worker(object): except Exception as err: self.manager.log.error("%s: Error writing: %s (%s)" % (self.key, task["inner_path"], err)) write_error = err + if task["done"] is False: if write_error: self.manager.failTask(task) @@ -121,10 +123,11 @@ class Worker(object): else: self.manager.doneTask(task) self.num_downloaded += 1 - task["workers_num"] -= 1 + + self.manager.removeTaskWorker(task, self) else: # Verify failed self.num_failed += 1 - task["workers_num"] -= 1 + self.manager.removeTaskWorker(task, self) if self.manager.started_task_num < 50 or config.verbose: self.manager.log.debug( "%s: Verify failed: %s, error: %s, failed peers: %s, workers: %s" % @@ -162,4 +165,4 @@ class Worker(object): if self.thread: self.thread.kill(exception=Debug.Notify("Worker stopped")) del self.thread - self.manager.removeWorker(self) + self.manager.removeWorker(self) \ No newline at end of file diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index c35b4b93..b40852dc 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -5,6 +5,7 @@ import collections import gevent from .Worker import Worker +from .WorkerTaskManager import WorkerTaskManager from Config import config from util import helper from Plugin import PluginManager @@ -17,8 +18,9 @@ class WorkerManager(object): def __init__(self, site): self.site = site self.workers = {} # Key: ip:port, Value: Worker.Worker - self.tasks = [] - # {"evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "optional_hash_id": None, + self.tasks = WorkerTaskManager() + self.next_task_id = 1 + # {"id": 1, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "optional_hash_id": None, # "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0, "failed": peer_ids} self.started_task_num = 0 # Last added task num self.asked_peers = [] @@ -115,9 +117,6 @@ class WorkerManager(object): # Returns the next free or less worked task def getTask(self, peer): - # Sort tasks by priority and worker numbers - self.tasks.sort(key=lambda task: task["priority"] - task["workers_num"] * 10, reverse=True) - for task in self.tasks: # Find a task if task["peers"] and peer not in task["peers"]: continue # This peer not allowed to pick this task @@ -212,7 +211,7 @@ class WorkerManager(object): worker = self.addWorker(peer) if worker: - self.log.debug("Added worker: %s, workers: %s/%s" % (peer.key, len(self.workers), max_workers)) + self.log.debug("Added worker: %s (rep: %s), workers: %s/%s" % (peer.key, peer.reputation, len(self.workers), max_workers)) # Find peers for optional hash in local hash tables and add to task peers def findOptionalTasks(self, optional_tasks, reset_task=False): @@ -463,9 +462,10 @@ class WorkerManager(object): # Create new task and return asyncresult def addTask(self, inner_path, peer=None, priority=0, file_info=None): self.site.onFileStart(inner_path) # First task, trigger site download started - task = self.findTask(inner_path) + task = self.tasks.findTask(inner_path) if task: # Already has task for that file - task["priority"] = max(priority, task["priority"]) + if priority > task["priority"]: + self.tasks.updateItem(task, "priority", priority) if peer and task["peers"]: # This peer also has new version, add it to task possible peers task["peers"].append(peer) self.log.debug("Added peer %s to %s" % (peer.key, task["inner_path"])) @@ -497,13 +497,14 @@ class WorkerManager(object): priority += 1 task = { - "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, + "id": self.next_task_id, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "optional_hash_id": optional_hash_id, "time_added": time.time(), "time_started": None, "time_action": None, "peers": peers, "priority": priority, "failed": [], "size": size } self.tasks.append(task) + self.next_task_id += 1 self.started_task_num += 1 if config.verbose: self.log.debug( @@ -525,12 +526,17 @@ class WorkerManager(object): self.startWorkers(peers, reason="Added new task") return task - # Find a task using inner_path - def findTask(self, inner_path): - for task in self.tasks: - if task["inner_path"] == inner_path: - return task - return None # Not found + def addTaskWorker(self, task, worker): + if task in self.tasks: + self.tasks.updateItem(task, "workers_num", task["workers_num"] + 1) + else: + task["workers_num"] += 1 + + def removeTaskWorker(self, task, worker): + if task in self.tasks: + self.tasks.updateItem(task, "workers_num", task["workers_num"] - 1) + else: + task["workers_num"] -= 1 # Wait for other tasks def checkComplete(self): @@ -567,4 +573,4 @@ class WorkerManager(object): self.site.onFileFail(task["inner_path"]) task["evt"].set(False) if not self.tasks: - self.started_task_num = 0 + self.started_task_num = 0 \ No newline at end of file diff --git a/src/Worker/WorkerTaskManager.py b/src/Worker/WorkerTaskManager.py new file mode 100644 index 00000000..791f5217 --- /dev/null +++ b/src/Worker/WorkerTaskManager.py @@ -0,0 +1,119 @@ +import bisect +from collections.abc import MutableSequence + + +class CustomSortedList(MutableSequence): + def __init__(self): + super().__init__() + self.items = [] # (priority, added index, actual value) + self.logging = False + + def __repr__(self): + return "<{0} {1}>".format(self.__class__.__name__, self.items) + + def __len__(self): + return len(self.items) + + def __getitem__(self, index): + if self.logging: + print("getitem", index) + if type(index) is int: + return self.items[index][2] + else: + return [item[2] for item in self.items[index]] + + def __delitem__(self, index): + if self.logging: + print("delitem", index) + del self.items[index] + + def __setitem__(self, index, value): + self.items[index] = self.valueToItem(value) + + def __str__(self): + return str(self[:]) + + def insert(self, index, value): + self.append(value) + + def append(self, value): + bisect.insort(self.items, self.valueToItem(value)) + + def updateItem(self, value, update_key=None, update_value=None): + self.remove(value) + if update_key: + value[update_key] = update_value + self.append(value) + + def sort(self, *args, **kwargs): + raise Exception("Sorted list can't be sorted") + + def valueToItem(self, value): + return (self.getPriority(value), self.getId(value), value) + + def getPriority(self, value): + return value + + def getId(self, value): + return id(value) + + def indexSlow(self, value): + for pos, item in enumerate(self.items): + if item[2] == value: + return pos + return None + + def index(self, value): + item = (self.getPriority(value), self.getId(value), value) + bisect_pos = bisect.bisect(self.items, item) - 1 + if bisect_pos >= 0 and self.items[bisect_pos][2] == value: + if self.logging: + print("Fast index for", value) + return bisect_pos + + # Item probably changed since added, switch to slow iteration + pos = self.indexSlow(value) + if pos is not None: + if self.logging: + print("Slow index for %s in pos %s bisect: %s" % (item[2], pos, bisect_pos)) + return pos + raise ValueError("%r not in list" % value) + + def __contains__(self, value): + try: + self.index(value) + return True + except ValueError: + return False + + +class WorkerTaskManager(CustomSortedList): + def __init__(self): + super().__init__() + self.inner_paths = {} + + def getPriority(self, value): + return 0 - (value["priority"] - value["workers_num"] * 10) + + def getId(self, value): + return value["id"] + + def __contains__(self, value): + return value["inner_path"] in self.inner_paths + + # Fast task search by inner_path + + def append(self, task): + if task["inner_path"] in self.inner_paths: + raise ValueError("File %s already has a task" % task["inner_path"]) + super().append(task) + # Create inner path cache for faster lookup by filename + self.inner_paths[task["inner_path"]] = task + + def __delitem__(self, index): + # Remove from inner path cache + del self.inner_paths[self.items[index][2]["inner_path"]] + super().__delitem__(index) + + def findTask(self, inner_path): + return self.inner_paths.get(inner_path, None) From 5df5e25d686c445065cdbec7a39df2ec72bc83e4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:49:40 +0100 Subject: [PATCH 242/483] Better logging of recent peers --- src/Site/Site.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 53b901e7..8399541a 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -891,7 +891,10 @@ class Site(object): # Return: Recently found peers def getRecentPeers(self, need_num): found = list(set(self.peers_recent)) - self.log.debug("Recent peers %s of %s (need: %s)" % (len(found), len(self.peers_recent), need_num)) + self.log.debug( + "Recent peers %s of %s (need: %s)" % + (len(found), len(self.peers), need_num) + ) if len(found) >= need_num or len(found) >= len(self.peers): return sorted( From 97ecb7e3aa4dc016e3b6f7b707d0b2c4492b572e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 25 Nov 2019 14:50:16 +0100 Subject: [PATCH 243/483] Rev4303 --- src/Config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 4c2373bd..182ce941 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4290 + self.rev = 4303 self.argv = argv self.action = None self.test_parser = None @@ -207,6 +207,8 @@ class Config(object): self.test_parser = self.subparsers.add_parser("test", help='Run a test') self.test_parser.add_argument('test_name', help='Test name', nargs="?") + # self.test_parser.add_argument('--benchmark', help='Run the tests multiple times to measure the performance', action='store_true') + # Config parameters self.parser.add_argument('--verbose', help='More detailed logging', action='store_true') self.parser.add_argument('--debug', help='Debug mode', action='store_true') @@ -281,9 +283,12 @@ class Config(object): self.parser.add_argument("--fix_float_decimals", help='Fix content.json modification date float precision on verification', type='bool', choices=[True, False], default=fix_float_decimals) self.parser.add_argument("--db_mode", choices=["speed", "security"], default="speed") + self.parser.add_argument('--threads_fs_read', help='Number of threads for file read operations', default=1, type=int) self.parser.add_argument('--threads_fs_write', help='Number of threads for file write operations', default=1, type=int) + self.parser.add_argument('--threads_db', help='Number of threads for database operations', default=1, type=int) self.parser.add_argument('--threads_crypt', help='Number of threads for cryptographic operations', default=2, type=int) + self.parser.add_argument("--download_optional", choices=["manual", "auto"], default="manual") self.parser.add_argument('--coffeescript_compiler', help='Coffeescript compiler for developing', default=coffeescript, From 8b6f221e22d6037b2757202ede0aaf0e7a11981e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 27 Nov 2019 03:02:18 +0100 Subject: [PATCH 244/483] Formatting --- src/util/UpnpPunch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/UpnpPunch.py b/src/util/UpnpPunch.py index 4b02717a..48b497b6 100644 --- a/src/util/UpnpPunch.py +++ b/src/util/UpnpPunch.py @@ -17,7 +17,6 @@ import gevent logger = logging.getLogger("Upnp") - class UpnpError(Exception): pass @@ -174,6 +173,7 @@ def _get_local_ips(): # Delete duplicates local_ips = list(set(local_ips)) + # Probably we looking for an ip starting with 192 local_ips = sorted(local_ips, key=lambda a: a.startswith("192"), reverse=True) @@ -388,6 +388,8 @@ if __name__ == "__main__": print("Success:", ask_to_open_port(15443, "ZeroNet", protos=["TCP"])) print("Done in", time.time() - s) + print("Closing port...") print("Success:", ask_to_close_port(15443, "ZeroNet", protos=["TCP"])) print("Done in", time.time() - s) + From 777486a5be33abc1329f373099063371db3f59a5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 27 Nov 2019 03:03:22 +0100 Subject: [PATCH 245/483] Try new way to avoid pytest io errors --- src/Test/conftest.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 5d0b6dc0..ebf8cea5 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -413,8 +413,36 @@ def crypt_bitcoin_lib(request, monkeypatch): CryptBitcoin.loadLib(request.param) return CryptBitcoin -# Workaround for pytest>=0.4.1 bug when logging in atexit handlers (I/O operation on closed file) -@pytest.fixture(scope='session', autouse=True) -def disableLog(): + + +def workaroundPytestLogError(): + # Workaround for pytest bug when logging in atexit/post-fixture handlers (I/O operation on closed file) + + import _pytest.capture + write_original = _pytest.capture.EncodedFile.write + + def write_patched(obj, *args, **kwargs): + try: + write_original(obj, *args, **kwargs) + except ValueError as err: + if str(err) == "I/O operation on closed file": + pass + else: + raise err + + def flush_patched(obj, *args, **kwargs): + try: + obj.buffer.flush(*args, **kwargs) + except ValueError as err: + if str(err).startswith("I/O operation on closed file"): + pass + else: + raise err + + _pytest.capture.EncodedFile.write = write_patched + _pytest.capture.EncodedFile.flush = flush_patched + + +workaroundPytestLogError() yield None # Wait until all test done logging.getLogger('').setLevel(logging.getLevelName(logging.CRITICAL)) From 1b2eee058c090afcca6ff2aee6005a094cd925be Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 27 Nov 2019 03:03:31 +0100 Subject: [PATCH 246/483] Log test case start and end --- src/Test/conftest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index ebf8cea5..17416709 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -444,5 +444,9 @@ def workaroundPytestLogError(): workaroundPytestLogError() + +@pytest.fixture(scope='function', autouse=True) +def logCaseStart(request): + logging.info("---- Start test case: %s ----" % request._pyfuncitem) yield None # Wait until all test done - logging.getLogger('').setLevel(logging.getLevelName(logging.CRITICAL)) + logging.info("---- End test case: %s ----" % request._pyfuncitem) From afd23849a62ca58fe069107453d51535ce697b59 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 27 Nov 2019 03:04:49 +0100 Subject: [PATCH 247/483] Log site delete as info --- src/Site/Site.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 8399541a..1ea086ef 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1031,7 +1031,7 @@ class Site(object): return self.settings.get("autodownloadoptional") def delete(self): - self.log.debug("Deleting site...") + self.log.info("Deleting site...") s = time.time() self.settings["serving"] = False self.saveSettings() @@ -1042,7 +1042,7 @@ class Site(object): self.content_manager.contents.db.deleteSite(self) self.updateWebsocket(deleted=True) self.storage.deleteFiles() - self.log.debug( + self.log.info( "Deleted site in %.3fs (greenlets: %s, workers: %s)" % (time.time() - s, num_greenlets, num_workers) ) From fca9db7972f17cca568bcaaac95159e0273609f6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 27 Nov 2019 03:07:08 +0100 Subject: [PATCH 248/483] Try fix Recursive use of cursors ProgrammingError by creating new cursor for every execute and move Lock to db --- plugins/OptionalManager/ContentDbPlugin.py | 4 +- .../OptionalManager/OptionalManagerPlugin.py | 4 +- plugins/PeerDb/PeerDbPlugin.py | 2 +- .../disabled-Bootstrapper/BootstrapperDb.py | 8 +-- src/Db/Db.py | 6 ++- src/Db/DbCursor.py | 54 +++++++++++++------ 6 files changed, 51 insertions(+), 27 deletions(-) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index fd28092b..ccfd6637 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -142,14 +142,14 @@ class ContentDbPlugin(object): if not user: user = UserManager.user_manager.create() auth_address = user.getAuthAddress(site.address) - self.execute( + res = self.execute( "UPDATE file_optional SET is_pinned = 1 WHERE site_id = :site_id AND inner_path LIKE :inner_path", {"site_id": site_id, "inner_path": "%%/%s/%%" % auth_address} ) self.log.debug( "Filled file_optional table for %s in %.3fs (loaded: %s, is_pinned: %s)" % - (site.address, time.time() - s, num, self.cur.cursor.rowcount) + (site.address, time.time() - s, num, res.rowcount) ) self.filled[site.address] = True diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py index 909caa31..c33063dc 100644 --- a/plugins/OptionalManager/OptionalManagerPlugin.py +++ b/plugins/OptionalManager/OptionalManagerPlugin.py @@ -72,12 +72,12 @@ class ContentManagerPlugin(object): return super(ContentManagerPlugin, self).optionalDownloaded(inner_path, hash_id, size, own) def optionalRemoved(self, inner_path, hash_id, size=None): - self.contents.db.execute( + res = self.contents.db.execute( "UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE site_id = :site_id AND inner_path = :inner_path AND is_downloaded = 1", {"site_id": self.contents.db.site_ids[self.site.address], "inner_path": inner_path} ) - if self.contents.db.cur.cursor.rowcount > 0: + if res.rowcount > 0: back = super(ContentManagerPlugin, self).optionalRemoved(inner_path, hash_id, size) # Re-add to hashfield if we have other file with the same hash_id if self.isDownloaded(hash_id=hash_id, force_check_db=True): diff --git a/plugins/PeerDb/PeerDbPlugin.py b/plugins/PeerDb/PeerDbPlugin.py index addd1d6f..2ce5e39f 100644 --- a/plugins/PeerDb/PeerDbPlugin.py +++ b/plugins/PeerDb/PeerDbPlugin.py @@ -79,7 +79,7 @@ class ContentDbPlugin(object): cur = self.getCursor() try: cur.execute("DELETE FROM peer WHERE site_id = :site_id", {"site_id": site_id}) - cur.cursor.executemany( + cur.executemany( "INSERT INTO peer (site_id, address, port, hashfield, reputation, time_added, time_found) VALUES (?, ?, ?, ?, ?, ?, ?)", self.iteratePeers(site) ) diff --git a/plugins/disabled-Bootstrapper/BootstrapperDb.py b/plugins/disabled-Bootstrapper/BootstrapperDb.py index 3c47b76d..0866dc3e 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperDb.py +++ b/plugins/disabled-Bootstrapper/BootstrapperDb.py @@ -79,8 +79,8 @@ class BootstrapperDb(Db.Db): def getHashId(self, hash): if hash not in self.hash_ids: self.log.debug("New hash: %s" % repr(hash)) - self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash}) - self.hash_ids[hash] = self.cur.cursor.lastrowid + res = self.execute("INSERT OR IGNORE INTO hash ?", {"hash": hash}) + self.hash_ids[hash] = res.lastrowid return self.hash_ids[hash] def peerAnnounce(self, ip_type, address, port=None, hashes=[], onion_signed=False, delete_missing_hashes=False): @@ -100,8 +100,8 @@ class BootstrapperDb(Db.Db): self.log.debug("New peer: %s signed: %s" % (address, onion_signed)) if ip_type == "onion" and not onion_signed: return len(hashes) - self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now}) - peer_id = self.cur.cursor.lastrowid + res = self.execute("INSERT INTO peer ?", {"type": ip_type, "address": address, "port": port, "date_announced": now}) + peer_id = res.lastrowid # Check user's hashes res = self.execute("SELECT * FROM peer_to_hash WHERE ?", {"peer_id": peer_id}) diff --git a/src/Db/Db.py b/src/Db/Db.py index 2d1b2e66..63cbe407 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -8,6 +8,7 @@ import atexit import sys import gevent +from gevent._threading import Lock from Debug import Debug from .DbCursor import DbCursor @@ -44,15 +45,18 @@ def dbCloseAll(): for db in opened_dbs[:]: db.close() + gevent.spawn(dbCleanup) gevent.spawn(dbCommitCheck) atexit.register(dbCloseAll) + class DbTableError(Exception): def __init__(self, message, table): super().__init__(message) self.table = table + class Db(object): def __init__(self, schema, db_path, close_idle=False): @@ -76,6 +80,7 @@ class Db(object): self.last_query_time = time.time() self.last_sleep_time = time.time() self.num_execute_since_sleep = 0 + self.lock = Lock() def __repr__(self): return "" % (id(self), self.db_path, self.close_idle) @@ -278,7 +283,6 @@ class Db(object): except Exception as err: self.log.error("Error creating table %s: %s" % (table_name, Debug.formatException(err))) raise DbTableError(err, table_name) - #return False self.log.debug("Db check done in %.3fs, changed tables: %s" % (time.time() - s, changed_tables)) if changed_tables: diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 07639130..201d29d4 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -1,7 +1,5 @@ import time import re -import gevent -from gevent._threading import Lock from util import helper @@ -13,9 +11,7 @@ class DbCursor: def __init__(self, conn, db): self.conn = conn self.db = db - self.cursor = conn.cursor() self.logging = False - self.lock = Lock() def quoteValue(self, value): if type(value) is int: @@ -100,19 +96,20 @@ class DbCursor: query, params = self.parseQuery(query, params) s = time.time() - + cursor = self.conn.cursor() + try: - self.lock.acquire(True) + self.db.lock.acquire(True) if params: # Query has parameters - res = self.cursor.execute(query, params) + res = cursor.execute(query, params) if self.logging: self.db.log.debug(query + " " + str(params) + " (Done in %.4f)" % (time.time() - s)) else: - res = self.cursor.execute(query) + res = cursor.execute(query) if self.logging: self.db.log.debug(query + " (Done in %.4f)" % (time.time() - s)) finally: - self.lock.release() + self.db.lock.release() # Log query stats if self.db.collect_stats: @@ -121,12 +118,35 @@ class DbCursor: self.db.query_stats[query]["call"] += 1 self.db.query_stats[query]["time"] += time.time() - s - if not self.db.need_commit: - query_type = query.split(" ", 1)[0].upper() - if query_type in ["UPDATE", "DELETE", "INSERT", "CREATE"]: - self.db.need_commit = True + query_type = query.split(" ", 1)[0].upper() + is_update_query = query_type in ["UPDATE", "DELETE", "INSERT", "CREATE"] + if not self.db.need_commit and is_update_query: + self.db.need_commit = True - return res + if is_update_query: + return cursor + else: + return res + + def executemany(self, query, params): + while self.db.progress_sleeping: + time.sleep(0.1) + + self.db.last_query_time = time.time() + + s = time.time() + cursor = self.conn.cursor() + + try: + self.db.lock.acquire(True) + cursor.executemany(query, params) + finally: + self.db.lock.release() + + if self.logging: + self.db.log.debug("%s x %s (Done in %.4f)" % (query, len(params), time.time() - s)) + + return cursor # Creates on updates a database row without incrementing the rowid def insertOrUpdate(self, table, query_sets, query_wheres, oninsert={}): @@ -135,11 +155,11 @@ class DbCursor: params = query_sets params.update(query_wheres) - self.execute( + res = self.execute( "UPDATE %s SET %s WHERE %s" % (table, ", ".join(sql_sets), " AND ".join(sql_wheres)), params ) - if self.cursor.rowcount == 0: + if res.rowcount == 0: params.update(oninsert) # Add insert-only fields self.execute("INSERT INTO %s ?" % table, params) @@ -215,4 +235,4 @@ class DbCursor: return row def close(self): - self.cursor.close() + pass From f7c767c1c80f51efec0e997d92e9fac5a33c2889 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 27 Nov 2019 03:07:44 +0100 Subject: [PATCH 249/483] Make Chart plugin compatible with db changes --- plugins/Chart/ChartCollector.py | 6 ++---- plugins/Chart/ChartDb.py | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/plugins/Chart/ChartCollector.py b/plugins/Chart/ChartCollector.py index 776343af..82e54bb6 100644 --- a/plugins/Chart/ChartCollector.py +++ b/plugins/Chart/ChartCollector.py @@ -146,8 +146,7 @@ class ChartCollector(object): s = time.time() cur = self.db.getCursor() - cur.cursor.executemany("INSERT INTO data (type_id, value, date_added) VALUES (?, ?, ?)", values) - cur.close() + cur.executemany("INSERT INTO data (type_id, value, date_added) VALUES (?, ?, ?)", values) self.log.debug("Global collectors inserted in %.3fs" % (time.time() - s)) def collectSites(self, sites, collectors, last_values): @@ -163,8 +162,7 @@ class ChartCollector(object): s = time.time() cur = self.db.getCursor() - cur.cursor.executemany("INSERT INTO data (type_id, site_id, value, date_added) VALUES (?, ?, ?, ?)", values) - cur.close() + cur.executemany("INSERT INTO data (type_id, site_id, value, date_added) VALUES (?, ?, ?, ?)", values) self.log.debug("Site collectors inserted in %.3fs" % (time.time() - s)) def collector(self): diff --git a/plugins/Chart/ChartDb.py b/plugins/Chart/ChartDb.py index 9dd4d3db..66a22082 100644 --- a/plugins/Chart/ChartDb.py +++ b/plugins/Chart/ChartDb.py @@ -48,15 +48,15 @@ class ChartDb(Db): def getTypeId(self, name): if name not in self.types: - self.execute("INSERT INTO type ?", {"name": name}) - self.types[name] = self.cur.cursor.lastrowid + res = self.execute("INSERT INTO type ?", {"name": name}) + self.types[name] = res.lastrowid return self.types[name] def getSiteId(self, address): if address not in self.sites: - self.execute("INSERT INTO site ?", {"address": address}) - self.sites[address] = self.cur.cursor.lastrowid + res = self.execute("INSERT INTO site ?", {"address": address}) + self.sites[address] = res.lastrowid return self.sites[address] From 59e0ffd8e09f8edde9bb165e4695ab33fd770df0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 27 Nov 2019 03:08:01 +0100 Subject: [PATCH 250/483] Remove unnecessary imports from CryptMessage --- plugins/CryptMessage/CryptMessage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index b7286e2e..b6c65673 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -1,12 +1,13 @@ import hashlib import base64 +import binascii import lib.pybitcointools as btctools +from util import ThreadPool from Crypt import Crypt ecc_cache = {} - def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): from lib import pyelliptic pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey)) @@ -40,7 +41,6 @@ def eciesDecrypt(encrypted_data, privatekey): ecc_key = getEcc(privatekey) return ecc_key.decrypt(base64.b64decode(encrypted_data)) - def split(encrypted): iv = encrypted[0:16] ciphertext = encrypted[16 + 70:-32] From e1dc29c374ca31613f09cd47646ad82cef5743d4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 27 Nov 2019 03:08:20 +0100 Subject: [PATCH 251/483] Rev4308 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 182ce941..7737ca2f 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4303 + self.rev = 4308 self.argv = argv self.action = None self.test_parser = None From 1c587bde25e482d579726c3b68ac200ef1b9efdd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:04:59 +0100 Subject: [PATCH 252/483] Avoid write race on same file --- src/Worker/Worker.py | 11 +++++++++-- src/Worker/WorkerManager.py | 6 ++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index fb6ce443..19900a29 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -107,7 +107,9 @@ class Worker(object): if self.manager.started_task_num < 50 or config.verbose: self.manager.log.debug("%s: Verify correct: %s" % (self.key, task["inner_path"])) write_error = None - if correct is True and task["done"] is False: # Save if changed and task not done yet + task_finished = False + if correct is True and task["locked"] is False: # Save if changed and task not done yet + task["locked"] = True buff.seek(0) try: site.storage.write(task["inner_path"], buff) @@ -115,8 +117,13 @@ class Worker(object): except Exception as err: self.manager.log.error("%s: Error writing: %s (%s)" % (self.key, task["inner_path"], err)) write_error = err + task_finished = True - if task["done"] is False: + if correct is None and task["locked"] is False: # Mark as done if same file + task["locked"] = True + task_finished = True + + if task_finished and not task["done"]: if write_error: self.manager.failTask(task) self.num_failed += 1 diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index b40852dc..bce3a3dd 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -21,7 +21,7 @@ class WorkerManager(object): self.tasks = WorkerTaskManager() self.next_task_id = 1 # {"id": 1, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "optional_hash_id": None, - # "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0, "failed": peer_ids} + # "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0, "failed": peer_ids, "locked": False} self.started_task_num = 0 # Last added task num self.asked_peers = [] self.running = True @@ -124,6 +124,8 @@ class WorkerManager(object): continue # Peer already tried to solve this, but failed if task["optional_hash_id"] and task["peers"] is None: continue # No peers found yet for the optional task + if task["done"]: + continue return task def removeSolvedFileTasks(self, mark_as_good=True): @@ -498,7 +500,7 @@ class WorkerManager(object): task = { "id": self.next_task_id, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, - "optional_hash_id": optional_hash_id, "time_added": time.time(), "time_started": None, + "optional_hash_id": optional_hash_id, "time_added": time.time(), "time_started": None, "locked": False, "time_action": None, "peers": peers, "priority": priority, "failed": [], "size": size } From b7c6b848261f100ea97d86491e8840ada93e8fde Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:05:20 +0100 Subject: [PATCH 253/483] Don't log killed worker write as error --- src/Worker/Worker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 19900a29..5b97099a 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -115,7 +115,10 @@ class Worker(object): site.storage.write(task["inner_path"], buff) write_error = False except Exception as err: - self.manager.log.error("%s: Error writing: %s (%s)" % (self.key, task["inner_path"], err)) + if type(err) == Debug.Notify: + self.manager.log.debug("%s: Write aborted: %s (%s)" % (self.key, task["inner_path"], err)) + else: + self.manager.log.error("%s: Error writing: %s (%s)" % (self.key, task["inner_path"], err)) write_error = err task_finished = True From 66a1c4d2425ad7a4016f7eab85639d714fb81172 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:07:30 +0100 Subject: [PATCH 254/483] Multi-process and gevent loop friendly lock --- src/Test/TestThreadPool.py | 46 ++++++++++++++++++++++++++++++++++++++ src/util/ThreadPool.py | 22 +++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py index 6e081206..caf4863a 100644 --- a/src/Test/TestThreadPool.py +++ b/src/Test/TestThreadPool.py @@ -1,3 +1,5 @@ +import time + import gevent from util import ThreadPool @@ -27,3 +29,47 @@ class TestThreadPool: res = blocker() assert res == 10000000 + + def testLockBlockingSameThread(self): + from gevent.lock import Semaphore + + lock = Semaphore() + + s = time.time() + + def unlocker(): + time.sleep(1) + lock.release() + + gevent.spawn(unlocker) + lock.acquire(True) + lock.acquire(True, timeout=2) + + unlock_taken = time.time() - s + + assert 1.0 < unlock_taken < 1.5 + + def testLockBlockingDifferentThread(self): + lock = ThreadPool.Lock() + + s = time.time() + + def locker(): + lock.acquire(True) + time.sleep(1) + lock.release() + + pool = gevent.threadpool.ThreadPool(10) + pool.spawn(locker) + threads = [ + pool.spawn(locker), + ] + time.sleep(0.1) + + lock.acquire(True, 5.0) + + unlock_taken = time.time() - s + + assert 2.0 < unlock_taken < 2.5 + + gevent.joinall(threads) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 8fbb12fd..54f6e699 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -1,4 +1,6 @@ import gevent.threadpool +import gevent._threading +import threading class ThreadPool: @@ -17,6 +19,24 @@ class ThreadPool: return func def wrapper(*args, **kwargs): - return self.pool.apply(func, args, kwargs) + res = self.pool.apply(func, args, kwargs) + return res return wrapper + + +main_thread_id = threading.current_thread().ident +lock_pool = gevent.threadpool.ThreadPool(10) + + +class Lock: + def __init__(self): + self.lock = gevent._threading.Lock() + self.locked = self.lock.locked + self.release = self.lock.release + + def acquire(self, *args, **kwargs): + if self.locked() and threading.current_thread().ident == main_thread_id: + return lock_pool.apply(self.lock.acquire, args, kwargs) + else: + return self.lock.acquire(*args, **kwargs) From fa0d1a50b5d24c35fc068ed54bb410bbc0039c8f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:07:40 +0100 Subject: [PATCH 255/483] Better test of threadpool --- src/Test/TestThreadPool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py index caf4863a..2a67f181 100644 --- a/src/Test/TestThreadPool.py +++ b/src/Test/TestThreadPool.py @@ -16,6 +16,8 @@ class TestThreadPool: events.append("S") out = 0 for i in range(10000000): + if i == 5000000: + events.append("M") out += 1 events.append("D") return out @@ -25,7 +27,7 @@ class TestThreadPool: threads.append(gevent.spawn(blocker)) gevent.joinall(threads) - assert events == ["S"] * 4 + ["D"] * 4 + assert events == ["S"] * 4 + ["M"] * 4 + ["D"] * 4 res = blocker() assert res == 10000000 From c10dd5239e2dd8a5d8a8846156d93deda76a7e83 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:08:11 +0100 Subject: [PATCH 256/483] Log test case start/end and debug message --- src/Test/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 17416709..c703b77e 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -447,6 +447,6 @@ workaroundPytestLogError() @pytest.fixture(scope='function', autouse=True) def logCaseStart(request): - logging.info("---- Start test case: %s ----" % request._pyfuncitem) + logging.debug("---- Start test case: %s ----" % request._pyfuncitem) yield None # Wait until all test done - logging.info("---- End test case: %s ----" % request._pyfuncitem) + logging.debug("---- End test case: %s ----" % request._pyfuncitem) From f0c10efca6b08fdebbe9a4b1ab7eabc4f57df9fe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:08:29 +0100 Subject: [PATCH 257/483] Progress meter for site delete --- src/Site/SiteStorage.py | 51 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 49d9e7d1..260f6827 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -248,6 +248,7 @@ class SiteStorage(object): os.makedirs(file_dir) # Write file if hasattr(content, 'read'): # File-like object + with open(file_path, "wb") as file: shutil.copyfileobj(content, file) # Write buff to disk else: # Simple string @@ -517,9 +518,13 @@ class SiteStorage(object): # Delete site's all file @thread_pool_fs_write.wrap def deleteFiles(self): - self.log.debug("Deleting files from content.json...") + site_title = self.site.content_manager.contents.get("content.json", {}).get("title", self.site.address) + message_id = "delete-%s" % self.site.address + self.log.debug("Deleting files from content.json (title: %s)..." % site_title) + files = [] # Get filenames - for content_inner_path in list(self.site.content_manager.contents.keys()): + content_inner_paths = list(self.site.content_manager.contents.keys()) + for i, content_inner_path in enumerate(content_inner_paths): content = self.site.content_manager.contents.get(content_inner_path, {}) files.append(content_inner_path) # Add normal files @@ -531,6 +536,13 @@ class SiteStorage(object): file_inner_path = helper.getDirname(content_inner_path) + file_relative_path # Relative to site dir files.append(file_inner_path) + if i % 100 == 0: + num_files = len(files) + self.site.messageWebsocket( + _("Deleting site {site_title}...
Collected {num_files} files"), + message_id, (i / len(content_inner_paths)) * 25 + ) + if self.isFile("dbschema.json"): self.log.debug("Deleting db file...") self.closeDb() @@ -543,7 +555,8 @@ class SiteStorage(object): except Exception as err: self.log.error("Db file delete error: %s" % err) - for inner_path in files: + num_files = len(files) + for i, inner_path in enumerate(files): path = self.getPath(inner_path) if os.path.isfile(path): for retry in range(5): @@ -553,20 +566,46 @@ class SiteStorage(object): except Exception as err: self.log.error("Error removing %s: %s, try #%s" % (inner_path, err, retry)) time.sleep(float(retry) / 10) + if i % 100 == 0: + self.site.messageWebsocket( + _("Deleting site {site_title}...
Deleting file {i}/{num_files}"), + message_id, 25 + (i / num_files) * 50 + ) self.onUpdated(inner_path, False) self.log.debug("Deleting empty dirs...") + i = 0 for root, dirs, files in os.walk(self.directory, topdown=False): for dir in dirs: path = os.path.join(root, dir) - if os.path.isdir(path) and os.listdir(path) == []: - os.rmdir(path) + if os.path.isdir(path): + try: + i += 1 + if i % 100 == 0: + self.site.messageWebsocket( + _("Deleting site {site_title}...
Deleting empty directories {i}"), + message_id, 85 + ) + os.rmdir(path) + except OSError: # Not empty + pass + if os.path.isdir(self.directory) and os.listdir(self.directory) == []: os.rmdir(self.directory) # Remove sites directory if empty if os.path.isdir(self.directory): self.log.debug("Some unknown file remained in site data dir: %s..." % self.directory) + self.site.messageWebsocket( + _("Deleting site {site_title}...
Site deleted, but some unknown files left in the directory"), + message_id, 100 + ) return False # Some files not deleted else: - self.log.debug("Site data directory deleted: %s..." % self.directory) + self.log.debug("Site %s data directory deleted: %s..." % (site_title, self.directory)) + + self.site.messageWebsocket( + _("Deleting site {site_title}...
All files deleted successfully"), + message_id, 100 + ) + return True # All clean From 5c93aadce323553307e3b1fb287a2ea34a60ed23 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:09:14 +0100 Subject: [PATCH 258/483] Gevent block time resolution log to ms --- src/Debug/Debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Debug/Debug.py b/src/Debug/Debug.py index efaf98be..a48bc2ba 100644 --- a/src/Debug/Debug.py +++ b/src/Debug/Debug.py @@ -78,7 +78,7 @@ def testBlock(): while 1: time.sleep(1) if time.time() - last_time > 1.1: - logging.debug("Gevent block detected: %s" % (time.time() - last_time - 1)) + logging.debug("Gevent block detected: %.3fs" % (time.time() - last_time - 1)) num_block += 1 last_time = time.time() gevent.spawn(testBlock) From 99304a09cad8642bcac3f84fdcde38bd04ea598f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:10:11 +0100 Subject: [PATCH 259/483] Log long db queries --- src/Db/DbCursor.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 201d29d4..78f204a5 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -99,15 +99,19 @@ class DbCursor: cursor = self.conn.cursor() try: + if self.db.lock.locked(): + self.db.log.debug("Query delayed: db locked") self.db.lock.acquire(True) - if params: # Query has parameters + if params: res = cursor.execute(query, params) - if self.logging: - self.db.log.debug(query + " " + str(params) + " (Done in %.4f)" % (time.time() - s)) else: res = cursor.execute(query) - if self.logging: - self.db.log.debug(query + " (Done in %.4f)" % (time.time() - s)) + taken_query = time.time() - s + if self.logging or taken_query > 0.1: + if params: # Query has parameters + self.db.log.debug("Query: " + query + " " + str(params) + " (Done in %.4f)" % (time.time() - s)) + else: + self.db.log.debug("Query: " + query + " (Done in %.4f)" % (time.time() - s)) finally: self.db.lock.release() @@ -143,8 +147,9 @@ class DbCursor: finally: self.db.lock.release() - if self.logging: - self.db.log.debug("%s x %s (Done in %.4f)" % (query, len(params), time.time() - s)) + taken_query = time.time() - s + if self.logging or taken_query > 0.1: + self.db.log.debug("Query: %s x %s (Done in %.4f)" % (query, len(params), taken_query)) return cursor From 594edc6e9aaa819ee8199d69308c93740243ab20 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:10:40 +0100 Subject: [PATCH 260/483] Commit after executemany --- src/Db/DbCursor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 78f204a5..6cf16e04 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -151,6 +151,8 @@ class DbCursor: if self.logging or taken_query > 0.1: self.db.log.debug("Query: %s x %s (Done in %.4f)" % (query, len(params), taken_query)) + self.db.need_commit = True + return cursor # Creates on updates a database row without incrementing the rowid From 12bfad8fe6e2eea9163ea0175a008063c67e474e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:11:11 +0100 Subject: [PATCH 261/483] Don't execute query while commiting --- src/Db/Db.py | 4 ++++ src/Db/DbCursor.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index 63cbe407..ae79293b 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -68,6 +68,7 @@ class Db(object): self.cur = None self.progress_sleeping = False self.log = logging.getLogger("Db:%s" % schema["db_name"]) + self.commiting = False self.table_names = None self.collect_stats = False self.foreign_keys = False @@ -125,6 +126,7 @@ class Db(object): try: s = time.time() + self.commiting = True self.conn.commit() self.log.debug("Commited in %.3fs (reason: %s)" % (time.time() - s, reason)) return True @@ -134,6 +136,8 @@ class Db(object): else: self.log.error("Commit error: %s (reason: %s)" % (Debug.formatException(err), reason)) return False + finally: + self.commiting = False def insertOrUpdate(self, *args, **kwargs): if not self.conn: diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 6cf16e04..c8bf29a4 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -88,7 +88,7 @@ class DbCursor: if query.upper().strip("; ") == "VACUUM": self.db.commit("vacuum called") query = query.strip() - while self.db.progress_sleeping: + while self.db.progress_sleeping or self.db.commiting: time.sleep(0.1) self.db.last_query_time = time.time() @@ -133,7 +133,7 @@ class DbCursor: return res def executemany(self, query, params): - while self.db.progress_sleeping: + while self.db.progress_sleeping or self.db.commiting: time.sleep(0.1) self.db.last_query_time = time.time() From ec3c44c5b3a292dbb908e779ecca7b0219e65e4b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:11:34 +0100 Subject: [PATCH 262/483] Use ThreadPool lock in Db --- src/Db/Db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index ae79293b..a7b8fc23 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -81,7 +81,7 @@ class Db(object): self.last_query_time = time.time() self.last_sleep_time = time.time() self.num_execute_since_sleep = 0 - self.lock = Lock() + self.lock = ThreadPool.Lock() def __repr__(self): return "" % (id(self), self.db_path, self.close_idle) From 1670d969083bc3cf5bbc76426872d1e4dd46f6cf Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:12:33 +0100 Subject: [PATCH 263/483] Execute db commit in separate thread --- src/Db/Db.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Db/Db.py b/src/Db/Db.py index a7b8fc23..60fe3726 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -14,7 +14,10 @@ from Debug import Debug from .DbCursor import DbCursor from util import SafeRe from util import helper +from util import ThreadPool +from Config import config +thread_pool_db = ThreadPool.ThreadPool(config.threads_db) opened_dbs = [] @@ -115,6 +118,7 @@ class Db(object): self.connect() return self.cur.execute(query, params) + @thread_pool_db.wrap def commit(self, reason="Unknown"): if self.progress_sleeping: self.log.debug("Commit ignored: Progress sleeping") From c24cfa721beae4701fe1d352defb872cec69dbee Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:13:17 +0100 Subject: [PATCH 264/483] Lock db while connecting --- src/Db/Db.py | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index 60fe3726..fa1128e8 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -85,27 +85,37 @@ class Db(object): self.last_sleep_time = time.time() self.num_execute_since_sleep = 0 self.lock = ThreadPool.Lock() + self.connect_lock = ThreadPool.Lock() def __repr__(self): return "" % (id(self), self.db_path, self.close_idle) def connect(self): - if self not in opened_dbs: - opened_dbs.append(self) - s = time.time() - if not os.path.isdir(self.db_dir): # Directory not exist yet - os.makedirs(self.db_dir) - self.log.debug("Created Db path: %s" % self.db_dir) - if not os.path.isfile(self.db_path): - self.log.debug("Db file not exist yet: %s" % self.db_path) - self.conn = sqlite3.connect(self.db_path, isolation_level="DEFERRED", check_same_thread=False) - self.conn.row_factory = sqlite3.Row - self.conn.set_progress_handler(self.progress, 5000000) - self.cur = self.getCursor() - self.log.debug( - "Connected to %s in %.3fs (opened: %s, sqlite version: %s)..." % - (self.db_path, time.time() - s, len(opened_dbs), sqlite3.version) - ) + self.connect_lock.acquire(True) + try: + if self.conn: + self.log.debug("Already connected, connection ignored") + return + + if self not in opened_dbs: + opened_dbs.append(self) + s = time.time() + if not os.path.isdir(self.db_dir): # Directory not exist yet + os.makedirs(self.db_dir) + self.log.debug("Created Db path: %s" % self.db_dir) + if not os.path.isfile(self.db_path): + self.log.debug("Db file not exist yet: %s" % self.db_path) + self.conn = sqlite3.connect(self.db_path, isolation_level="DEFERRED", check_same_thread=False) + self.conn.row_factory = sqlite3.Row + self.conn.set_progress_handler(self.progress, 5000000) + self.cur = self.getCursor() + self.log.debug( + "Connected to %s in %.3fs (opened: %s, sqlite version: %s)..." % + (self.db_path, time.time() - s, len(opened_dbs), sqlite3.version) + ) + self.log.debug("Connect called by %s" % Debug.formatStack()) + finally: + self.connect_lock.release() def progress(self, *args, **kwargs): self.progress_sleeping = True From bd90e0ce52232866a065613369328f3ac76bbe4b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:13:39 +0100 Subject: [PATCH 265/483] Add Db id to logging identifier --- src/Db/Db.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index fa1128e8..1b9c07fa 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -18,6 +18,8 @@ from util import ThreadPool from Config import config thread_pool_db = ThreadPool.ThreadPool(config.threads_db) + +next_db_id = 0 opened_dbs = [] @@ -63,15 +65,18 @@ class DbTableError(Exception): class Db(object): def __init__(self, schema, db_path, close_idle=False): + global next_db_id self.db_path = db_path self.db_dir = os.path.dirname(db_path) + "/" self.schema = schema self.schema["version"] = self.schema.get("version", 1) self.conn = None self.cur = None + self.id = next_db_id + next_db_id += 1 self.progress_sleeping = False - self.log = logging.getLogger("Db:%s" % schema["db_name"]) self.commiting = False + self.log = logging.getLogger("Db#%s:%s" % (self.id, schema["db_name"])) self.table_names = None self.collect_stats = False self.foreign_keys = False From 5fba850d746b51b10d284b15026cd0053c0fa605 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:13:58 +0100 Subject: [PATCH 266/483] Don't close connection if it's already closed --- src/Db/Db.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Db/Db.py b/src/Db/Db.py index 1b9c07fa..fa511e3f 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -194,6 +194,8 @@ class Db(object): self.delayed_queue_thread = None def close(self): + if not self.conn: + return False s = time.time() if self.delayed_queue: self.processDelayed() @@ -201,6 +203,7 @@ class Db(object): opened_dbs.remove(self) self.need_commit = False self.commit("Closing") + self.log.debug("Close called by %s" % Debug.formatStack()) if self.cur: self.cur.close() if self.conn: @@ -208,6 +211,7 @@ class Db(object): self.conn = None self.cur = None self.log.debug("%s closed in %.3fs, opened: %s" % (self.db_path, time.time() - s, len(opened_dbs))) + return True # Gets a cursor object to database # Return: Cursor class From 1a17645e93d7bb87306d83d8eb5b22ecb6736125 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:14:08 +0100 Subject: [PATCH 267/483] Remove unnecessary import --- src/Db/Db.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index fa511e3f..b2844900 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -8,7 +8,6 @@ import atexit import sys import gevent -from gevent._threading import Lock from Debug import Debug from .DbCursor import DbCursor From 37b8c0241f1df42e7eb5beb512d32a2d9e6f7cf5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:14:54 +0100 Subject: [PATCH 268/483] Db threads modify in config interface --- .../UiConfig/media/js/ConfigStorage.coffee | 14 +++++++++ plugins/UiConfig/media/js/all.js | 31 ++++++++++++++++++- src/Config.py | 7 +++-- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/plugins/UiConfig/media/js/ConfigStorage.coffee b/plugins/UiConfig/media/js/ConfigStorage.coffee index 642bae45..9f35a91c 100644 --- a/plugins/UiConfig/media/js/ConfigStorage.coffee +++ b/plugins/UiConfig/media/js/ConfigStorage.coffee @@ -191,6 +191,20 @@ class ConfigStorage extends Class {title: "10 threads", value: 10} ] + section.items.push + key: "threads_db" + title: "Threads for database operations" + type: "select" + options: [ + {title: "Sync execution", value: 0} + {title: "1 thread", value: 1} + {title: "2 threads", value: 2} + {title: "3 threads", value: 3} + {title: "4 threads", value: 4} + {title: "5 threads", value: 5} + {title: "10 threads", value: 10} + ] + createSection: (title) => section = {} section.title = title diff --git a/plugins/UiConfig/media/js/all.js b/plugins/UiConfig/media/js/all.js index b2c09e39..43a91bc8 100644 --- a/plugins/UiConfig/media/js/all.js +++ b/plugins/UiConfig/media/js/all.js @@ -1539,7 +1539,7 @@ } ] }); - return section.items.push({ + section.items.push({ key: "threads_crypt", title: "Threads for cryptographic functions", type: "select", @@ -1568,6 +1568,35 @@ } ] }); + return section.items.push({ + key: "threads_db", + title: "Threads for database operations", + type: "select", + options: [ + { + title: "Sync execution", + value: 0 + }, { + title: "1 thread", + value: 1 + }, { + title: "2 threads", + value: 2 + }, { + title: "3 threads", + value: 3 + }, { + title: "4 threads", + value: 4 + }, { + title: "5 threads", + value: 5 + }, { + title: "10 threads", + value: 10 + } + ] + }); }; ConfigStorage.prototype.createSection = function(title) { diff --git a/src/Config.py b/src/Config.py index 7737ca2f..c57de0c7 100644 --- a/src/Config.py +++ b/src/Config.py @@ -22,7 +22,10 @@ class Config(object): self.keys_api_change_allowed = set([ "tor", "fileserver_port", "language", "tor_use_bridges", "trackers_proxy", "trackers", "trackers_file", "open_browser", "log_level", "fileserver_ip_type", "ip_external", "offline", - "threads_fs_read", "threads_fs_write", "threads_crypt" + "threads_fs_read", "threads_fs_write", "threads_crypt", "threads_db" + ]) + self.keys_restart_need = set([ + "tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt", "threads_db" ]) self.keys_restart_need = set(["tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt"]) self.start_dir = self.getStartDir() @@ -286,8 +289,8 @@ class Config(object): self.parser.add_argument('--threads_fs_read', help='Number of threads for file read operations', default=1, type=int) self.parser.add_argument('--threads_fs_write', help='Number of threads for file write operations', default=1, type=int) - self.parser.add_argument('--threads_db', help='Number of threads for database operations', default=1, type=int) self.parser.add_argument('--threads_crypt', help='Number of threads for cryptographic operations', default=2, type=int) + self.parser.add_argument('--threads_db', help='Number of threads for database operations', default=1, type=int) self.parser.add_argument("--download_optional", choices=["manual", "auto"], default="manual") From 566c29363f3ab28e2af6a01a16611abe30b0f972 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:15:17 +0100 Subject: [PATCH 269/483] Slower progress bar animation --- src/Ui/media/Wrapper.css | 2 +- src/Ui/media/all.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/media/Wrapper.css b/src/Ui/media/Wrapper.css index 7e012768..d633ff45 100644 --- a/src/Ui/media/Wrapper.css +++ b/src/Ui/media/Wrapper.css @@ -181,7 +181,7 @@ a { color: black } .progressbar { background: #26C281; position: fixed; width: 100%; z-index: 100; top: 0; left: 0; transform: scaleX(0); transform-origin: 0% 0%; transform:translate3d(0,0,0); - height: 2px; transition: transform 0.5s, opacity 1s; display: none; backface-visibility: hidden; transform-style: preserve-3d; + height: 2px; transition: transform 1s, opacity 1s; display: none; backface-visibility: hidden; transform-style: preserve-3d; } .progressbar .peg { display: block; position: absolute; right: 0; width: 100px; height: 100%; diff --git a/src/Ui/media/all.css b/src/Ui/media/all.css index dfeae47e..6e4173c2 100644 --- a/src/Ui/media/all.css +++ b/src/Ui/media/all.css @@ -194,7 +194,7 @@ a { color: black } .progressbar { background: #26C281; position: fixed; width: 100%; z-index: 100; top: 0; left: 0; -webkit-transform: scaleX(0); -moz-transform: scaleX(0); -o-transform: scaleX(0); -ms-transform: scaleX(0); transform: scaleX(0) ; transform-origin: 0% 0%; transform:translate3d(0,0,0); - height: 2px; -webkit-transition: transform 0.5s, opacity 1s; -moz-transition: transform 0.5s, opacity 1s; -o-transition: transform 0.5s, opacity 1s; -ms-transition: transform 0.5s, opacity 1s; transition: transform 0.5s, opacity 1s ; display: none; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; transform-style: preserve-3d; + height: 2px; -webkit-transition: transform 1s, opacity 1s; -moz-transition: transform 1s, opacity 1s; -o-transition: transform 1s, opacity 1s; -ms-transition: transform 1s, opacity 1s; transition: transform 1s, opacity 1s ; display: none; -webkit-backface-visibility: hidden; -moz-backface-visibility: hidden; -o-backface-visibility: hidden; -ms-backface-visibility: hidden; backface-visibility: hidden ; transform-style: preserve-3d; } .progressbar .peg { display: block; position: absolute; right: 0; width: 100px; height: 100%; From bdb655243fb325417dc51f5dbb2f45d821e4dcfa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:16:29 +0100 Subject: [PATCH 270/483] Rev4322 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index c57de0c7..0b2e6238 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4308 + self.rev = 4322 self.argv = argv self.action = None self.test_parser = None From aa9fe09337975d0631e2c0bd53730ae1414d619e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 30 Nov 2019 02:19:18 +0100 Subject: [PATCH 271/483] Remove unnecessary line from config --- src/Config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 0b2e6238..ae427b94 100644 --- a/src/Config.py +++ b/src/Config.py @@ -27,7 +27,6 @@ class Config(object): self.keys_restart_need = set([ "tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt", "threads_db" ]) - self.keys_restart_need = set(["tor", "fileserver_port", "fileserver_ip_type", "threads_fs_read", "threads_fs_write", "threads_crypt"]) self.start_dir = self.getStartDir() self.config_file = self.start_dir + "/zeronet.conf" From 6a1a821ed451b13a247ca86bb8f5688f7635b1df Mon Sep 17 00:00:00 2001 From: Eduaddad Date: Sat, 30 Nov 2019 12:04:25 -0300 Subject: [PATCH 272/483] Translation update for latest changes Translation update for latest changes --- plugins/UiConfig/languages/pt-br.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/UiConfig/languages/pt-br.json b/plugins/UiConfig/languages/pt-br.json index bbd2ed59..ba5fa1c3 100644 --- a/plugins/UiConfig/languages/pt-br.json +++ b/plugins/UiConfig/languages/pt-br.json @@ -39,6 +39,14 @@ "Performance": "Desempenho", "Level of logging to file": "Nível de registro no arquivo", "Everything": "Tudo", - "Only important messages": "Apenas mensagens importantes", - "Only errors": "Apenas erros" + "Only important messages": "Apenas mensagens importantes",, + "Only errors": "Apenas erros", + + "Threads for async file system reads": "Threads para leituras de sistema de arquivos assíncronas", + "Threads for async file system writes": "Threads para gravações do sistema de arquivos assíncrono", + "Threads for cryptographic functions": "Threads para funções criptográficas", + "Threads for database operations": "Threads para operações de banco de dados", + "Sync execution": "Execução de sincronização", + "Custom": "Personalizado", + "Custom socks proxy address for trackers": "Endereço de proxy de meias personalizadas para trackers" } From 901ccf2d14c860382e4028437c2c75b32f06d5b1 Mon Sep 17 00:00:00 2001 From: ethernetcat Date: Wed, 4 Dec 2019 17:52:33 +0900 Subject: [PATCH 273/483] Update jp.json --- src/Translate/languages/jp.json | 127 +++++++++++++------------------- 1 file changed, 51 insertions(+), 76 deletions(-) diff --git a/src/Translate/languages/jp.json b/src/Translate/languages/jp.json index 9978acc7..5f858dd6 100644 --- a/src/Translate/languages/jp.json +++ b/src/Translate/languages/jp.json @@ -1,82 +1,57 @@ { - "Peers": "ピア", - "Connected": "接続済み", - "Connectable": "利用可能", - "Connectable peers": "ピアに接続可能", + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "おめでとうございます。ポート {0} が開きました。これでZeroNetネットワークのメンバーです。", + "Tor mode active, every connection using Onion route.": "Torモードがアクティブです、全ての接続はOnionルートを使用します。", + "Successfully started Tor onion hidden services.": "Tor onionサービスを正常に開始しました。", + "Unable to start hidden services, please check your config.": "非表示のサービスを開始できません。設定を確認してください。", + "For faster connections open {0} port on your router.": "接続を高速化するにはルーターのポート {0} を開けてください。", + "Your connection is restricted. Please, open {0} port on your router": "接続が制限されています。ルーターのポート {0} を開けてください。", + "or configure Tor to become a full member of the ZeroNet network.": "または、TorをZeroNetネットワークのメンバーになるように設定してください。", - "Data transfer": "データ転送", - "Received": "受信", - "Received bytes": "受信バイト数", - "Sent": "送信", - "Sent bytes": "送信バイト数", + "Select account you want to use in this site:": "このサイトで使用するアカウントを選択:", + "currently selected": "現在選択中", + "Unique to site": "サイト固有", - "Files": "ファイル", - "Total": "合計", - "Image": "画像", - "Other": "その他", - "User data": "ユーザーデータ", + "Content signing failed": "コンテンツの署名に失敗", + "Content publish queued for {0:.0f} seconds.": "コンテンツの公開は{0:.0f}秒のキューに入れられました。", + "Content published to {0} peers.": "{0}ピアに公開されたコンテンツ。", + "No peers found, but your content is ready to access.": "ピアは見つかりませんでしたが、コンテンツにアクセスする準備ができました。", + "Your network connection is restricted. Please, open {0} port": "ネットワーク接続が制限されています。ポート {0} を開いて、", + "on your router to make your site accessible for everyone.": "誰でもサイトにアクセスできるようにしてください。", + "Content publish failed.": "コンテンツの公開に失敗しました。", + "This file still in sync, if you write it now, then the previous content may be lost.": "このファイルはまだ同期しています。今すぐ書き込むと、前のコンテンツが失われる可能性があります。", + "Write content anyway": "とにかくコンテンツを書く", + "New certificate added:": "新しい証明書が追加されました:", + "You current certificate:": "現在の証明書:", + "Change it to {auth_type}/{auth_user_name}@{domain}": "{auth_type}/{auth_user_name}@{domain} に変更", + "Certificate changed to: {auth_type}/{auth_user_name}@{domain}.": "変更後の証明書: {auth_type}/{auth_user_name}@{domain}", + "Site cloned": "複製されたサイト", - "Size limit": "サイズ制限", - "limit used": "使用上限", - "free space": "フリースペース", - "Set": "セット", + "You have successfully changed the web interface's language!": "Webインターフェースの言語が正常に変更されました!", + "Due to the browser's caching, the full transformation could take some minute.": "ブラウザのキャッシュにより、完全な変換には数分かかる場合があります。", - "Optional files": "オプション ファイル", - "Downloaded": "ダウンロード済み", - "Download and help distribute all files": "ダウンロードしてすべてのファイルの配布を支援する", - "Total size": "合計サイズ", - "Downloaded files": "ダウンロードされたファイル", + "Connection with UiServer Websocket was lost. Reconnecting...": "UiServer Websocketとの接続が失われました。再接続しています...", + "Connection with UiServer Websocket recovered.": "UiServer Websocketとの接続が回復しました。", + "UiServer Websocket error, please reload the page.": "UiServer Websocketエラー、ページをリロードしてください。", + "   Connecting...": "   接続しています...", + "Site size: ": "サイトサイズ: ", + "MB is larger than default allowed ": "MBはデフォルトの許容値よりも大きいです。 ", + "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "サイトを開き、サイズ制限を \" + site_info.next_size_limit + \"MB に設定", + " files needs to be downloaded": " ファイルをダウンロードする必要があります", + " downloaded": " ダウンロード", + " download failed": " ダウンロード失敗", + "Peers found: ": "ピアが見つかりました: ", + "No peers found": "ピアが見つかりません", + "Running out of size limit (": "サイズ制限を使い果たしました (", + "Set limit to \" + site_info.next_size_limit + \"MB": "制限を \" + site_info.next_size_limit + \"MB に設定", + "Site size limit changed to {0}MB": "サイトのサイズ制限が {0}MB に変更されました", + " New version of this page has just released.
Reload to see the modified content.": " このページの新しいバージョンが公開されました。
変更されたコンテンツを見るには再読み込みしてください。", + "This site requests permission:": "このサイトは権限を要求しています:", + "_(Accept)": "_(許可)", + + "Save": "保存", + "Trackers announcing": "トラッカーをお知らせ", + "Error": "エラー", + "Done": "完了", + "Tracker connection error detected.": "トラッカー接続エラーが検出されました。" - "Database": "データベース", - "search feeds": "フィードを検索する", - "{feeds} query": "{フィード} お問い合わせ", - "Reload": "再読込", - "Rebuild": "再ビルド", - "No database found": "データベースが見つかりません", - - "Identity address": "Identity address", - "Change": "編集", - - "Site control": "サイト管理", - "Update": "更新", - "Pause": "一時停止", - "Resume": "再開", - "Delete": "削除", - "Are you sure?": "本当によろしいですか?", - - "Site address": "サイトアドレス", - "Donate": "寄付する", - - "Missing files": "ファイルがありません", - "{} try": "{} 試す", - "{} tries": "{} 試行", - "+ {num_bad_files} more": "+ {num_bad_files} more", - - "This is my site": "This is my site", - "Site title": "サイトタイトル", - "Site description": "サイトの説明", - "Save site settings": "サイトの設定を保存する", - - "Content publishing": "コンテンツを公開する", - "Choose": "選択", - "Sign": "Sign", - "Publish": "公開する", - - "This function is disabled on this proxy": "この機能はこのプロキシで無効になっています", - "GeoLite2 City database download error: {}!
Please download manually and unpack to data dir:
{}": "GeoLite2 Cityデータベースのダウンロードエラー: {}!
手動でダウンロードして、フォルダに解凍してください。:
{}", - "Downloading GeoLite2 City database (one time only, ~20MB)...": "GeoLite2 Cityデータベースの読み込み (これは一度だけ行われます, ~20MB)...", - "GeoLite2 City database downloaded!": "GeoLite2 Cityデータベースがダウンロードされました!", - - "Are you sure?": "本当によろしいですか?", - "Site storage limit modified!": "サイトの保存容量の制限が変更されました!", - "Database schema reloaded!": "データベーススキーマがリロードされました!", - "Database rebuilding....": "データベースの再構築中....", - "Database rebuilt!": "データベースが再構築されました!", - "Site updated!": "サイトが更新されました!", - "Delete this site": "このサイトを削除する", - "File write error: ": "ファイル書き込みエラー:", - "Site settings saved!": "サイト設定が保存されました!", - "Enter your private key:": "秘密鍵を入力してください:", - " Signed!": " Signed!", - "WebGL not supported": "WebGLはサポートされていません" -} \ No newline at end of file +} From 3dd04b27de73c7d916b226fec687764e350d047a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 11:03:45 +0100 Subject: [PATCH 274/483] Correct invalid UiConfig pt-br json file --- plugins/UiConfig/languages/pt-br.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UiConfig/languages/pt-br.json b/plugins/UiConfig/languages/pt-br.json index ba5fa1c3..b95758f2 100644 --- a/plugins/UiConfig/languages/pt-br.json +++ b/plugins/UiConfig/languages/pt-br.json @@ -39,9 +39,9 @@ "Performance": "Desempenho", "Level of logging to file": "Nível de registro no arquivo", "Everything": "Tudo", - "Only important messages": "Apenas mensagens importantes",, + "Only important messages": "Apenas mensagens importantes", "Only errors": "Apenas erros", - + "Threads for async file system reads": "Threads para leituras de sistema de arquivos assíncronas", "Threads for async file system writes": "Threads para gravações do sistema de arquivos assíncrono", "Threads for cryptographic functions": "Threads para funções criptográficas", From ea5f64bfeafdcb6de39ab685cc943d9806043c38 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 12:46:13 +0100 Subject: [PATCH 275/483] Only log at start of the test cases --- src/Test/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index c703b77e..4ec4354d 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -413,6 +413,10 @@ def crypt_bitcoin_lib(request, monkeypatch): CryptBitcoin.loadLib(request.param) return CryptBitcoin +@pytest.fixture(scope='function', autouse=True) +def logCaseStart(request): + logging.debug("---- Start test case: %s ----" % request._pyfuncitem) + yield None # Wait until all test done def workaroundPytestLogError(): @@ -449,4 +453,5 @@ workaroundPytestLogError() def logCaseStart(request): logging.debug("---- Start test case: %s ----" % request._pyfuncitem) yield None # Wait until all test done - logging.debug("---- End test case: %s ----" % request._pyfuncitem) + logging.getLogger('').setLevel(logging.getLevelName(logging.CRITICAL)) + From 1935a69c04ac41e18775a33e6f1e511ecbf532f2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 12:46:44 +0100 Subject: [PATCH 276/483] Add session based log disable at test --- src/Test/conftest.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 4ec4354d..74f27712 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -419,9 +419,8 @@ def logCaseStart(request): yield None # Wait until all test done +# Workaround for pytest bug when logging in atexit/post-fixture handlers (I/O operation on closed file) def workaroundPytestLogError(): - # Workaround for pytest bug when logging in atexit/post-fixture handlers (I/O operation on closed file) - import _pytest.capture write_original = _pytest.capture.EncodedFile.write @@ -449,9 +448,8 @@ def workaroundPytestLogError(): workaroundPytestLogError() -@pytest.fixture(scope='function', autouse=True) -def logCaseStart(request): - logging.debug("---- Start test case: %s ----" % request._pyfuncitem) +@pytest.fixture(scope='session', autouse=True) +def disableLog(): yield None # Wait until all test done logging.getLogger('').setLevel(logging.getLevelName(logging.CRITICAL)) From c8214bf3ea7c231fddb740e3180668e474d29b70 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 12:47:47 +0100 Subject: [PATCH 277/483] Fix threadpool test premature end on some platforms --- src/Test/TestThreadPool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py index 2a67f181..0f9b3b63 100644 --- a/src/Test/TestThreadPool.py +++ b/src/Test/TestThreadPool.py @@ -16,7 +16,7 @@ class TestThreadPool: events.append("S") out = 0 for i in range(10000000): - if i == 5000000: + if i == 1000000: events.append("M") out += 1 events.append("D") From daee14533c44990c7cea2dcee767baab2204c297 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 17:14:04 +0100 Subject: [PATCH 278/483] Fix site number changes when data collected for stats --- plugins/Chart/ChartCollector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/Chart/ChartCollector.py b/plugins/Chart/ChartCollector.py index 82e54bb6..215c603c 100644 --- a/plugins/Chart/ChartCollector.py +++ b/plugins/Chart/ChartCollector.py @@ -119,6 +119,8 @@ class ChartCollector(object): value = collector(peers) else: value = collector() + except ValueError: + value = None except Exception as err: self.log.info("Collector %s error: %s" % (key, err)) value = None @@ -153,7 +155,7 @@ class ChartCollector(object): now = int(time.time()) s = time.time() values = [] - for address, site in sites.items(): + for address, site in list(sites.items()): site_datas = self.collectDatas(collectors, last_values["site:%s" % address], site) for key, value in site_datas.items(): values.append((self.db.getTypeId(key), self.db.getSiteId(address), value, now)) From 5ce1782d05ab12fbf94a73631e7ce15a42c7616c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 17:14:50 +0100 Subject: [PATCH 279/483] Change journal and foreign keys mode on db connect --- src/Db/Db.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index b2844900..0a2f280c 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -112,7 +112,11 @@ class Db(object): self.conn = sqlite3.connect(self.db_path, isolation_level="DEFERRED", check_same_thread=False) self.conn.row_factory = sqlite3.Row self.conn.set_progress_handler(self.progress, 5000000) + self.conn.execute('PRAGMA journal_mode=WAL') + if self.foreign_keys: + self.conn.execute("PRAGMA foreign_keys = ON") self.cur = self.getCursor() + self.log.debug( "Connected to %s in %.3fs (opened: %s, sqlite version: %s)..." % (self.db_path, time.time() - s, len(opened_dbs), sqlite3.version) @@ -219,10 +223,6 @@ class Db(object): self.connect() cur = DbCursor(self.conn, self) - cur.execute('PRAGMA journal_mode=WAL') - if self.foreign_keys: - cur.execute("PRAGMA foreign_keys = ON") - return cur def getSharedCursor(self): From 23f851343f810dd0d6219e99c4e447a8cad1b08e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 17:15:08 +0100 Subject: [PATCH 280/483] Fix exception when params is an iterator --- src/Db/DbCursor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index c8bf29a4..b7d354da 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -149,7 +149,7 @@ class DbCursor: taken_query = time.time() - s if self.logging or taken_query > 0.1: - self.db.log.debug("Query: %s x %s (Done in %.4f)" % (query, len(params), taken_query)) + self.db.log.debug("Execute many: %s (Done in %.4f)" % (query, taken_query)) self.db.need_commit = True From 04ecb89e9a20c7065e85e7d1162163ae39b5c9e7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 17:15:42 +0100 Subject: [PATCH 281/483] Avoid sending too many publish request to an outdated client --- src/Site/Site.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 1ea086ef..f8a1d7d7 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -394,6 +394,7 @@ class Site(object): queried.append(peer) modified_contents = [] my_modified = self.content_manager.listModified(since) + num_old_files = 0 for inner_path, modified in res["modified_files"].items(): # Check if the peer has newer files than we has_newer = int(modified) > my_modified.get(inner_path, 0) has_older = int(modified) < my_modified.get(inner_path, 0) @@ -402,8 +403,9 @@ class Site(object): # We dont have this file or we have older modified_contents.append(inner_path) self.bad_files[inner_path] = self.bad_files.get(inner_path, 0) + 1 - if has_older: - self.log.debug("%s client has older version of %s, publishing there..." % (peer, inner_path)) + if has_older and num_old_files < 5: + num_old_files += 1 + self.log.debug("%s client has older version of %s, publishing there (%s/5)..." % (peer, inner_path, num_old_files)) gevent.spawn(self.publisher, inner_path, [peer], [], 1) if modified_contents: self.log.debug("%s new modified file from %s" % (len(modified_contents), peer)) From 5e26161e84b5ceac95678169586e0cf0d21463db Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Dec 2019 17:16:08 +0100 Subject: [PATCH 282/483] Rev4325 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index ae427b94..65e29fe0 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4322 + self.rev = 4325 self.argv = argv self.action = None self.test_parser = None From 2fd337bb55097bf0187dee85ba8866f1ef333e30 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 11 Dec 2019 20:03:28 +0100 Subject: [PATCH 283/483] Add wasm content type --- src/Ui/UiRequest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index cd23d47d..2d8ebdee 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -36,6 +36,7 @@ content_types = { "sig": "application/pgp-signature", "txt": "text/plain", "webmanifest": "application/manifest+json", + "wasm": "application/wasm", "webp": "image/webp" } From 71939097b061577902f0e9370e29489e1f3c0991 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 11 Dec 2019 20:04:39 +0100 Subject: [PATCH 284/483] Make execution order test more predictable --- src/Test/TestThreadPool.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py index 0f9b3b63..bb88b3bf 100644 --- a/src/Test/TestThreadPool.py +++ b/src/Test/TestThreadPool.py @@ -16,18 +16,18 @@ class TestThreadPool: events.append("S") out = 0 for i in range(10000000): - if i == 1000000: + if i == 3000000: events.append("M") out += 1 events.append("D") return out threads = [] - for i in range(4): + for i in range(2): threads.append(gevent.spawn(blocker)) gevent.joinall(threads) - assert events == ["S"] * 4 + ["M"] * 4 + ["D"] * 4 + assert events == ["S"] * 2 + ["M"] * 2 + ["D"] * 2 res = blocker() assert res == 10000000 From 28fcf3c1ead072cdc2a4303186028d0de8298953 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 11 Dec 2019 20:04:50 +0100 Subject: [PATCH 285/483] Rev4327 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 65e29fe0..7ea187ea 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4325 + self.rev = 4327 self.argv = argv self.action = None self.test_parser = None From 3178b69172c8058c448210d48dad2d7cb32746bc Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 9 Dec 2019 22:13:38 +0300 Subject: [PATCH 286/483] Switch to bencode_open --- .../AnnounceBitTorrentPlugin.py | 6 +- requirements.txt | 1 - src/lib/bencode_open/LICENSE | 21 +++ src/lib/bencode_open/__init__.py | 160 ++++++++++++++++++ 4 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 src/lib/bencode_open/LICENSE create mode 100644 src/lib/bencode_open/__init__.py diff --git a/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py b/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py index ae674c00..defa9266 100644 --- a/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py +++ b/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py @@ -3,7 +3,7 @@ import urllib.request import struct import socket -import bencode +import bencode_open from lib.subtl.subtl import UdpTrackerClient import socks import sockshandler @@ -133,9 +133,7 @@ class SiteAnnouncerPlugin(object): # Decode peers try: - peer_data = bencode.decode(response)["peers"] - if type(peer_data) is not bytes: - peer_data = peer_data.encode() + peer_data = bencode_open.loads(response)[b"peers"] response = None peer_count = int(len(peer_data) / 6) peers = [] diff --git a/requirements.txt b/requirements.txt index 8e756f66..7c063e3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ PySocks>=1.6.8 pyasn1 websocket_client gevent-websocket -bencode.py coincurve python-bitcoinlib maxminddb diff --git a/src/lib/bencode_open/LICENSE b/src/lib/bencode_open/LICENSE new file mode 100644 index 00000000..f0e46d71 --- /dev/null +++ b/src/lib/bencode_open/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Ivan Machugovskiy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/lib/bencode_open/__init__.py b/src/lib/bencode_open/__init__.py new file mode 100644 index 00000000..e3c783cc --- /dev/null +++ b/src/lib/bencode_open/__init__.py @@ -0,0 +1,160 @@ +def loads(data): + if not isinstance(data, bytes): + raise TypeError("Expected 'bytes' object, got {}".format(type(data))) + + offset = 0 + + + def parseInteger(): + nonlocal offset + + offset += 1 + had_digit = False + abs_value = 0 + + sign = 1 + if data[offset] == ord("-"): + sign = -1 + offset += 1 + while offset < len(data): + if data[offset] == ord("e"): + # End of string + offset += 1 + if not had_digit: + raise ValueError("Integer without value") + break + if ord("0") <= data[offset] <= ord("9"): + abs_value = abs_value * 10 + int(chr(data[offset])) + had_digit = True + offset += 1 + else: + raise ValueError("Invalid integer") + else: + raise ValueError("Unexpected EOF, expected integer") + + if not had_digit: + raise ValueError("Empty integer") + + return sign * abs_value + + + def parseString(): + nonlocal offset + + length = int(chr(data[offset])) + offset += 1 + + while offset < len(data): + if data[offset] == ord(":"): + offset += 1 + break + if ord("0") <= data[offset] <= ord("9"): + length = length * 10 + int(chr(data[offset])) + offset += 1 + else: + raise ValueError("Invalid string length") + else: + raise ValueError("Unexpected EOF, expected string contents") + + if offset + length > len(data): + raise ValueError("Unexpected EOF, expected string contents") + offset += length + + return data[offset - length:offset] + + + def parseList(): + nonlocal offset + + offset += 1 + values = [] + + while offset < len(data): + if data[offset] == ord("e"): + # End of list + offset += 1 + return values + else: + values.append(parse()) + + raise ValueError("Unexpected EOF, expected list contents") + + + def parseDict(): + nonlocal offset + + offset += 1 + items = {} + + while offset < len(data): + if data[offset] == ord("e"): + # End of list + offset += 1 + return items + else: + key, value = parse(), parse() + if not isinstance(key, bytes): + raise ValueError("A dict key must be a byte string") + if key in items: + raise ValueError("Duplicate dict key: {}".format(key)) + items[key] = value + + raise ValueError("Unexpected EOF, expected dict contents") + + + def parse(): + nonlocal offset + + if data[offset] == ord("i"): + return parseInteger() + elif data[offset] == ord("l"): + return parseList() + elif data[offset] == ord("d"): + return parseDict() + elif ord("0") <= data[offset] <= ord("9"): + return parseString() + + raise ValueError("Unknown type specifier: '{}'".format(chr(data[offset]))) + + result = parse() + + if offset != len(data): + raise ValueError("Expected EOF, got {} bytes left".format(len(data) - offset)) + + return result + + +def dumps(data): + result = bytearray() + + + def convert(data): + nonlocal result + + if isinstance(data, str): + raise ValueError("bencode only supports bytes, not str. Use encode") + + if isinstance(data, bytes): + result += str(len(data)).encode() + b":" + data + elif isinstance(data, int): + result += b"i" + str(data).encode() + b"e" + elif isinstance(data, list): + result += b"l" + for val in data: + convert(val) + result += b"e" + elif isinstance(data, dict): + result += b"d" + for key in sorted(data.keys()): + if not isinstance(key, bytes): + raise ValueError("Dict key can only be bytes, not {}".format(type(key))) + convert(key) + convert(data[key]) + result += b"e" + else: + raise ValueError("bencode only supports bytes, int, list and dict") + + + convert(data) + + return bytes(result) From fbc7b6fc4f1466bbc7df9b922cf58ed108e96938 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Sun, 15 Dec 2019 14:46:06 +0300 Subject: [PATCH 287/483] Switch to sslcrypto for cryptography tasks (#2338) * Use sslcrypto instead of pyelliptic and pybitcointools * Fix CryptMessage * Support Python 3.4 * Fix user creation * Get rid of pyelliptic and pybitcointools * Fix typo * Delete test file * Add sslcrypto to tree * Update sslcrypto * Add pyaes to src/lib * Fix typo in tests * Update sslcrypto version * Use privatekey_bin instead of privatekey for bytes objects * Fix sslcrypto * Fix Benchmark plugin * Don't calculate the same thing twice * Only import sslcrypto once * Handle fallback sslcrypto implementation during tests * Fix sslcrypto fallback implementation selection --- .gitlab-ci.yml | 2 +- .travis.yml | 2 +- plugins/Benchmark/BenchmarkPlugin.py | 4 +- plugins/CryptMessage/CryptMessage.py | 75 +- plugins/CryptMessage/CryptMessagePlugin.py | 56 +- plugins/CryptMessage/Test/TestCrypt.py | 7 +- requirements.txt | 1 - src/Crypt/CryptBitcoin.py | 86 +- src/Test/conftest.py | 2 +- .../libsecp256k1message.py | 43 +- .../LICENSE => pyaes/LICENSE.txt} | 9 +- src/lib/pyaes/README.md | 363 +++ src/lib/pyaes/__init__.py | 53 + src/lib/pyaes/aes.py | 589 +++++ src/lib/pyaes/blockfeeder.py | 227 ++ src/lib/pyaes/util.py | 60 + src/lib/pybitcointools/__init__.py | 10 - src/lib/pybitcointools/bci.py | 528 ----- src/lib/pybitcointools/blocks.py | 50 - src/lib/pybitcointools/composite.py | 128 -- src/lib/pybitcointools/deterministic.py | 199 -- src/lib/pybitcointools/english.txt | 2048 ----------------- src/lib/pybitcointools/main.py | 581 ----- src/lib/pybitcointools/mnemonic.py | 127 - src/lib/pybitcointools/py2specials.py | 98 - src/lib/pybitcointools/py3specials.py | 123 - src/lib/pybitcointools/ripemd.py | 414 ---- src/lib/pybitcointools/stealth.py | 100 - src/lib/pybitcointools/transaction.py | 514 ----- src/lib/pyelliptic/LICENSE | 674 ------ src/lib/pyelliptic/README.md | 96 - src/lib/pyelliptic/__init__.py | 19 - src/lib/pyelliptic/arithmetic.py | 144 -- src/lib/pyelliptic/cipher.py | 84 - src/lib/pyelliptic/ecc.py | 505 ---- src/lib/pyelliptic/hash.py | 69 - src/lib/pyelliptic/openssl.py | 553 ----- src/lib/pyelliptic/setup.py | 23 - src/lib/sslcrypto/LICENSE | 27 + src/lib/sslcrypto/__init__.py | 6 + src/lib/sslcrypto/_aes.py | 53 + src/lib/sslcrypto/_ecc.py | 334 +++ src/lib/sslcrypto/_ripemd.py | 375 +++ src/lib/sslcrypto/fallback/__init__.py | 3 + src/lib/sslcrypto/fallback/_jacobian.py | 159 ++ src/lib/sslcrypto/fallback/_util.py | 79 + src/lib/sslcrypto/fallback/aes.py | 101 + src/lib/sslcrypto/fallback/ecc.py | 371 +++ src/lib/sslcrypto/fallback/rsa.py | 8 + src/lib/sslcrypto/openssl/__init__.py | 3 + src/lib/sslcrypto/openssl/aes.py | 156 ++ src/lib/sslcrypto/openssl/ecc.py | 551 +++++ src/lib/sslcrypto/openssl/library.py | 93 + src/lib/sslcrypto/openssl/rsa.py | 11 + src/util/Electrum.py | 39 + 55 files changed, 3748 insertions(+), 7287 deletions(-) rename src/lib/{pybitcointools/LICENSE => pyaes/LICENSE.txt} (80%) create mode 100644 src/lib/pyaes/README.md create mode 100644 src/lib/pyaes/__init__.py create mode 100644 src/lib/pyaes/aes.py create mode 100644 src/lib/pyaes/blockfeeder.py create mode 100644 src/lib/pyaes/util.py delete mode 100644 src/lib/pybitcointools/__init__.py delete mode 100644 src/lib/pybitcointools/bci.py delete mode 100644 src/lib/pybitcointools/blocks.py delete mode 100644 src/lib/pybitcointools/composite.py delete mode 100644 src/lib/pybitcointools/deterministic.py delete mode 100644 src/lib/pybitcointools/english.txt delete mode 100644 src/lib/pybitcointools/main.py delete mode 100644 src/lib/pybitcointools/mnemonic.py delete mode 100644 src/lib/pybitcointools/py2specials.py delete mode 100644 src/lib/pybitcointools/py3specials.py delete mode 100644 src/lib/pybitcointools/ripemd.py delete mode 100644 src/lib/pybitcointools/stealth.py delete mode 100644 src/lib/pybitcointools/transaction.py delete mode 100644 src/lib/pyelliptic/LICENSE delete mode 100644 src/lib/pyelliptic/README.md delete mode 100644 src/lib/pyelliptic/__init__.py delete mode 100644 src/lib/pyelliptic/arithmetic.py delete mode 100644 src/lib/pyelliptic/cipher.py delete mode 100644 src/lib/pyelliptic/ecc.py delete mode 100644 src/lib/pyelliptic/hash.py delete mode 100644 src/lib/pyelliptic/openssl.py delete mode 100644 src/lib/pyelliptic/setup.py create mode 100644 src/lib/sslcrypto/LICENSE create mode 100644 src/lib/sslcrypto/__init__.py create mode 100644 src/lib/sslcrypto/_aes.py create mode 100644 src/lib/sslcrypto/_ecc.py create mode 100644 src/lib/sslcrypto/_ripemd.py create mode 100644 src/lib/sslcrypto/fallback/__init__.py create mode 100644 src/lib/sslcrypto/fallback/_jacobian.py create mode 100644 src/lib/sslcrypto/fallback/_util.py create mode 100644 src/lib/sslcrypto/fallback/aes.py create mode 100644 src/lib/sslcrypto/fallback/ecc.py create mode 100644 src/lib/sslcrypto/fallback/rsa.py create mode 100644 src/lib/sslcrypto/openssl/__init__.py create mode 100644 src/lib/sslcrypto/openssl/aes.py create mode 100644 src/lib/sslcrypto/openssl/ecc.py create mode 100644 src/lib/sslcrypto/openssl/library.py create mode 100644 src/lib/sslcrypto/openssl/rsa.py create mode 100644 src/util/Electrum.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b62c7b0c..f3e1ed29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ stages: - python -m pytest -x plugins/Multiuser/Test --color=yes - mv plugins/disabled-Bootstrapper plugins/Bootstrapper - python -m pytest -x plugins/Bootstrapper/Test --color=yes - - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ test:py3.4: image: python:3.4.3 diff --git a/.travis.yml b/.travis.yml index 1f81b60a..c67ecf88 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ script: - python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini - mv plugins/disabled-Multiuser plugins/Multiuser && python -m pytest -x plugins/Multiuser/Test - mv plugins/disabled-Bootstrapper plugins/Bootstrapper && python -m pytest -x plugins/Bootstrapper/Test - - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ after_success: - codecov - coveralls --rcfile=src/Test/coverage.ini diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index 73b95d22..72f8b6ac 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -103,8 +103,8 @@ class ActionsPlugin: tests.extend([ {"func": self.testHdPrivatekey, "num": 50, "time_standard": 0.57}, {"func": self.testSign, "num": 20, "time_standard": 0.46}, - {"func": self.testVerify, "kwargs": {"lib_verify": "btctools"}, "num": 20, "time_standard": 0.38}, - {"func": self.testVerify, "kwargs": {"lib_verify": "openssl"}, "num": 200, "time_standard": 0.30}, + {"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto_fallback"}, "num": 20, "time_standard": 0.38}, + {"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto"}, "num": 200, "time_standard": 0.30}, {"func": self.testVerify, "kwargs": {"lib_verify": "libsecp256k1"}, "num": 200, "time_standard": 0.10}, {"func": self.testPackMsgpack, "num": 100, "time_standard": 0.35}, diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index b6c65673..2bb61d03 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -1,28 +1,21 @@ import hashlib import base64 -import binascii - -import lib.pybitcointools as btctools -from util import ThreadPool +import sslcrypto from Crypt import Crypt -ecc_cache = {} -def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): - from lib import pyelliptic - pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey)) - curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey_openssl) - if ephemcurve is None: - ephemcurve = curve - ephem = pyelliptic.ECC(curve=ephemcurve) - key = hashlib.sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - pubkey = ephem.get_pubkey() - iv = pyelliptic.OpenSSL.rand(pyelliptic.OpenSSL.get_cipher(ciphername).get_blocksize()) - ctx = pyelliptic.Cipher(key_e, iv, 1, ciphername) - ciphertext = iv + pubkey + ctx.ciphering(data) - mac = pyelliptic.hmac_sha256(key_m, ciphertext) - return key_e, ciphertext + mac +curve = sslcrypto.ecc.get_curve("secp256k1") + + +def eciesEncrypt(data, pubkey, ciphername="aes-256-cbc"): + ciphertext, key_e = curve.encrypt( + data, + base64.b64decode(pubkey), + algo=ciphername, + derivation="sha512", + return_aes_key=True + ) + return key_e, ciphertext @Crypt.thread_pool_crypt.wrap @@ -37,38 +30,12 @@ def eciesDecryptMulti(encrypted_datas, privatekey): return texts -def eciesDecrypt(encrypted_data, privatekey): - ecc_key = getEcc(privatekey) - return ecc_key.decrypt(base64.b64decode(encrypted_data)) +def eciesDecrypt(ciphertext, privatekey): + return curve.decrypt( + base64.b64decode(ciphertext), + curve.wif_to_private(privatekey), + derivation="sha512" + ) -def split(encrypted): - iv = encrypted[0:16] - ciphertext = encrypted[16 + 70:-32] - - return iv, ciphertext - - -def getEcc(privatekey=None): - from lib import pyelliptic - global ecc_cache - if privatekey not in ecc_cache: - if privatekey: - publickey_bin = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin") - publickey_openssl = toOpensslPublickey(publickey_bin) - privatekey_openssl = toOpensslPrivatekey(privatekey) - ecc_cache[privatekey] = pyelliptic.ECC(curve='secp256k1', privkey=privatekey_openssl, pubkey=publickey_openssl) - else: - ecc_cache[None] = pyelliptic.ECC() - return ecc_cache[privatekey] - - -def toOpensslPrivatekey(privatekey): - privatekey_bin = btctools.encode_privkey(privatekey, "bin") - return b'\x02\xca\x00\x20' + privatekey_bin - - -def toOpensslPublickey(publickey): - publickey_bin = btctools.encode_pubkey(publickey, "bin") - publickey_bin = publickey_bin[1:] - publickey_openssl = b'\x02\xca\x00 ' + publickey_bin[:32] + b'\x00 ' + publickey_bin[32:] - return publickey_openssl +def split(ciphertext): + return ciphertext[:16], ciphertext[86:-32] diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index 45afe184..150bf8be 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -5,24 +5,22 @@ import gevent from Plugin import PluginManager from Crypt import CryptBitcoin, CryptHash -import lib.pybitcointools as btctools from Config import config +import sslcrypto + from . import CryptMessage +curve = sslcrypto.ecc.get_curve("secp256k1") + @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): - def eciesDecrypt(self, encrypted, privatekey): - back = CryptMessage.getEcc(privatekey).decrypt(encrypted) - return back.decode("utf8") - # - Actions - # Returns user's public key unique to site # Return: Public key def actionUserPublickey(self, to, index=0): - publickey = self.user.getEncryptPublickey(self.site.address, index) - self.response(to, publickey) + self.response(to, self.user.getEncryptPublickey(self.site.address, index)) # Encrypt a text using the publickey or user's sites unique publickey # Return: Encrypted text using base64 encoding @@ -55,23 +53,16 @@ class UiWebsocketPlugin(object): # Encrypt a text using AES # Return: Iv, AES key, Encrypted text - def actionAesEncrypt(self, to, text, key=None, iv=None): - from lib import pyelliptic - + def actionAesEncrypt(self, to, text, key=None): if key: key = base64.b64decode(key) else: - key = os.urandom(32) - - if iv: # Generate new AES key if not definied - iv = base64.b64decode(iv) - else: - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') + key = sslcrypto.aes.new_key() if text: - encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(text.encode("utf8")) + encrypted, iv = sslcrypto.aes.encrypt(text.encode("utf8"), key) else: - encrypted = b"" + encrypted, iv = b"", b"" res = [base64.b64encode(item).decode("utf8") for item in [key, iv, encrypted]] self.response(to, res) @@ -79,8 +70,6 @@ class UiWebsocketPlugin(object): # Decrypt a text using AES # Return: Decrypted text def actionAesDecrypt(self, to, *args): - from lib import pyelliptic - if len(args) == 3: # Single decrypt encrypted_texts = [(args[0], args[1])] keys = [args[2]] @@ -93,9 +82,8 @@ class UiWebsocketPlugin(object): iv = base64.b64decode(iv) text = None for key in keys: - ctx = pyelliptic.Cipher(base64.b64decode(key), iv, 0, ciphername='aes-256-cbc') try: - decrypted = ctx.ciphering(encrypted_text) + decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, base64.b64decode(key)) if decrypted and decrypted.decode("utf8"): # Valid text decoded text = decrypted.decode("utf8") except Exception as err: @@ -122,12 +110,11 @@ class UiWebsocketPlugin(object): # Gets the publickey of a given privatekey def actionEccPrivToPub(self, to, privatekey): - self.response(to, btctools.privtopub(privatekey)) + self.response(to, curve.private_to_public(curve.wif_to_private(privatekey))) # Gets the address of a given publickey def actionEccPubToAddr(self, to, publickey): - address = btctools.pubtoaddr(btctools.decode_pubkey(publickey)) - self.response(to, address) + self.response(to, curve.public_to_address(bytes.fromhex(publickey))) @PluginManager.registerTo("User") @@ -163,7 +150,7 @@ class UserPlugin(object): if "encrypt_publickey_%s" % index not in site_data: privatekey = self.getEncryptPrivatekey(address, param_index) - publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed") + publickey = curve.private_to_public(curve.wif_to_private(privatekey)) site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey).decode("utf8") return site_data["encrypt_publickey_%s" % index] @@ -200,8 +187,8 @@ class ActionsPlugin: aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) for i in range(num_run): assert len(aes_key) == 32 - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == self.utf8_text.encode("utf8"), "%s != %s" % (ecc.decrypt(encrypted), self.utf8_text.encode("utf8")) + decrypted = CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) + assert decrypted == self.utf8_text.encode("utf8"), "%s != %s" % (decrypted, self.utf8_text.encode("utf8")) yield "." def testCryptEciesDecryptMulti(self, num_run=1): @@ -223,23 +210,16 @@ class ActionsPlugin: gevent.joinall(threads) def testCryptAesEncrypt(self, num_run=1): - from lib import pyelliptic - for i in range(num_run): key = os.urandom(32) - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') - encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) + encrypted = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) yield "." def testCryptAesDecrypt(self, num_run=1): - from lib import pyelliptic - key = os.urandom(32) - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') - encrypted_text = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) + encrypted_text, iv = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) for i in range(num_run): - ctx = pyelliptic.Cipher(key, iv, 0, ciphername='aes-256-cbc') - decrypted = ctx.ciphering(encrypted_text).decode("utf8") + decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, key).decode("utf8") assert decrypted == self.utf8_text yield "." diff --git a/plugins/CryptMessage/Test/TestCrypt.py b/plugins/CryptMessage/Test/TestCrypt.py index 05cc6e44..4315a260 100644 --- a/plugins/CryptMessage/Test/TestCrypt.py +++ b/plugins/CryptMessage/Test/TestCrypt.py @@ -18,13 +18,10 @@ class TestCrypt: assert len(aes_key) == 32 # assert len(encrypted) == 134 + int(len(text) / 16) * 16 # Not always true - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == text_repeated + assert CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) == text_repeated def testDecryptEcies(self, user): - encrypted = base64.b64decode(self.ecies_encrypted_text) - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == b"hello" + assert CryptMessage.eciesDecrypt(self.ecies_encrypted_text, self.privatekey) == b"hello" def testPublickey(self, ui_websocket): pub = ui_websocket.testAction("UserPublickey", 0) diff --git a/requirements.txt b/requirements.txt index 8e756f66..13dfb21c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,4 @@ websocket_client gevent-websocket bencode.py coincurve -python-bitcoinlib maxminddb diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index b6bfaa77..18bdd2f5 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -1,16 +1,22 @@ import logging import base64 +import binascii import time +import hashlib from util import OpensslFindPatch -from lib import pybitcointools as btctools +from util.Electrum import dbl_format from Config import config -lib_verify_best = "btctools" +lib_verify_best = "sslcrypto" +import sslcrypto +sslcurve_native = sslcrypto.ecc.get_curve("secp256k1") +sslcurve_fallback = sslcrypto.fallback.ecc.get_curve("secp256k1") +sslcurve = sslcurve_native def loadLib(lib_name, silent=False): - global bitcoin, libsecp256k1message, lib_verify_best + global sslcurve, libsecp256k1message, lib_verify_best if lib_name == "libsecp256k1": s = time.time() from lib import libsecp256k1message @@ -21,24 +27,10 @@ def loadLib(lib_name, silent=False): "Libsecpk256k1 loaded: %s in %.3fs" % (type(coincurve._libsecp256k1.lib).__name__, time.time() - s) ) - elif lib_name == "openssl": - s = time.time() - import bitcoin.signmessage - import bitcoin.core.key - import bitcoin.wallet - - try: - # OpenSSL 1.1.0 - ssl_version = bitcoin.core.key._ssl.SSLeay() - except AttributeError: - # OpenSSL 1.1.1+ - ssl_version = bitcoin.core.key._ssl.OpenSSL_version_num() - - if not silent: - logging.info( - "OpenSSL loaded: %s, version: %.9X in %.3fs" % - (bitcoin.core.key._ssl, ssl_version, time.time() - s) - ) + elif lib_name == "sslcrypto": + sslcurve = sslcurve_native + elif lib_name == "sslcrypto_fallback": + sslcurve = sslcurve_fallback try: if not config.use_libsecp256k1: @@ -46,35 +38,30 @@ try: loadLib("libsecp256k1") lib_verify_best = "libsecp256k1" except Exception as err: - logging.info("Libsecp256k1 load failed: %s, try to load OpenSSL" % err) - try: - if not config.use_openssl: - raise Exception("Disabled by config") - loadLib("openssl") - lib_verify_best = "openssl" - except Exception as err: - logging.info("OpenSSL load failed: %s, falling back to slow bitcoin verify" % err) + logging.info("Libsecp256k1 load failed: %s" % err) -def newPrivatekey(uncompressed=True): # Return new private key - privatekey = btctools.encode_privkey(btctools.random_key(), "wif") - return privatekey +def newPrivatekey(): # Return new private key + return sslcurve.private_to_wif(sslcurve.new_private_key()).decode() def newSeed(): - return btctools.random_key() + return binascii.hexlify(sslcurve.new_private_key()).decode() def hdPrivatekey(seed, child): - masterkey = btctools.bip32_master_key(bytes(seed, "ascii")) - childkey = btctools.bip32_ckd(masterkey, child % 100000000) # Too large child id could cause problems - key = btctools.bip32_extract_key(childkey) - return btctools.encode_privkey(key, "wif") + # Too large child id could cause problems + privatekey_bin = sslcurve.derive_child(seed.encode(), child % 100000000) + return sslcurve.private_to_wif(privatekey_bin).decode() def privatekeyToAddress(privatekey): # Return address from private key try: - return btctools.privkey_to_address(privatekey) + if len(privatekey) == 64: + privatekey_bin = bytes.fromhex(privatekey) + else: + privatekey_bin = sslcurve.wif_to_private(privatekey.encode()) + return sslcurve.private_to_address(privatekey_bin, is_compressed=False).decode() except Exception: # Invalid privatekey return False @@ -82,8 +69,13 @@ def privatekeyToAddress(privatekey): # Return address from private key def sign(data, privatekey): # Return sign to data using private key if privatekey.startswith("23") and len(privatekey) > 52: return None # Old style private key not supported - sign = btctools.ecdsa_sign(data, privatekey) - return sign + return base64.b64encode(sslcurve.sign( + data.encode(), + sslcurve.wif_to_private(privatekey.encode()), + is_compressed=False, + recoverable=True, + hash=dbl_format + )).decode() def verify(data, valid_address, sign, lib_verify=None): # Verify data using address and sign @@ -95,17 +87,9 @@ def verify(data, valid_address, sign, lib_verify=None): # Verify data using add if lib_verify == "libsecp256k1": sign_address = libsecp256k1message.recover_address(data.encode("utf8"), sign).decode("utf8") - elif lib_verify == "openssl": - sig = base64.b64decode(sign) - message = bitcoin.signmessage.BitcoinMessage(data) - hash = message.GetHash() - - pubkey = bitcoin.core.key.CPubKey.recover_compact(hash, sig) - - sign_address = str(bitcoin.wallet.P2PKHBitcoinAddress.from_pubkey(pubkey)) - elif lib_verify == "btctools": # Use pure-python - pub = btctools.ecdsa_recover(data, sign) - sign_address = btctools.pubtoaddr(pub) + elif lib_verify in ("sslcrypto", "sslcrypto_fallback"): + publickey = sslcurve.recover(base64.b64decode(sign), data.encode(), hash=dbl_format) + sign_address = sslcurve.public_to_address(publickey).decode() else: raise Exception("No library enabled for signature verification") diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 74f27712..d93ee33a 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -407,7 +407,7 @@ def db(request): return db -@pytest.fixture(params=["btctools", "openssl", "libsecp256k1"]) +@pytest.fixture(params=["sslcrypto", "sslcrypto_fallback", "libsecp256k1"]) def crypt_bitcoin_lib(request, monkeypatch): monkeypatch.setattr(CryptBitcoin, "lib_verify_best", request.param) CryptBitcoin.loadLib(request.param) diff --git a/src/lib/libsecp256k1message/libsecp256k1message.py b/src/lib/libsecp256k1message/libsecp256k1message.py index 92802e1c..59768b88 100644 --- a/src/lib/libsecp256k1message/libsecp256k1message.py +++ b/src/lib/libsecp256k1message/libsecp256k1message.py @@ -1,9 +1,9 @@ import hashlib -import struct import base64 from coincurve import PrivateKey, PublicKey from base58 import b58encode_check, b58decode_check from hmac import compare_digest +from util.Electrum import format as zero_format RECID_MIN = 0 RECID_MAX = 3 @@ -97,7 +97,7 @@ def sign_data(secretkey, byte_string): """Sign [byte_string] with [secretkey]. Return serialized signature compatible with Electrum (ZeroNet).""" # encode the message - encoded = _zero_format(byte_string) + encoded = zero_format(byte_string) # sign the message and get a coincurve signature signature = secretkey.sign_recoverable(encoded) # reserialize signature and return it @@ -110,7 +110,7 @@ def verify_data(key_digest, electrum_signature, byte_string): # reserialize signature signature = coincurve_sig(electrum_signature) # encode the message - encoded = _zero_format(byte_string) + encoded = zero_format(byte_string) # recover full public key from signature # "which guarantees a correct signature" publickey = recover_public_key(signature, encoded) @@ -130,45 +130,10 @@ def verify_sig(publickey, signature, byte_string): def verify_key(publickey, key_digest): return compare_digest(key_digest, public_digest(publickey)) - -# Electrum, the heck?! - -def bchr(i): - return struct.pack('B', i) - -def _zero_encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = b''.join([bchr(x) for x in range(256)]) - result = b'' - while val > 0: - index = val % base - result = code_string[index:index + 1] + result - val //= base - return code_string[0:1] * max(minlen - len(result), 0) + result - -def _zero_insane_int(x): - x = int(x) - if x < 253: - return bchr(x) - elif x < 65536: - return bchr(253) + _zero_encode(x, 256, 2)[::-1] - elif x < 4294967296: - return bchr(254) + _zero_encode(x, 256, 4)[::-1] - else: - return bchr(255) + _zero_encode(x, 256, 8)[::-1] - - -def _zero_magic(message): - return b'\x18Bitcoin Signed Message:\n' + _zero_insane_int(len(message)) + message - -def _zero_format(message): - padded = _zero_magic(message) - return hashlib.sha256(padded).digest() - def recover_address(data, sign): sign_bytes = base64.b64decode(sign) is_compressed = ((sign_bytes[0] - 27) & 4) != 0 - publickey = recover_public_key(coincurve_sig(sign_bytes), _zero_format(data)) + publickey = recover_public_key(coincurve_sig(sign_bytes), zero_format(data)) return compute_public_address(publickey, compressed=is_compressed) __all__ = [ diff --git a/src/lib/pybitcointools/LICENSE b/src/lib/pyaes/LICENSE.txt similarity index 80% rename from src/lib/pybitcointools/LICENSE rename to src/lib/pyaes/LICENSE.txt index c47d4ad0..0417a6c2 100644 --- a/src/lib/pybitcointools/LICENSE +++ b/src/lib/pyaes/LICENSE.txt @@ -1,12 +1,6 @@ -This code is public domain. Everyone has the right to do whatever they want -with it for any purpose. - -In case your jurisdiction does not consider the above disclaimer valid or -enforceable, here's an MIT license for you: - The MIT License (MIT) -Copyright (c) 2013 Vitalik Buterin +Copyright (c) 2014 Richard Moore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -25,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/lib/pyaes/README.md b/src/lib/pyaes/README.md new file mode 100644 index 00000000..26e3b2ba --- /dev/null +++ b/src/lib/pyaes/README.md @@ -0,0 +1,363 @@ +pyaes +===== + +A pure-Python implementation of the AES block cipher algorithm and the common modes of operation (CBC, CFB, CTR, ECB and OFB). + + +Features +-------- + +* Supports all AES key sizes +* Supports all AES common modes +* Pure-Python (no external dependencies) +* BlockFeeder API allows streams to easily be encrypted and decrypted +* Python 2.x and 3.x support (make sure you pass in bytes(), not strings for Python 3) + + +API +--- + +All keys may be 128 bits (16 bytes), 192 bits (24 bytes) or 256 bits (32 bytes) long. + +To generate a random key use: +```python +import os + +# 128 bit, 192 bit and 256 bit keys +key_128 = os.urandom(16) +key_192 = os.urandom(24) +key_256 = os.urandom(32) +``` + +To generate keys from simple-to-remember passwords, consider using a _password-based key-derivation function_ such as [scrypt](https://github.com/ricmoo/pyscrypt). + + +### Common Modes of Operation + +There are many modes of operations, each with various pros and cons. In general though, the **CBC** and **CTR** modes are recommended. The **ECB is NOT recommended.**, and is included primarily for completeness. + +Each of the following examples assumes the following key: +```python +import pyaes + +# A 256 bit (32 byte) key +key = "This_key_for_demo_purposes_only!" + +# For some modes of operation we need a random initialization vector +# of 16 bytes +iv = "InitializationVe" +``` + + +#### Counter Mode of Operation (recommended) + +```python +aes = pyaes.AESModeOfOperationCTR(key) +plaintext = "Text may be any length you wish, no padding is required" +ciphertext = aes.encrypt(plaintext) + +# '''\xb6\x99\x10=\xa4\x96\x88\xd1\x89\x1co\xe6\x1d\xef;\x11\x03\xe3\xee +# \xa9V?wY\xbfe\xcdO\xe3\xdf\x9dV\x19\xe5\x8dk\x9fh\xb87>\xdb\xa3\xd6 +# \x86\xf4\xbd\xb0\x97\xf1\t\x02\xe9 \xed''' +print repr(ciphertext) + +# The counter mode of operation maintains state, so decryption requires +# a new instance be created +aes = pyaes.AESModeOfOperationCTR(key) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext + +# To use a custom initial value +counter = pyaes.Counter(initial_value = 100) +aes = pyaes.AESModeOfOperationCTR(key, counter = counter) +ciphertext = aes.encrypt(plaintext) + +# '''WZ\x844\x02\xbfoY\x1f\x12\xa6\xce\x03\x82Ei)\xf6\x97mX\x86\xe3\x9d +# _1\xdd\xbd\x87\xb5\xccEM_4\x01$\xa6\x81\x0b\xd5\x04\xd7Al\x07\xe5 +# \xb2\x0e\\\x0f\x00\x13,\x07''' +print repr(ciphertext) +``` + + +#### Cipher-Block Chaining (recommended) + +```python +aes = pyaes.AESModeOfOperationCBC(key, iv = iv) +plaintext = "TextMustBe16Byte" +ciphertext = aes.encrypt(plaintext) + +# '\xd6:\x18\xe6\xb1\xb3\xc3\xdc\x87\xdf\xa7|\x08{k\xb6' +print repr(ciphertext) + + +# The cipher-block chaining mode of operation maintains state, so +# decryption requires a new instance be created +aes = pyaes.AESModeOfOperationCBC(key, iv = iv) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Cipher Feedback + +```python +# Each block into the mode of operation must be a multiple of the segment +# size. For this example we choose 8 bytes. +aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) +plaintext = "TextMustBeAMultipleOfSegmentSize" +ciphertext = aes.encrypt(plaintext) + +# '''v\xa9\xc1w"\x8aL\x93\xcb\xdf\xa0/\xf8Y\x0b\x8d\x88i\xcb\x85rmp +# \x85\xfe\xafM\x0c)\xd5\xeb\xaf''' +print repr(ciphertext) + + +# The cipher-block chaining mode of operation maintains state, so +# decryption requires a new instance be created +aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Output Feedback Mode of Operation + +```python +aes = pyaes.AESModeOfOperationOFB(key, iv = iv) +plaintext = "Text may be any length you wish, no padding is required" +ciphertext = aes.encrypt(plaintext) + +# '''v\xa9\xc1wO\x92^\x9e\rR\x1e\xf7\xb1\xa2\x9d"l1\xc7\xe7\x9d\x87(\xc26s +# \xdd8\xc8@\xb6\xd9!\xf5\x0cM\xaa\x9b\xc4\xedLD\xe4\xb9\xd8\xdf\x9e\xac +# \xa1\xb8\xea\x0f\x8ev\xb5''' +print repr(ciphertext) + +# The counter mode of operation maintains state, so decryption requires +# a new instance be created +aes = pyaes.AESModeOfOperationOFB(key, iv = iv) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Electronic Codebook (NOT recommended) + +```python +aes = pyaes.AESModeOfOperationECB(key) +plaintext = "TextMustBe16Byte" +ciphertext = aes.encrypt(plaintext) + +# 'L6\x95\x85\xe4\xd9\xf1\x8a\xfb\xe5\x94X\x80|\x19\xc3' +print repr(ciphertext) + +# Since there is no state stored in this mode of operation, it +# is not necessary to create a new aes object for decryption. +#aes = pyaes.AESModeOfOperationECB(key) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +### BlockFeeder + +Since most of the modes of operations require data in specific block-sized or segment-sized blocks, it can be difficult when working with large arbitrary streams or strings of data. + +The BlockFeeder class is meant to make life easier for you, by buffering bytes across multiple calls and returning bytes as they are available, as well as padding or stripping the output when finished, if necessary. + +```python +import pyaes + +# Any mode of operation can be used; for this example CBC +key = "This_key_for_demo_purposes_only!" +iv = "InitializationVe" + +ciphertext = '' + +# We can encrypt one line at a time, regardles of length +encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv)) +for line in file('/etc/passwd'): + ciphertext += encrypter.feed(line) + +# Make a final call to flush any remaining bytes and add paddin +ciphertext += encrypter.feed() + +# We can decrypt the cipher text in chunks (here we split it in half) +decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv)) +decrypted = decrypter.feed(ciphertext[:len(ciphertext) / 2]) +decrypted += decrypter.feed(ciphertext[len(ciphertext) / 2:]) + +# Again, make a final call to flush any remaining bytes and strip padding +decrypted += decrypter.feed() + +print file('/etc/passwd').read() == decrypted +``` + +### Stream Feeder + +This is meant to make it even easier to encrypt and decrypt streams and large files. + +```python +import pyaes + +# Any mode of operation can be used; for this example CTR +key = "This_key_for_demo_purposes_only!" + +# Create the mode of operation to encrypt with +mode = pyaes.AESModeOfOperationCTR(key) + +# The input and output files +file_in = file('/etc/passwd') +file_out = file('/tmp/encrypted.bin', 'wb') + +# Encrypt the data as a stream, the file is read in 8kb chunks, be default +pyaes.encrypt_stream(mode, file_in, file_out) + +# Close the files +file_in.close() +file_out.close() +``` + +Decrypting is identical, except you would use `pyaes.decrypt_stream`, and the encrypted file would be the `file_in` and target for decryption the `file_out`. + +### AES block cipher + +Generally you should use one of the modes of operation above. This may however be useful for experimenting with a custom mode of operation or dealing with encrypted blocks. + +The block cipher requires exactly one block of data to encrypt or decrypt, and each block should be an array with each element an integer representation of a byte. + +```python +import pyaes + +# 16 byte block of plain text +plaintext = "Hello World!!!!!" +plaintext_bytes = [ ord(c) for c in plaintext ] + +# 32 byte key (256 bit) +key = "This_key_for_demo_purposes_only!" + +# Our AES instance +aes = pyaes.AES(key) + +# Encrypt! +ciphertext = aes.encrypt(plaintext_bytes) + +# [55, 250, 182, 25, 185, 208, 186, 95, 206, 115, 50, 115, 108, 58, 174, 115] +print repr(ciphertext) + +# Decrypt! +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext_bytes +``` + +What is a key? +-------------- + +This seems to be a point of confusion for many people new to using encryption. You can think of the key as the *"password"*. However, these algorithms require the *"password"* to be a specific length. + +With AES, there are three possible key lengths, 16-bytes, 24-bytes or 32-bytes. When you create an AES object, the key size is automatically detected, so it is important to pass in a key of the correct length. + +Often, you wish to provide a password of arbitrary length, for example, something easy to remember or write down. In these cases, you must come up with a way to transform the password into a key, of a specific length. A **Password-Based Key Derivation Function** (PBKDF) is an algorithm designed for this exact purpose. + +Here is an example, using the popular (possibly obsolete?) *crypt* PBKDF: + +``` +# See: https://www.dlitz.net/software/python-pbkdf2/ +import pbkdf2 + +password = "HelloWorld" + +# The crypt PBKDF returns a 48-byte string +key = pbkdf2.crypt(password) + +# A 16-byte, 24-byte and 32-byte key, respectively +key_16 = key[:16] +key_24 = key[:24] +key_32 = key[:32] +``` + +The [scrypt](https://github.com/ricmoo/pyscrypt) PBKDF is intentionally slow, to make it more difficult to brute-force guess a password: + +``` +# See: https://github.com/ricmoo/pyscrypt +import pyscrypt + +password = "HelloWorld" + +# Salt is required, and prevents Rainbow Table attacks +salt = "SeaSalt" + +# N, r, and p are parameters to specify how difficult it should be to +# generate a key; bigger numbers take longer and more memory +N = 1024 +r = 1 +p = 1 + +# A 16-byte, 24-byte and 32-byte key, respectively; the scrypt algorithm takes +# a 6-th parameter, indicating key length +key_16 = pyscrypt.hash(password, salt, N, r, p, 16) +key_24 = pyscrypt.hash(password, salt, N, r, p, 24) +key_32 = pyscrypt.hash(password, salt, N, r, p, 32) +``` + +Another possibility, is to use a hashing function, such as SHA256 to hash the password, but this method may be vulnerable to [Rainbow Attacks](http://en.wikipedia.org/wiki/Rainbow_table), unless you use a [salt](http://en.wikipedia.org/wiki/Salt_(cryptography)). + +```python +import hashlib + +password = "HelloWorld" + +# The SHA256 hash algorithm returns a 32-byte string +hashed = hashlib.sha256(password).digest() + +# A 16-byte, 24-byte and 32-byte key, respectively +key_16 = hashed[:16] +key_24 = hashed[:24] +key_32 = hashed +``` + + + + +Performance +----------- + +There is a test case provided in _/tests/test-aes.py_ which does some basic performance testing (its primary purpose is moreso as a regression test). + +Based on that test, in **CPython**, this library is about 30x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 80x slower for CFB; and 300x slower for CTR. + +Based on that same test, in **Pypy**, this library is about 4x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 12x slower for CFB; and 19x slower for CTR. + +The PyCrypto documentation makes reference to the counter call being responsible for the speed problems of the counter (CTR) mode of operation, which is why they use a specially optimized counter. I will investigate this problem further in the future. + + +FAQ +--- + +#### Why do this? + +The short answer, *why not?* + +The longer answer, is for my [pyscrypt](https://github.com/ricmoo/pyscrypt) library. I required a pure-Python AES implementation that supported 256-bit keys with the counter (CTR) mode of operation. After searching, I found several implementations, but all were missing CTR or only supported 128 bit keys. After all the work of learning AES inside and out to implement the library, it was only a marginal amount of extra work to library-ify a more general solution. So, *why not?* + +#### How do I get a question I have added? + +E-mail me at pyaes@ricmoo.com with any questions, suggestions, comments, et cetera. + + +#### Can I give you my money? + +Umm... Ok? :-) + +_Bitcoin_ - `18UDs4qV1shu2CgTS2tKojhCtM69kpnWg9` diff --git a/src/lib/pyaes/__init__.py b/src/lib/pyaes/__init__.py new file mode 100644 index 00000000..5712f794 --- /dev/null +++ b/src/lib/pyaes/__init__.py @@ -0,0 +1,53 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + +# See the README.md for API details and general information. + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +VERSION = [1, 3, 0] + +from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter +from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter +from .blockfeeder import PADDING_NONE, PADDING_DEFAULT diff --git a/src/lib/pyaes/aes.py b/src/lib/pyaes/aes.py new file mode 100644 index 00000000..c6e8bc02 --- /dev/null +++ b/src/lib/pyaes/aes.py @@ -0,0 +1,589 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard + +# Honestly, the best description of the modes of operations are the wonderful +# diagrams on Wikipedia. They explain in moments what my words could never +# achieve. Hence the inline documentation here is sparer than I'd prefer. +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + + +# See the README.md for API details and general information. + + +import copy +import struct + +__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB", + "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"] + + +def _compact_word(word): + return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3] + +def _string_to_bytes(text): + return list(ord(c) for c in text) + +def _bytes_to_string(binary): + return "".join(chr(b) for b in binary) + +def _concat_list(a, b): + return a + b + + +# Python 3 compatibility +try: + xrange +except Exception: + xrange = range + + # Python 3 supports bytes, which is already an array of integers + def _string_to_bytes(text): + if isinstance(text, bytes): + return text + return [ord(c) for c in text] + + # In Python 3, we return bytes + def _bytes_to_string(binary): + return bytes(binary) + + # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first + def _concat_list(a, b): + return a + bytes(b) + + +# Based *largely* on the Rijndael implementation +# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf +class AES(object): + '''Encapsulates the AES block cipher. + + You generally should not need this. Use the AESModeOfOperation classes + below instead.''' + + # Number of rounds by keysize + number_of_rounds = {16: 10, 24: 12, 32: 14} + + # Round constant words + rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ] + + # S-box and Inverse S-box (S is for Substitution) + S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] + Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] + + # Transformations for encryption + T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ] + T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ] + T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ] + T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ] + + # Transformations for decryption + T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ] + T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ] + T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ] + T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ] + + # Transformations for decryption key expansion + U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ] + U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ] + U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ] + U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ] + + def __init__(self, key): + + if len(key) not in (16, 24, 32): + raise ValueError('Invalid key size') + + rounds = self.number_of_rounds[len(key)] + + # Encryption round keys + self._Ke = [[0] * 4 for i in xrange(rounds + 1)] + + # Decryption round keys + self._Kd = [[0] * 4 for i in xrange(rounds + 1)] + + round_key_count = (rounds + 1) * 4 + KC = len(key) // 4 + + # Convert the key into ints + tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ] + + # Copy values into round key arrays + for i in xrange(0, KC): + self._Ke[i // 4][i % 4] = tk[i] + self._Kd[rounds - (i // 4)][i % 4] = tk[i] + + # Key expansion (fips-197 section 5.2) + rconpointer = 0 + t = KC + while t < round_key_count: + + tt = tk[KC - 1] + tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^ + (self.S[(tt >> 8) & 0xFF] << 16) ^ + (self.S[ tt & 0xFF] << 8) ^ + self.S[(tt >> 24) & 0xFF] ^ + (self.rcon[rconpointer] << 24)) + rconpointer += 1 + + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i - 1] + + # Key expansion for 256-bit keys is "slightly different" (fips-197) + else: + for i in xrange(1, KC // 2): + tk[i] ^= tk[i - 1] + tt = tk[KC // 2 - 1] + + tk[KC // 2] ^= (self.S[ tt & 0xFF] ^ + (self.S[(tt >> 8) & 0xFF] << 8) ^ + (self.S[(tt >> 16) & 0xFF] << 16) ^ + (self.S[(tt >> 24) & 0xFF] << 24)) + + for i in xrange(KC // 2 + 1, KC): + tk[i] ^= tk[i - 1] + + # Copy values into round key arrays + j = 0 + while j < KC and t < round_key_count: + self._Ke[t // 4][t % 4] = tk[j] + self._Kd[rounds - (t // 4)][t % 4] = tk[j] + j += 1 + t += 1 + + # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3) + for r in xrange(1, rounds): + for j in xrange(0, 4): + tt = self._Kd[r][j] + self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^ + self.U2[(tt >> 16) & 0xFF] ^ + self.U3[(tt >> 8) & 0xFF] ^ + self.U4[ tt & 0xFF]) + + def encrypt(self, plaintext): + 'Encrypt a block of plain text using the AES block cipher.' + + if len(plaintext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Ke) - 1 + (s1, s2, s3) = [1, 2, 3] + a = [0, 0, 0, 0] + + # Convert plaintext to (ints ^ key) + t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^ + self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T4[ t[(i + s3) % 4] & 0xFF] ^ + self._Ke[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Ke[rounds][i] + result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + def decrypt(self, ciphertext): + 'Decrypt a block of cipher text using the AES block cipher.' + + if len(ciphertext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Kd) - 1 + (s1, s2, s3) = [3, 2, 1] + a = [0, 0, 0, 0] + + # Convert ciphertext to (ints ^ key) + t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^ + self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T8[ t[(i + s3) % 4] & 0xFF] ^ + self._Kd[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Kd[rounds][i] + result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + +class Counter(object): + '''A counter object for the Counter (CTR) mode of operation. + + To create a custom counter, you can usually just override the + increment method.''' + + def __init__(self, initial_value = 1): + + # Convert the value into an array of bytes long + self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ] + + value = property(lambda s: s._counter) + + def increment(self): + '''Increment the counter (overflow rolls back to 0).''' + + for i in xrange(len(self._counter) - 1, -1, -1): + self._counter[i] += 1 + + if self._counter[i] < 256: break + + # Carry the one + self._counter[i] = 0 + + # Overflow + else: + self._counter = [ 0 ] * len(self._counter) + + +class AESBlockModeOfOperation(object): + '''Super-class for AES modes of operation that require blocks.''' + def __init__(self, key): + self._aes = AES(key) + + def decrypt(self, ciphertext): + raise Exception('not implemented') + + def encrypt(self, plaintext): + raise Exception('not implemented') + + +class AESStreamModeOfOperation(AESBlockModeOfOperation): + '''Super-class for AES modes of operation that are stream-ciphers.''' + +class AESSegmentModeOfOperation(AESStreamModeOfOperation): + '''Super-class for AES modes of operation that segment data.''' + + segment_bytes = 16 + + + +class AESModeOfOperationECB(AESBlockModeOfOperation): + '''AES Electronic Codebook Mode of Operation. + + o Block-cipher, so data must be padded to 16 byte boundaries + + Security Notes: + o This mode is not recommended + o Any two identical blocks produce identical encrypted values, + exposing data patterns. (See the image of Tux on wikipedia) + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1''' + + + name = "Electronic Codebook (ECB)" + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + return _bytes_to_string(self._aes.encrypt(plaintext)) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + ciphertext = _string_to_bytes(ciphertext) + return _bytes_to_string(self._aes.decrypt(ciphertext)) + + + +class AESModeOfOperationCBC(AESBlockModeOfOperation): + '''AES Cipher-Block Chaining Mode of Operation. + + o The Initialization Vector (IV) + o Block-cipher, so data must be padded to 16 byte boundaries + o An incorrect initialization vector will only cause the first + block to be corrupt; all other blocks will be intact + o A corrupt bit in the cipher text will cause a block to be + corrupted, and the next block to be inverted, but all other + blocks will be intact. + + Security Notes: + o This method (and CTR) ARE recommended. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2''' + + + name = "Cipher-Block Chaining (CBC)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_cipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_cipherblock = _string_to_bytes(iv) + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ] + self._last_cipherblock = self._aes.encrypt(precipherblock) + + return _bytes_to_string(self._last_cipherblock) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + cipherblock = _string_to_bytes(ciphertext) + plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ] + self._last_cipherblock = cipherblock + + return _bytes_to_string(plaintext) + + + +class AESModeOfOperationCFB(AESSegmentModeOfOperation): + '''AES Cipher Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + but does need to be padded to segment_size + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3''' + + + name = "Cipher Feedback (CFB)" + + def __init__(self, key, iv, segment_size = 1): + if segment_size == 0: segment_size = 1 + + if iv is None: + self._shift_register = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._shift_register = _string_to_bytes(iv) + + self._segment_bytes = segment_size + + AESBlockModeOfOperation.__init__(self, key) + + segment_bytes = property(lambda s: s._segment_bytes) + + def encrypt(self, plaintext): + if len(plaintext) % self._segment_bytes != 0: + raise ValueError('plaintext block must be a multiple of segment_size') + + plaintext = _string_to_bytes(plaintext) + + # Break block into segments + encrypted = [ ] + for i in xrange(0, len(plaintext), self._segment_bytes): + plaintext_segment = plaintext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)] + cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + encrypted.extend(cipher_segment) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + if len(ciphertext) % self._segment_bytes != 0: + raise ValueError('ciphertext block must be a multiple of segment_size') + + ciphertext = _string_to_bytes(ciphertext) + + # Break block into segments + decrypted = [ ] + for i in xrange(0, len(ciphertext), self._segment_bytes): + cipher_segment = ciphertext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)] + plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + decrypted.extend(plaintext_segment) + + return _bytes_to_string(decrypted) + + + +class AESModeOfOperationOFB(AESStreamModeOfOperation): + '''AES Output Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o A bit twiddled in the cipher text, twiddles the same bit in the + same bit in the plain text, which can be useful for error + correction techniques. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4''' + + + name = "Output Feedback (OFB)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_precipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_precipherblock = _string_to_bytes(iv) + + self._remaining_block = [ ] + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + encrypted = [ ] + for p in _string_to_bytes(plaintext): + if len(self._remaining_block) == 0: + self._remaining_block = self._aes.encrypt(self._last_precipherblock) + self._last_precipherblock = [ ] + precipherbyte = self._remaining_block.pop(0) + self._last_precipherblock.append(precipherbyte) + cipherbyte = p ^ precipherbyte + encrypted.append(cipherbyte) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + # AES-OFB is symetric + return self.encrypt(ciphertext) + + + +class AESModeOfOperationCTR(AESStreamModeOfOperation): + '''AES Counter Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o The counter must be the same size as the key size (ie. len(key)) + o Each block independant of the other, so a corrupt byte will not + damage future blocks. + o Each block has a uniue counter value associated with it, which + contributes to the encrypted value, so no data patterns are + leaked. + o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and + Segmented Integer Counter (SIC + + Security Notes: + o This method (and CBC) ARE recommended. + o Each message block is associated with a counter value which must be + unique for ALL messages with the same key. Otherwise security may be + compromised. + + Also see: + + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5 + and Appendix B for managing the initial counter''' + + + name = "Counter (CTR)" + + def __init__(self, key, counter = None): + AESBlockModeOfOperation.__init__(self, key) + + if counter is None: + counter = Counter() + + self._counter = counter + self._remaining_counter = [ ] + + def encrypt(self, plaintext): + while len(self._remaining_counter) < len(plaintext): + self._remaining_counter += self._aes.encrypt(self._counter.value) + self._counter.increment() + + plaintext = _string_to_bytes(plaintext) + + encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ] + self._remaining_counter = self._remaining_counter[len(encrypted):] + + return _bytes_to_string(encrypted) + + def decrypt(self, crypttext): + # AES-CTR is symetric + return self.encrypt(crypttext) + + +# Simple lookup table for each mode +AESModesOfOperation = dict( + ctr = AESModeOfOperationCTR, + cbc = AESModeOfOperationCBC, + cfb = AESModeOfOperationCFB, + ecb = AESModeOfOperationECB, + ofb = AESModeOfOperationOFB, +) diff --git a/src/lib/pyaes/blockfeeder.py b/src/lib/pyaes/blockfeeder.py new file mode 100644 index 00000000..b9a904d2 --- /dev/null +++ b/src/lib/pyaes/blockfeeder.py @@ -0,0 +1,227 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation +from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable + + +# First we inject three functions to each of the modes of operations +# +# _can_consume(size) +# - Given a size, determine how many bytes could be consumed in +# a single call to either the decrypt or encrypt method +# +# _final_encrypt(data, padding = PADDING_DEFAULT) +# - call and return encrypt on this (last) chunk of data, +# padding as necessary; this will always be at least 16 +# bytes unless the total incoming input was less than 16 +# bytes +# +# _final_decrypt(data, padding = PADDING_DEFAULT) +# - same as _final_encrypt except for decrypt, for +# stripping off padding +# + +PADDING_NONE = 'none' +PADDING_DEFAULT = 'default' + +# @TODO: Ciphertext stealing and explicit PKCS#7 +# PADDING_CIPHERTEXT_STEALING +# PADDING_PKCS7 + +# ECB and CBC are block-only ciphers + +def _block_can_consume(self, size): + if size >= 16: return 16 + return 0 + +# After padding, we may have more than one block +def _block_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + data = append_PKCS7_padding(data) + + elif padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + else: + raise Exception('invalid padding option') + + if len(data) == 32: + return self.encrypt(data[:16]) + self.encrypt(data[16:]) + + return self.encrypt(data) + + +def _block_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + return strip_PKCS7_padding(self.decrypt(data)) + + if padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + return self.decrypt(data) + + raise Exception('invalid padding option') + +AESBlockModeOfOperation._can_consume = _block_can_consume +AESBlockModeOfOperation._final_encrypt = _block_final_encrypt +AESBlockModeOfOperation._final_decrypt = _block_final_decrypt + + + +# CFB is a segment cipher + +def _segment_can_consume(self, size): + return self.segment_bytes * int(size // self.segment_bytes) + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.encrypt(padded)[:len(data)] + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.decrypt(padded)[:len(data)] + +AESSegmentModeOfOperation._can_consume = _segment_can_consume +AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt +AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt + + + +# OFB and CTR are stream ciphers + +def _stream_can_consume(self, size): + return size + +def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.encrypt(data) + +def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.decrypt(data) + +AESStreamModeOfOperation._can_consume = _stream_can_consume +AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt +AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt + + + +class BlockFeeder(object): + '''The super-class for objects to handle chunking a stream of bytes + into the appropriate block size for the underlying mode of operation + and applying (or stripping) padding, as necessary.''' + + def __init__(self, mode, feed, final, padding = PADDING_DEFAULT): + self._mode = mode + self._feed = feed + self._final = final + self._buffer = to_bufferable("") + self._padding = padding + + def feed(self, data = None): + '''Provide bytes to encrypt (or decrypt), returning any bytes + possible from this or any previous calls to feed. + + Call with None or an empty string to flush the mode of + operation and return any final bytes; no further calls to + feed may be made.''' + + if self._buffer is None: + raise ValueError('already finished feeder') + + # Finalize; process the spare bytes we were keeping + if data is None: + result = self._final(self._buffer, self._padding) + self._buffer = None + return result + + self._buffer += to_bufferable(data) + + # We keep 16 bytes around so we can determine padding + result = to_bufferable('') + while len(self._buffer) > 16: + can_consume = self._mode._can_consume(len(self._buffer) - 16) + if can_consume == 0: break + result += self._feed(self._buffer[:can_consume]) + self._buffer = self._buffer[can_consume:] + + return result + + +class Encrypter(BlockFeeder): + 'Accepts bytes of plaintext and returns encrypted ciphertext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding) + + +class Decrypter(BlockFeeder): + 'Accepts bytes of ciphertext and returns decrypted plaintext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding) + + +# 8kb blocks +BLOCK_SIZE = (1 << 13) + +def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE): + 'Uses feeder to read and convert from in_stream and write to out_stream.' + + while True: + chunk = in_stream.read(block_size) + if not chunk: + break + converted = feeder.feed(chunk) + out_stream.write(converted) + converted = feeder.feed() + out_stream.write(converted) + + +def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Encrypts a stream of bytes from in_stream to out_stream using mode.' + + encrypter = Encrypter(mode, padding = padding) + _feed_stream(encrypter, in_stream, out_stream, block_size) + + +def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Decrypts a stream of bytes from in_stream to out_stream using mode.' + + decrypter = Decrypter(mode, padding = padding) + _feed_stream(decrypter, in_stream, out_stream, block_size) diff --git a/src/lib/pyaes/util.py b/src/lib/pyaes/util.py new file mode 100644 index 00000000..081a3759 --- /dev/null +++ b/src/lib/pyaes/util.py @@ -0,0 +1,60 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Why to_bufferable? +# Python 3 is very different from Python 2.x when it comes to strings of text +# and strings of bytes; in Python 3, strings of bytes do not exist, instead to +# represent arbitrary binary data, we must use the "bytes" object. This method +# ensures the object behaves as we need it to. + +def to_bufferable(binary): + return binary + +def _get_byte(c): + return ord(c) + +try: + xrange +except: + + def to_bufferable(binary): + if isinstance(binary, bytes): + return binary + return bytes(ord(b) for b in binary) + + def _get_byte(c): + return c + +def append_PKCS7_padding(data): + pad = 16 - (len(data) % 16) + return data + to_bufferable(chr(pad) * pad) + +def strip_PKCS7_padding(data): + if len(data) % 16 != 0: + raise ValueError("invalid length") + + pad = _get_byte(data[-1]) + + if pad > 16: + raise ValueError("invalid padding byte") + + return data[:-pad] diff --git a/src/lib/pybitcointools/__init__.py b/src/lib/pybitcointools/__init__.py deleted file mode 100644 index 7d529abc..00000000 --- a/src/lib/pybitcointools/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .py2specials import * -from .py3specials import * -from .main import * -from .transaction import * -from .deterministic import * -from .bci import * -from .composite import * -from .stealth import * -from .blocks import * -from .mnemonic import * diff --git a/src/lib/pybitcointools/bci.py b/src/lib/pybitcointools/bci.py deleted file mode 100644 index 79a2c401..00000000 --- a/src/lib/pybitcointools/bci.py +++ /dev/null @@ -1,528 +0,0 @@ -#!/usr/bin/python -import json, re -import random -import sys -try: - from urllib.request import build_opener -except: - from urllib2 import build_opener - - -# Makes a request to a given URL (first arg) and optional params (second arg) -def make_request(*args): - opener = build_opener() - opener.addheaders = [('User-agent', - 'Mozilla/5.0'+str(random.randrange(1000000)))] - try: - return opener.open(*args).read().strip() - except Exception as e: - try: - p = e.read().strip() - except: - p = e - raise Exception(p) - - -def is_testnet(inp): - '''Checks if inp is a testnet address or if UTXO is a known testnet TxID''' - if isinstance(inp, (list, tuple)) and len(inp) >= 1: - return any([is_testnet(x) for x in inp]) - elif not isinstance(inp, basestring): # sanity check - raise TypeError("Input must be str/unicode, not type %s" % str(type(inp))) - - if not inp or (inp.lower() in ("btc", "testnet")): - pass - - ## ADDRESSES - if inp[0] in "123mn": - if re.match("^[2mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp): - return True - elif re.match("^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp): - return False - else: - #sys.stderr.write("Bad address format %s") - return None - - ## TXID - elif re.match('^[0-9a-fA-F]{64}$', inp): - base_url = "http://api.blockcypher.com/v1/btc/{network}/txs/{txid}?includesHex=false" - try: - # try testnet fetchtx - make_request(base_url.format(network="test3", txid=inp.lower())) - return True - except: - # try mainnet fetchtx - make_request(base_url.format(network="main", txid=inp.lower())) - return False - sys.stderr.write("TxID %s has no match for testnet or mainnet (Bad TxID)") - return None - else: - raise TypeError("{0} is unknown input".format(inp)) - - -def set_network(*args): - '''Decides if args for unspent/fetchtx/pushtx are mainnet or testnet''' - r = [] - for arg in args: - if not arg: - pass - if isinstance(arg, basestring): - r.append(is_testnet(arg)) - elif isinstance(arg, (list, tuple)): - return set_network(*arg) - if any(r) and not all(r): - raise Exception("Mixed Testnet/Mainnet queries") - return "testnet" if any(r) else "btc" - - -def parse_addr_args(*args): - # Valid input formats: unspent([addr1, addr2, addr3]) - # unspent([addr1, addr2, addr3], network) - # unspent(addr1, addr2, addr3) - # unspent(addr1, addr2, addr3, network) - addr_args = args - network = "btc" - if len(args) == 0: - return [], 'btc' - if len(args) >= 1 and args[-1] in ('testnet', 'btc'): - network = args[-1] - addr_args = args[:-1] - if len(addr_args) == 1 and isinstance(addr_args, list): - network = set_network(*addr_args[0]) - addr_args = addr_args[0] - if addr_args and isinstance(addr_args, tuple) and isinstance(addr_args[0], list): - addr_args = addr_args[0] - network = set_network(addr_args) - return network, addr_args - - -# Gets the unspent outputs of one or more addresses -def bci_unspent(*args): - network, addrs = parse_addr_args(*args) - u = [] - for a in addrs: - try: - data = make_request('https://blockchain.info/unspent?active='+a) - except Exception as e: - if str(e) == 'No free outputs to spend': - continue - else: - raise Exception(e) - try: - jsonobj = json.loads(data.decode("utf-8")) - for o in jsonobj["unspent_outputs"]: - h = o['tx_hash'].decode('hex')[::-1].encode('hex') - u.append({ - "output": h+':'+str(o['tx_output_n']), - "value": o['value'] - }) - except: - raise Exception("Failed to decode data: "+data) - return u - - -def blockr_unspent(*args): - # Valid input formats: blockr_unspent([addr1, addr2,addr3]) - # blockr_unspent(addr1, addr2, addr3) - # blockr_unspent([addr1, addr2, addr3], network) - # blockr_unspent(addr1, addr2, addr3, network) - # Where network is 'btc' or 'testnet' - network, addr_args = parse_addr_args(*args) - - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/address/unspent/' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/address/unspent/' - else: - raise Exception( - 'Unsupported network {0} for blockr_unspent'.format(network)) - - if len(addr_args) == 0: - return [] - elif isinstance(addr_args[0], list): - addrs = addr_args[0] - else: - addrs = addr_args - res = make_request(blockr_url+','.join(addrs)) - data = json.loads(res.decode("utf-8"))['data'] - o = [] - if 'unspent' in data: - data = [data] - for dat in data: - for u in dat['unspent']: - o.append({ - "output": u['tx']+':'+str(u['n']), - "value": int(u['amount'].replace('.', '')) - }) - return o - - -def helloblock_unspent(*args): - addrs, network = parse_addr_args(*args) - if network == 'testnet': - url = 'https://testnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s' - elif network == 'btc': - url = 'https://mainnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s' - o = [] - for addr in addrs: - for offset in xrange(0, 10**9, 500): - res = make_request(url % (addr, offset)) - data = json.loads(res.decode("utf-8"))["data"] - if not len(data["unspents"]): - break - elif offset: - sys.stderr.write("Getting more unspents: %d\n" % offset) - for dat in data["unspents"]: - o.append({ - "output": dat["txHash"]+':'+str(dat["index"]), - "value": dat["value"], - }) - return o - - -unspent_getters = { - 'bci': bci_unspent, - 'blockr': blockr_unspent, - 'helloblock': helloblock_unspent -} - - -def unspent(*args, **kwargs): - f = unspent_getters.get(kwargs.get('source', ''), bci_unspent) - return f(*args) - - -# Gets the transaction output history of a given set of addresses, -# including whether or not they have been spent -def history(*args): - # Valid input formats: history([addr1, addr2,addr3]) - # history(addr1, addr2, addr3) - if len(args) == 0: - return [] - elif isinstance(args[0], list): - addrs = args[0] - else: - addrs = args - - txs = [] - for addr in addrs: - offset = 0 - while 1: - gathered = False - while not gathered: - try: - data = make_request( - 'https://blockchain.info/address/%s?format=json&offset=%s' % - (addr, offset)) - gathered = True - except Exception as e: - try: - sys.stderr.write(e.read().strip()) - except: - sys.stderr.write(str(e)) - gathered = False - try: - jsonobj = json.loads(data.decode("utf-8")) - except: - raise Exception("Failed to decode data: "+data) - txs.extend(jsonobj["txs"]) - if len(jsonobj["txs"]) < 50: - break - offset += 50 - sys.stderr.write("Fetching more transactions... "+str(offset)+'\n') - outs = {} - for tx in txs: - for o in tx["out"]: - if o.get('addr', None) in addrs: - key = str(tx["tx_index"])+':'+str(o["n"]) - outs[key] = { - "address": o["addr"], - "value": o["value"], - "output": tx["hash"]+':'+str(o["n"]), - "block_height": tx.get("block_height", None) - } - for tx in txs: - for i, inp in enumerate(tx["inputs"]): - if "prev_out" in inp: - if inp["prev_out"].get("addr", None) in addrs: - key = str(inp["prev_out"]["tx_index"]) + \ - ':'+str(inp["prev_out"]["n"]) - if outs.get(key): - outs[key]["spend"] = tx["hash"]+':'+str(i) - return [outs[k] for k in outs] - - -# Pushes a transaction to the network using https://blockchain.info/pushtx -def bci_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request('https://blockchain.info/pushtx', 'tx='+tx) - - -def eligius_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - s = make_request( - 'http://eligius.st/~wizkid057/newstats/pushtxn.php', - 'transaction='+tx+'&send=Push') - strings = re.findall('string[^"]*"[^"]*"', s) - for string in strings: - quote = re.findall('"[^"]*"', string)[0] - if len(quote) >= 5: - return quote[1:-1] - - -def blockr_pushtx(tx, network='btc'): - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/tx/push' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/tx/push' - else: - raise Exception( - 'Unsupported network {0} for blockr_pushtx'.format(network)) - - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request(blockr_url, '{"hex":"%s"}' % tx) - - -def helloblock_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request('https://mainnet.helloblock.io/v1/transactions', - 'rawTxHex='+tx) - -pushtx_getters = { - 'bci': bci_pushtx, - 'blockr': blockr_pushtx, - 'helloblock': helloblock_pushtx -} - - -def pushtx(*args, **kwargs): - f = pushtx_getters.get(kwargs.get('source', ''), bci_pushtx) - return f(*args) - - -def last_block_height(network='btc'): - if network == 'testnet': - data = make_request('http://tbtc.blockr.io/api/v1/block/info/last') - jsonobj = json.loads(data.decode("utf-8")) - return jsonobj["data"]["nb"] - - data = make_request('https://blockchain.info/latestblock') - jsonobj = json.loads(data.decode("utf-8")) - return jsonobj["height"] - - -# Gets a specific transaction -def bci_fetchtx(txhash): - if isinstance(txhash, list): - return [bci_fetchtx(h) for h in txhash] - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - data = make_request('https://blockchain.info/rawtx/'+txhash+'?format=hex') - return data - - -def blockr_fetchtx(txhash, network='btc'): - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/tx/raw/' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/tx/raw/' - else: - raise Exception( - 'Unsupported network {0} for blockr_fetchtx'.format(network)) - if isinstance(txhash, list): - txhash = ','.join([x.encode('hex') if not re.match('^[0-9a-fA-F]*$', x) - else x for x in txhash]) - jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8")) - return [d['tx']['hex'] for d in jsondata['data']] - else: - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8")) - return jsondata['data']['tx']['hex'] - - -def helloblock_fetchtx(txhash, network='btc'): - if isinstance(txhash, list): - return [helloblock_fetchtx(h) for h in txhash] - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - if network == 'testnet': - url = 'https://testnet.helloblock.io/v1/transactions/' - elif network == 'btc': - url = 'https://mainnet.helloblock.io/v1/transactions/' - else: - raise Exception( - 'Unsupported network {0} for helloblock_fetchtx'.format(network)) - data = json.loads(make_request(url + txhash).decode("utf-8"))["data"]["transaction"] - o = { - "locktime": data["locktime"], - "version": data["version"], - "ins": [], - "outs": [] - } - for inp in data["inputs"]: - o["ins"].append({ - "script": inp["scriptSig"], - "outpoint": { - "index": inp["prevTxoutIndex"], - "hash": inp["prevTxHash"], - }, - "sequence": 4294967295 - }) - for outp in data["outputs"]: - o["outs"].append({ - "value": outp["value"], - "script": outp["scriptPubKey"] - }) - from .transaction import serialize - from .transaction import txhash as TXHASH - tx = serialize(o) - assert TXHASH(tx) == txhash - return tx - - -fetchtx_getters = { - 'bci': bci_fetchtx, - 'blockr': blockr_fetchtx, - 'helloblock': helloblock_fetchtx -} - - -def fetchtx(*args, **kwargs): - f = fetchtx_getters.get(kwargs.get('source', ''), bci_fetchtx) - return f(*args) - - -def firstbits(address): - if len(address) >= 25: - return make_request('https://blockchain.info/q/getfirstbits/'+address) - else: - return make_request( - 'https://blockchain.info/q/resolvefirstbits/'+address) - - -def get_block_at_height(height): - j = json.loads(make_request("https://blockchain.info/block-height/" + - str(height)+"?format=json").decode("utf-8")) - for b in j['blocks']: - if b['main_chain'] is True: - return b - raise Exception("Block at this height not found") - - -def _get_block(inp): - if len(str(inp)) < 64: - return get_block_at_height(inp) - else: - return json.loads(make_request( - 'https://blockchain.info/rawblock/'+inp).decode("utf-8")) - - -def bci_get_block_header_data(inp): - j = _get_block(inp) - return { - 'version': j['ver'], - 'hash': j['hash'], - 'prevhash': j['prev_block'], - 'timestamp': j['time'], - 'merkle_root': j['mrkl_root'], - 'bits': j['bits'], - 'nonce': j['nonce'], - } - -def blockr_get_block_header_data(height, network='btc'): - if network == 'testnet': - blockr_url = "http://tbtc.blockr.io/api/v1/block/raw/" - elif network == 'btc': - blockr_url = "http://btc.blockr.io/api/v1/block/raw/" - else: - raise Exception( - 'Unsupported network {0} for blockr_get_block_header_data'.format(network)) - - k = json.loads(make_request(blockr_url + str(height)).decode("utf-8")) - j = k['data'] - return { - 'version': j['version'], - 'hash': j['hash'], - 'prevhash': j['previousblockhash'], - 'timestamp': j['time'], - 'merkle_root': j['merkleroot'], - 'bits': int(j['bits'], 16), - 'nonce': j['nonce'], - } - - -def get_block_timestamp(height, network='btc'): - if network == 'testnet': - blockr_url = "http://tbtc.blockr.io/api/v1/block/info/" - elif network == 'btc': - blockr_url = "http://btc.blockr.io/api/v1/block/info/" - else: - raise Exception( - 'Unsupported network {0} for get_block_timestamp'.format(network)) - - import time, calendar - if isinstance(height, list): - k = json.loads(make_request(blockr_url + ','.join([str(x) for x in height])).decode("utf-8")) - o = {x['nb']: calendar.timegm(time.strptime(x['time_utc'], - "%Y-%m-%dT%H:%M:%SZ")) for x in k['data']} - return [o[x] for x in height] - else: - k = json.loads(make_request(blockr_url + str(height)).decode("utf-8")) - j = k['data']['time_utc'] - return calendar.timegm(time.strptime(j, "%Y-%m-%dT%H:%M:%SZ")) - - -block_header_data_getters = { - 'bci': bci_get_block_header_data, - 'blockr': blockr_get_block_header_data -} - - -def get_block_header_data(inp, **kwargs): - f = block_header_data_getters.get(kwargs.get('source', ''), - bci_get_block_header_data) - return f(inp, **kwargs) - - -def get_txs_in_block(inp): - j = _get_block(inp) - hashes = [t['hash'] for t in j['tx']] - return hashes - - -def get_block_height(txhash): - j = json.loads(make_request('https://blockchain.info/rawtx/'+txhash).decode("utf-8")) - return j['block_height'] - -# fromAddr, toAddr, 12345, changeAddress -def get_tx_composite(inputs, outputs, output_value, change_address=None, network=None): - """mktx using blockcypher API""" - inputs = [inputs] if not isinstance(inputs, list) else inputs - outputs = [outputs] if not isinstance(outputs, list) else outputs - network = set_network(change_address or inputs) if not network else network.lower() - url = "http://api.blockcypher.com/v1/btc/{network}/txs/new?includeToSignTx=true".format( - network=('test3' if network=='testnet' else 'main')) - is_address = lambda a: bool(re.match("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", a)) - if any([is_address(x) for x in inputs]): - inputs_type = 'addresses' # also accepts UTXOs, only addresses supported presently - if any([is_address(x) for x in outputs]): - outputs_type = 'addresses' # TODO: add UTXO support - data = { - 'inputs': [{inputs_type: inputs}], - 'confirmations': 0, - 'preference': 'high', - 'outputs': [{outputs_type: outputs, "value": output_value}] - } - if change_address: - data["change_address"] = change_address # - jdata = json.loads(make_request(url, data)) - hash, txh = jdata.get("tosign")[0], jdata.get("tosign_tx")[0] - assert bin_dbl_sha256(txh.decode('hex')).encode('hex') == hash, "checksum mismatch %s" % hash - return txh.encode("utf-8") - -blockcypher_mktx = get_tx_composite diff --git a/src/lib/pybitcointools/blocks.py b/src/lib/pybitcointools/blocks.py deleted file mode 100644 index 9df6b35c..00000000 --- a/src/lib/pybitcointools/blocks.py +++ /dev/null @@ -1,50 +0,0 @@ -from .main import * - - -def serialize_header(inp): - o = encode(inp['version'], 256, 4)[::-1] + \ - inp['prevhash'].decode('hex')[::-1] + \ - inp['merkle_root'].decode('hex')[::-1] + \ - encode(inp['timestamp'], 256, 4)[::-1] + \ - encode(inp['bits'], 256, 4)[::-1] + \ - encode(inp['nonce'], 256, 4)[::-1] - h = bin_sha256(bin_sha256(o))[::-1].encode('hex') - assert h == inp['hash'], (sha256(o), inp['hash']) - return o.encode('hex') - - -def deserialize_header(inp): - inp = inp.decode('hex') - return { - "version": decode(inp[:4][::-1], 256), - "prevhash": inp[4:36][::-1].encode('hex'), - "merkle_root": inp[36:68][::-1].encode('hex'), - "timestamp": decode(inp[68:72][::-1], 256), - "bits": decode(inp[72:76][::-1], 256), - "nonce": decode(inp[76:80][::-1], 256), - "hash": bin_sha256(bin_sha256(inp))[::-1].encode('hex') - } - - -def mk_merkle_proof(header, hashes, index): - nodes = [h.decode('hex')[::-1] for h in hashes] - if len(nodes) % 2 and len(nodes) > 2: - nodes.append(nodes[-1]) - layers = [nodes] - while len(nodes) > 1: - newnodes = [] - for i in range(0, len(nodes) - 1, 2): - newnodes.append(bin_sha256(bin_sha256(nodes[i] + nodes[i+1]))) - if len(newnodes) % 2 and len(newnodes) > 2: - newnodes.append(newnodes[-1]) - nodes = newnodes - layers.append(nodes) - # Sanity check, make sure merkle root is valid - assert nodes[0][::-1].encode('hex') == header['merkle_root'] - merkle_siblings = \ - [layers[i][(index >> i) ^ 1] for i in range(len(layers)-1)] - return { - "hash": hashes[index], - "siblings": [x[::-1].encode('hex') for x in merkle_siblings], - "header": header - } diff --git a/src/lib/pybitcointools/composite.py b/src/lib/pybitcointools/composite.py deleted file mode 100644 index e5d50492..00000000 --- a/src/lib/pybitcointools/composite.py +++ /dev/null @@ -1,128 +0,0 @@ -from .main import * -from .transaction import * -from .bci import * -from .deterministic import * -from .blocks import * - - -# Takes privkey, address, value (satoshis), fee (satoshis) -def send(frm, to, value, fee=10000, **kwargs): - return sendmultitx(frm, to + ":" + str(value), fee, **kwargs) - - -# Takes privkey, "address1:value1,address2:value2" (satoshis), fee (satoshis) -def sendmultitx(frm, *args, **kwargs): - tv, fee = args[:-1], int(args[-1]) - outs = [] - outvalue = 0 - for a in tv: - outs.append(a) - outvalue += int(a.split(":")[1]) - - u = unspent(privtoaddr(frm), **kwargs) - u2 = select(u, int(outvalue)+int(fee)) - argz = u2 + outs + [privtoaddr(frm), fee] - tx = mksend(*argz) - tx2 = signall(tx, frm) - return pushtx(tx2, **kwargs) - - -# Takes address, address, value (satoshis), fee(satoshis) -def preparetx(frm, to, value, fee=10000, **kwargs): - tovalues = to + ":" + str(value) - return preparemultitx(frm, tovalues, fee, **kwargs) - - -# Takes address, address:value, address:value ... (satoshis), fee(satoshis) -def preparemultitx(frm, *args, **kwargs): - tv, fee = args[:-1], int(args[-1]) - outs = [] - outvalue = 0 - for a in tv: - outs.append(a) - outvalue += int(a.split(":")[1]) - - u = unspent(frm, **kwargs) - u2 = select(u, int(outvalue)+int(fee)) - argz = u2 + outs + [frm, fee] - return mksend(*argz) - - -# BIP32 hierarchical deterministic multisig script -def bip32_hdm_script(*args): - if len(args) == 3: - keys, req, path = args - else: - i, keys, path = 0, [], [] - while len(args[i]) > 40: - keys.append(args[i]) - i += 1 - req = int(args[i]) - path = map(int, args[i+1:]) - pubs = sorted(map(lambda x: bip32_descend(x, path), keys)) - return mk_multisig_script(pubs, req) - - -# BIP32 hierarchical deterministic multisig address -def bip32_hdm_addr(*args): - return scriptaddr(bip32_hdm_script(*args)) - - -# Setup a coinvault transaction -def setup_coinvault_tx(tx, script): - txobj = deserialize(tx) - N = deserialize_script(script)[-2] - for inp in txobj["ins"]: - inp["script"] = serialize_script([None] * (N+1) + [script]) - return serialize(txobj) - - -# Sign a coinvault transaction -def sign_coinvault_tx(tx, priv): - pub = privtopub(priv) - txobj = deserialize(tx) - subscript = deserialize_script(txobj['ins'][0]['script']) - oscript = deserialize_script(subscript[-1]) - k, pubs = oscript[0], oscript[1:-2] - for j in range(len(txobj['ins'])): - scr = deserialize_script(txobj['ins'][j]['script']) - for i, p in enumerate(pubs): - if p == pub: - scr[i+1] = multisign(tx, j, subscript[-1], priv) - if len(filter(lambda x: x, scr[1:-1])) >= k: - scr = [None] + filter(lambda x: x, scr[1:-1])[:k] + [scr[-1]] - txobj['ins'][j]['script'] = serialize_script(scr) - return serialize(txobj) - - -# Inspects a transaction -def inspect(tx, **kwargs): - d = deserialize(tx) - isum = 0 - ins = {} - for _in in d['ins']: - h = _in['outpoint']['hash'] - i = _in['outpoint']['index'] - prevout = deserialize(fetchtx(h, **kwargs))['outs'][i] - isum += prevout['value'] - a = script_to_address(prevout['script']) - ins[a] = ins.get(a, 0) + prevout['value'] - outs = [] - osum = 0 - for _out in d['outs']: - outs.append({'address': script_to_address(_out['script']), - 'value': _out['value']}) - osum += _out['value'] - return { - 'fee': isum - osum, - 'outs': outs, - 'ins': ins - } - - -def merkle_prove(txhash): - blocknum = str(get_block_height(txhash)) - header = get_block_header_data(blocknum) - hashes = get_txs_in_block(blocknum) - i = hashes.index(txhash) - return mk_merkle_proof(header, hashes, i) diff --git a/src/lib/pybitcointools/deterministic.py b/src/lib/pybitcointools/deterministic.py deleted file mode 100644 index b2bdbbc6..00000000 --- a/src/lib/pybitcointools/deterministic.py +++ /dev/null @@ -1,199 +0,0 @@ -from .main import * -import hmac -import hashlib -from binascii import hexlify -# Electrum wallets - - -def electrum_stretch(seed): - return slowsha(seed) - -# Accepts seed or stretched seed, returns master public key - - -def electrum_mpk(seed): - if len(seed) == 32: - seed = electrum_stretch(seed) - return privkey_to_pubkey(seed)[2:] - -# Accepts (seed or stretched seed), index and secondary index -# (conventionally 0 for ordinary addresses, 1 for change) , returns privkey - - -def electrum_privkey(seed, n, for_change=0): - if len(seed) == 32: - seed = electrum_stretch(seed) - mpk = electrum_mpk(seed) - offset = dbl_sha256(from_int_representation_to_bytes(n)+b':'+from_int_representation_to_bytes(for_change)+b':'+binascii.unhexlify(mpk)) - return add_privkeys(seed, offset) - -# Accepts (seed or stretched seed or master pubkey), index and secondary index -# (conventionally 0 for ordinary addresses, 1 for change) , returns pubkey - - -def electrum_pubkey(masterkey, n, for_change=0): - if len(masterkey) == 32: - mpk = electrum_mpk(electrum_stretch(masterkey)) - elif len(masterkey) == 64: - mpk = electrum_mpk(masterkey) - else: - mpk = masterkey - bin_mpk = encode_pubkey(mpk, 'bin_electrum') - offset = bin_dbl_sha256(from_int_representation_to_bytes(n)+b':'+from_int_representation_to_bytes(for_change)+b':'+bin_mpk) - return add_pubkeys('04'+mpk, privtopub(offset)) - -# seed/stretched seed/pubkey -> address (convenience method) - - -def electrum_address(masterkey, n, for_change=0, version=0): - return pubkey_to_address(electrum_pubkey(masterkey, n, for_change), version) - -# Given a master public key, a private key from that wallet and its index, -# cracks the secret exponent which can be used to generate all other private -# keys in the wallet - - -def crack_electrum_wallet(mpk, pk, n, for_change=0): - bin_mpk = encode_pubkey(mpk, 'bin_electrum') - offset = dbl_sha256(str(n)+':'+str(for_change)+':'+bin_mpk) - return subtract_privkeys(pk, offset) - -# Below code ASSUMES binary inputs and compressed pubkeys -MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' -MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' -TESTNET_PRIVATE = b'\x04\x35\x83\x94' -TESTNET_PUBLIC = b'\x04\x35\x87\xCF' -PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE] -PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC] - -# BIP32 child key derivation - - -def raw_bip32_ckd(rawtuple, i): - vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple - i = int(i) - - if vbytes in PRIVATE: - priv = key - pub = privtopub(key) - else: - pub = key - - if i >= 2**31: - if vbytes in PUBLIC: - raise Exception("Can't do private derivation on public key!") - I = hmac.new(chaincode, b'\x00'+priv[:32]+encode(i, 256, 4), hashlib.sha512).digest() - else: - I = hmac.new(chaincode, pub+encode(i, 256, 4), hashlib.sha512).digest() - - if vbytes in PRIVATE: - newkey = add_privkeys(I[:32]+B'\x01', priv) - fingerprint = bin_hash160(privtopub(key))[:4] - if vbytes in PUBLIC: - newkey = add_pubkeys(compress(privtopub(I[:32])), key) - fingerprint = bin_hash160(key)[:4] - - return (vbytes, depth + 1, fingerprint, i, I[32:], newkey) - - -def bip32_serialize(rawtuple): - vbytes, depth, fingerprint, i, chaincode, key = rawtuple - i = encode(i, 256, 4) - chaincode = encode(hash_to_int(chaincode), 256, 32) - keydata = b'\x00'+key[:-1] if vbytes in PRIVATE else key - bindata = vbytes + from_int_to_byte(depth % 256) + fingerprint + i + chaincode + keydata - return changebase(bindata+bin_dbl_sha256(bindata)[:4], 256, 58) - - -def bip32_deserialize(data): - dbin = changebase(data, 58, 256) - if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]: - raise Exception("Invalid checksum") - vbytes = dbin[0:4] - depth = from_byte_to_int(dbin[4]) - fingerprint = dbin[5:9] - i = decode(dbin[9:13], 256) - chaincode = dbin[13:45] - key = dbin[46:78]+b'\x01' if vbytes in PRIVATE else dbin[45:78] - return (vbytes, depth, fingerprint, i, chaincode, key) - - -def raw_bip32_privtopub(rawtuple): - vbytes, depth, fingerprint, i, chaincode, key = rawtuple - newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC - return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key)) - - -def bip32_privtopub(data): - return bip32_serialize(raw_bip32_privtopub(bip32_deserialize(data))) - - -def bip32_ckd(data, i): - return bip32_serialize(raw_bip32_ckd(bip32_deserialize(data), i)) - - -def bip32_master_key(seed, vbytes=MAINNET_PRIVATE): - I = hmac.new(from_string_to_bytes("Bitcoin seed"), seed, hashlib.sha512).digest() - return bip32_serialize((vbytes, 0, b'\x00'*4, 0, I[32:], I[:32]+b'\x01')) - - -def bip32_bin_extract_key(data): - return bip32_deserialize(data)[-1] - - -def bip32_extract_key(data): - return safe_hexlify(bip32_deserialize(data)[-1]) - -# Exploits the same vulnerability as above in Electrum wallets -# Takes a BIP32 pubkey and one of the child privkeys of its corresponding -# privkey and returns the BIP32 privkey associated with that pubkey - - -def raw_crack_bip32_privkey(parent_pub, priv): - vbytes, depth, fingerprint, i, chaincode, key = priv - pvbytes, pdepth, pfingerprint, pi, pchaincode, pkey = parent_pub - i = int(i) - - if i >= 2**31: - raise Exception("Can't crack private derivation!") - - I = hmac.new(pchaincode, pkey+encode(i, 256, 4), hashlib.sha512).digest() - - pprivkey = subtract_privkeys(key, I[:32]+b'\x01') - - newvbytes = MAINNET_PRIVATE if vbytes == MAINNET_PUBLIC else TESTNET_PRIVATE - return (newvbytes, pdepth, pfingerprint, pi, pchaincode, pprivkey) - - -def crack_bip32_privkey(parent_pub, priv): - dsppub = bip32_deserialize(parent_pub) - dspriv = bip32_deserialize(priv) - return bip32_serialize(raw_crack_bip32_privkey(dsppub, dspriv)) - - -def coinvault_pub_to_bip32(*args): - if len(args) == 1: - args = args[0].split(' ') - vals = map(int, args[34:]) - I1 = ''.join(map(chr, vals[:33])) - I2 = ''.join(map(chr, vals[35:67])) - return bip32_serialize((MAINNET_PUBLIC, 0, b'\x00'*4, 0, I2, I1)) - - -def coinvault_priv_to_bip32(*args): - if len(args) == 1: - args = args[0].split(' ') - vals = map(int, args[34:]) - I2 = ''.join(map(chr, vals[35:67])) - I3 = ''.join(map(chr, vals[72:104])) - return bip32_serialize((MAINNET_PRIVATE, 0, b'\x00'*4, 0, I2, I3+b'\x01')) - - -def bip32_descend(*args): - if len(args) == 2 and isinstance(args[1], list): - key, path = args - else: - key, path = args[0], map(int, args[1:]) - for p in path: - key = bip32_ckd(key, p) - return bip32_extract_key(key) diff --git a/src/lib/pybitcointools/english.txt b/src/lib/pybitcointools/english.txt deleted file mode 100644 index 942040ed..00000000 --- a/src/lib/pybitcointools/english.txt +++ /dev/null @@ -1,2048 +0,0 @@ -abandon -ability -able -about -above -absent -absorb -abstract -absurd -abuse -access -accident -account -accuse -achieve -acid -acoustic -acquire -across -act -action -actor -actress -actual -adapt -add -addict -address -adjust -admit -adult -advance -advice -aerobic -affair -afford -afraid -again -age -agent -agree -ahead -aim -air -airport -aisle -alarm -album -alcohol -alert -alien -all -alley -allow -almost -alone -alpha -already -also -alter -always -amateur -amazing -among -amount -amused -analyst -anchor -ancient -anger -angle -angry -animal -ankle -announce -annual -another -answer -antenna -antique -anxiety -any -apart -apology -appear -apple -approve -april -arch -arctic -area -arena -argue -arm -armed -armor -army -around -arrange -arrest -arrive -arrow -art -artefact -artist -artwork -ask -aspect -assault -asset -assist -assume -asthma -athlete -atom -attack -attend -attitude -attract -auction -audit -august -aunt -author -auto -autumn -average -avocado -avoid -awake -aware -away -awesome -awful -awkward -axis -baby -bachelor -bacon -badge -bag -balance -balcony -ball -bamboo -banana -banner -bar -barely -bargain -barrel -base -basic -basket -battle -beach -bean -beauty -because -become -beef -before -begin -behave -behind -believe -below -belt -bench -benefit -best -betray -better -between -beyond -bicycle -bid -bike -bind -biology -bird -birth -bitter -black -blade -blame -blanket -blast -bleak -bless -blind -blood -blossom -blouse -blue -blur -blush -board -boat -body -boil -bomb -bone -bonus -book -boost -border -boring -borrow -boss -bottom -bounce -box -boy -bracket -brain -brand -brass -brave -bread -breeze -brick -bridge -brief -bright -bring -brisk -broccoli -broken -bronze -broom -brother -brown -brush -bubble -buddy -budget -buffalo -build -bulb -bulk -bullet -bundle -bunker -burden -burger -burst -bus -business -busy -butter -buyer -buzz -cabbage -cabin -cable -cactus -cage -cake -call -calm -camera -camp -can -canal -cancel -candy -cannon -canoe -canvas -canyon -capable -capital -captain -car -carbon -card -cargo -carpet -carry -cart -case -cash -casino -castle -casual -cat -catalog -catch -category -cattle -caught -cause -caution -cave -ceiling -celery -cement -census -century -cereal -certain -chair -chalk -champion -change -chaos -chapter -charge -chase -chat -cheap -check -cheese -chef -cherry -chest -chicken -chief -child -chimney -choice -choose -chronic -chuckle -chunk -churn -cigar -cinnamon -circle -citizen -city -civil -claim -clap -clarify -claw -clay -clean -clerk -clever -click -client -cliff -climb -clinic -clip -clock -clog -close -cloth -cloud -clown -club -clump -cluster -clutch -coach -coast -coconut -code -coffee -coil -coin -collect -color -column -combine -come -comfort -comic -common -company -concert -conduct -confirm -congress -connect -consider -control -convince -cook -cool -copper -copy -coral -core -corn -correct -cost -cotton -couch -country -couple -course -cousin -cover -coyote -crack -cradle -craft -cram -crane -crash -crater -crawl -crazy -cream -credit -creek -crew -cricket -crime -crisp -critic -crop -cross -crouch -crowd -crucial -cruel -cruise -crumble -crunch -crush -cry -crystal -cube -culture -cup -cupboard -curious -current -curtain -curve -cushion -custom -cute -cycle -dad -damage -damp -dance -danger -daring -dash -daughter -dawn -day -deal -debate -debris -decade -december -decide -decline -decorate -decrease -deer -defense -define -defy -degree -delay -deliver -demand -demise -denial -dentist -deny -depart -depend -deposit -depth -deputy -derive -describe -desert -design -desk -despair -destroy -detail -detect -develop -device -devote -diagram -dial -diamond -diary -dice -diesel -diet -differ -digital -dignity -dilemma -dinner -dinosaur -direct -dirt -disagree -discover -disease -dish -dismiss -disorder -display -distance -divert -divide -divorce -dizzy -doctor -document -dog -doll -dolphin -domain -donate -donkey -donor -door -dose -double -dove -draft -dragon -drama -drastic -draw -dream -dress -drift -drill -drink -drip -drive -drop -drum -dry -duck -dumb -dune -during -dust -dutch -duty -dwarf -dynamic -eager -eagle -early -earn -earth -easily -east -easy -echo -ecology -economy -edge -edit -educate -effort -egg -eight -either -elbow -elder -electric -elegant -element -elephant -elevator -elite -else -embark -embody -embrace -emerge -emotion -employ -empower -empty -enable -enact -end -endless -endorse -enemy -energy -enforce -engage -engine -enhance -enjoy -enlist -enough -enrich -enroll -ensure -enter -entire -entry -envelope -episode -equal -equip -era -erase -erode -erosion -error -erupt -escape -essay -essence -estate -eternal -ethics -evidence -evil -evoke -evolve -exact -example -excess -exchange -excite -exclude -excuse -execute -exercise -exhaust -exhibit -exile -exist -exit -exotic -expand -expect -expire -explain -expose -express -extend -extra -eye -eyebrow -fabric -face -faculty -fade -faint -faith -fall -false -fame -family -famous -fan -fancy -fantasy -farm -fashion -fat -fatal -father -fatigue -fault -favorite -feature -february -federal -fee -feed -feel -female -fence -festival -fetch -fever -few -fiber -fiction -field -figure -file -film -filter -final -find -fine -finger -finish -fire -firm -first -fiscal -fish -fit -fitness -fix -flag -flame -flash -flat -flavor -flee -flight -flip -float -flock -floor -flower -fluid -flush -fly -foam -focus -fog -foil -fold -follow -food -foot -force -forest -forget -fork -fortune -forum -forward -fossil -foster -found -fox -fragile -frame -frequent -fresh -friend -fringe -frog -front -frost -frown -frozen -fruit -fuel -fun -funny -furnace -fury -future -gadget -gain -galaxy -gallery -game -gap -garage -garbage -garden -garlic -garment -gas -gasp -gate -gather -gauge -gaze -general -genius -genre -gentle -genuine -gesture -ghost -giant -gift -giggle -ginger -giraffe -girl -give -glad -glance -glare -glass -glide -glimpse -globe -gloom -glory -glove -glow -glue -goat -goddess -gold -good -goose -gorilla -gospel -gossip -govern -gown -grab -grace -grain -grant -grape -grass -gravity -great -green -grid -grief -grit -grocery -group -grow -grunt -guard -guess -guide -guilt -guitar -gun -gym -habit -hair -half -hammer -hamster -hand -happy -harbor -hard -harsh -harvest -hat -have -hawk -hazard -head -health -heart -heavy -hedgehog -height -hello -helmet -help -hen -hero -hidden -high -hill -hint -hip -hire -history -hobby -hockey -hold -hole -holiday -hollow -home -honey -hood -hope -horn -horror -horse -hospital -host -hotel -hour -hover -hub -huge -human -humble -humor -hundred -hungry -hunt -hurdle -hurry -hurt -husband -hybrid -ice -icon -idea -identify -idle -ignore -ill -illegal -illness -image -imitate -immense -immune -impact -impose -improve -impulse -inch -include -income -increase -index -indicate -indoor -industry -infant -inflict -inform -inhale -inherit -initial -inject -injury -inmate -inner -innocent -input -inquiry -insane -insect -inside -inspire -install -intact -interest -into -invest -invite -involve -iron -island -isolate -issue -item -ivory -jacket -jaguar -jar -jazz -jealous -jeans -jelly -jewel -job -join -joke -journey -joy -judge -juice -jump -jungle -junior -junk -just -kangaroo -keen -keep -ketchup -key -kick -kid -kidney -kind -kingdom -kiss -kit -kitchen -kite -kitten -kiwi -knee -knife -knock -know -lab -label -labor -ladder -lady -lake -lamp -language -laptop -large -later -latin -laugh -laundry -lava -law -lawn -lawsuit -layer -lazy -leader -leaf -learn -leave -lecture -left -leg -legal -legend -leisure -lemon -lend -length -lens -leopard -lesson -letter -level -liar -liberty -library -license -life -lift -light -like -limb -limit -link -lion -liquid -list -little -live -lizard -load -loan -lobster -local -lock -logic -lonely -long -loop -lottery -loud -lounge -love -loyal -lucky -luggage -lumber -lunar -lunch -luxury -lyrics -machine -mad -magic -magnet -maid -mail -main -major -make -mammal -man -manage -mandate -mango -mansion -manual -maple -marble -march -margin -marine -market -marriage -mask -mass -master -match -material -math -matrix -matter -maximum -maze -meadow -mean -measure -meat -mechanic -medal -media -melody -melt -member -memory -mention -menu -mercy -merge -merit -merry -mesh -message -metal -method -middle -midnight -milk -million -mimic -mind -minimum -minor -minute -miracle -mirror -misery -miss -mistake -mix -mixed -mixture -mobile -model -modify -mom -moment -monitor -monkey -monster -month -moon -moral -more -morning -mosquito -mother -motion -motor -mountain -mouse -move -movie -much -muffin -mule -multiply -muscle -museum -mushroom -music -must -mutual -myself -mystery -myth -naive -name -napkin -narrow -nasty -nation -nature -near -neck -need -negative -neglect -neither -nephew -nerve -nest -net -network -neutral -never -news -next -nice -night -noble -noise -nominee -noodle -normal -north -nose -notable -note -nothing -notice -novel -now -nuclear -number -nurse -nut -oak -obey -object -oblige -obscure -observe -obtain -obvious -occur -ocean -october -odor -off -offer -office -often -oil -okay -old -olive -olympic -omit -once -one -onion -online -only -open -opera -opinion -oppose -option -orange -orbit -orchard -order -ordinary -organ -orient -original -orphan -ostrich -other -outdoor -outer -output -outside -oval -oven -over -own -owner -oxygen -oyster -ozone -pact -paddle -page -pair -palace -palm -panda -panel -panic -panther -paper -parade -parent -park -parrot -party -pass -patch -path -patient -patrol -pattern -pause -pave -payment -peace -peanut -pear -peasant -pelican -pen -penalty -pencil -people -pepper -perfect -permit -person -pet -phone -photo -phrase -physical -piano -picnic -picture -piece -pig -pigeon -pill -pilot -pink -pioneer -pipe -pistol -pitch -pizza -place -planet -plastic -plate -play -please -pledge -pluck -plug -plunge -poem -poet -point -polar -pole -police -pond -pony -pool -popular -portion -position -possible -post -potato -pottery -poverty -powder -power -practice -praise -predict -prefer -prepare -present -pretty -prevent -price -pride -primary -print -priority -prison -private -prize -problem -process -produce -profit -program -project -promote -proof -property -prosper -protect -proud -provide -public -pudding -pull -pulp -pulse -pumpkin -punch -pupil -puppy -purchase -purity -purpose -purse -push -put -puzzle -pyramid -quality -quantum -quarter -question -quick -quit -quiz -quote -rabbit -raccoon -race -rack -radar -radio -rail -rain -raise -rally -ramp -ranch -random -range -rapid -rare -rate -rather -raven -raw -razor -ready -real -reason -rebel -rebuild -recall -receive -recipe -record -recycle -reduce -reflect -reform -refuse -region -regret -regular -reject -relax -release -relief -rely -remain -remember -remind -remove -render -renew -rent -reopen -repair -repeat -replace -report -require -rescue -resemble -resist -resource -response -result -retire -retreat -return -reunion -reveal -review -reward -rhythm -rib -ribbon -rice -rich -ride -ridge -rifle -right -rigid -ring -riot -ripple -risk -ritual -rival -river -road -roast -robot -robust -rocket -romance -roof -rookie -room -rose -rotate -rough -round -route -royal -rubber -rude -rug -rule -run -runway -rural -sad -saddle -sadness -safe -sail -salad -salmon -salon -salt -salute -same -sample -sand -satisfy -satoshi -sauce -sausage -save -say -scale -scan -scare -scatter -scene -scheme -school -science -scissors -scorpion -scout -scrap -screen -script -scrub -sea -search -season -seat -second -secret -section -security -seed -seek -segment -select -sell -seminar -senior -sense -sentence -series -service -session -settle -setup -seven -shadow -shaft -shallow -share -shed -shell -sheriff -shield -shift -shine -ship -shiver -shock -shoe -shoot -shop -short -shoulder -shove -shrimp -shrug -shuffle -shy -sibling -sick -side -siege -sight -sign -silent -silk -silly -silver -similar -simple -since -sing -siren -sister -situate -six -size -skate -sketch -ski -skill -skin -skirt -skull -slab -slam -sleep -slender -slice -slide -slight -slim -slogan -slot -slow -slush -small -smart -smile -smoke -smooth -snack -snake -snap -sniff -snow -soap -soccer -social -sock -soda -soft -solar -soldier -solid -solution -solve -someone -song -soon -sorry -sort -soul -sound -soup -source -south -space -spare -spatial -spawn -speak -special -speed -spell -spend -sphere -spice -spider -spike -spin -spirit -split -spoil -sponsor -spoon -sport -spot -spray -spread -spring -spy -square -squeeze -squirrel -stable -stadium -staff -stage -stairs -stamp -stand -start -state -stay -steak -steel -stem -step -stereo -stick -still -sting -stock -stomach -stone -stool -story -stove -strategy -street -strike -strong -struggle -student -stuff -stumble -style -subject -submit -subway -success -such -sudden -suffer -sugar -suggest -suit -summer -sun -sunny -sunset -super -supply -supreme -sure -surface -surge -surprise -surround -survey -suspect -sustain -swallow -swamp -swap -swarm -swear -sweet -swift -swim -swing -switch -sword -symbol -symptom -syrup -system -table -tackle -tag -tail -talent -talk -tank -tape -target -task -taste -tattoo -taxi -teach -team -tell -ten -tenant -tennis -tent -term -test -text -thank -that -theme -then -theory -there -they -thing -this -thought -three -thrive -throw -thumb -thunder -ticket -tide -tiger -tilt -timber -time -tiny -tip -tired -tissue -title -toast -tobacco -today -toddler -toe -together -toilet -token -tomato -tomorrow -tone -tongue -tonight -tool -tooth -top -topic -topple -torch -tornado -tortoise -toss -total -tourist -toward -tower -town -toy -track -trade -traffic -tragic -train -transfer -trap -trash -travel -tray -treat -tree -trend -trial -tribe -trick -trigger -trim -trip -trophy -trouble -truck -true -truly -trumpet -trust -truth -try -tube -tuition -tumble -tuna -tunnel -turkey -turn -turtle -twelve -twenty -twice -twin -twist -two -type -typical -ugly -umbrella -unable -unaware -uncle -uncover -under -undo -unfair -unfold -unhappy -uniform -unique -unit -universe -unknown -unlock -until -unusual -unveil -update -upgrade -uphold -upon -upper -upset -urban -urge -usage -use -used -useful -useless -usual -utility -vacant -vacuum -vague -valid -valley -valve -van -vanish -vapor -various -vast -vault -vehicle -velvet -vendor -venture -venue -verb -verify -version -very -vessel -veteran -viable -vibrant -vicious -victory -video -view -village -vintage -violin -virtual -virus -visa -visit -visual -vital -vivid -vocal -voice -void -volcano -volume -vote -voyage -wage -wagon -wait -walk -wall -walnut -want -warfare -warm -warrior -wash -wasp -waste -water -wave -way -wealth -weapon -wear -weasel -weather -web -wedding -weekend -weird -welcome -west -wet -whale -what -wheat -wheel -when -where -whip -whisper -wide -width -wife -wild -will -win -window -wine -wing -wink -winner -winter -wire -wisdom -wise -wish -witness -wolf -woman -wonder -wood -wool -word -work -world -worry -worth -wrap -wreck -wrestle -wrist -write -wrong -yard -year -yellow -you -young -youth -zebra -zero -zone -zoo diff --git a/src/lib/pybitcointools/main.py b/src/lib/pybitcointools/main.py deleted file mode 100644 index 8cf3a9f7..00000000 --- a/src/lib/pybitcointools/main.py +++ /dev/null @@ -1,581 +0,0 @@ -#!/usr/bin/python -from .py2specials import * -from .py3specials import * -import binascii -import hashlib -import re -import sys -import os -import base64 -import time -import random -import hmac -from .ripemd import * - -# Elliptic curve parameters (secp256k1) - -P = 2**256 - 2**32 - 977 -N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 -A = 0 -B = 7 -Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 -Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 -G = (Gx, Gy) - - -def change_curve(p, n, a, b, gx, gy): - global P, N, A, B, Gx, Gy, G - P, N, A, B, Gx, Gy = p, n, a, b, gx, gy - G = (Gx, Gy) - - -def getG(): - return G - -# Extended Euclidean Algorithm - - -def inv(a, n): - if a == 0: - return 0 - lm, hm = 1, 0 - low, high = a % n, n - while low > 1: - r = high//low - nm, new = hm-lm*r, high-low*r - lm, low, hm, high = nm, new, lm, low - return lm % n - - - -# JSON access (for pybtctool convenience) - - -def access(obj, prop): - if isinstance(obj, dict): - if prop in obj: - return obj[prop] - elif '.' in prop: - return obj[float(prop)] - else: - return obj[int(prop)] - else: - return obj[int(prop)] - - -def multiaccess(obj, prop): - return [access(o, prop) for o in obj] - - -def slice(obj, start=0, end=2**200): - return obj[int(start):int(end)] - - -def count(obj): - return len(obj) - -_sum = sum - - -def sum(obj): - return _sum(obj) - - -def isinf(p): - return p[0] == 0 and p[1] == 0 - - -def to_jacobian(p): - o = (p[0], p[1], 1) - return o - - -def jacobian_double(p): - if not p[1]: - return (0, 0, 0) - ysq = (p[1] ** 2) % P - S = (4 * p[0] * ysq) % P - M = (3 * p[0] ** 2 + A * p[2] ** 4) % P - nx = (M**2 - 2 * S) % P - ny = (M * (S - nx) - 8 * ysq ** 2) % P - nz = (2 * p[1] * p[2]) % P - return (nx, ny, nz) - - -def jacobian_add(p, q): - if not p[1]: - return q - if not q[1]: - return p - U1 = (p[0] * q[2] ** 2) % P - U2 = (q[0] * p[2] ** 2) % P - S1 = (p[1] * q[2] ** 3) % P - S2 = (q[1] * p[2] ** 3) % P - if U1 == U2: - if S1 != S2: - return (0, 0, 1) - return jacobian_double(p) - H = U2 - U1 - R = S2 - S1 - H2 = (H * H) % P - H3 = (H * H2) % P - U1H2 = (U1 * H2) % P - nx = (R ** 2 - H3 - 2 * U1H2) % P - ny = (R * (U1H2 - nx) - S1 * H3) % P - nz = (H * p[2] * q[2]) % P - return (nx, ny, nz) - - -def from_jacobian(p): - z = inv(p[2], P) - return ((p[0] * z**2) % P, (p[1] * z**3) % P) - - -def jacobian_multiply(a, n): - if a[1] == 0 or n == 0: - return (0, 0, 1) - if n == 1: - return a - if n < 0 or n >= N: - return jacobian_multiply(a, n % N) - if (n % 2) == 0: - return jacobian_double(jacobian_multiply(a, n//2)) - if (n % 2) == 1: - return jacobian_add(jacobian_double(jacobian_multiply(a, n//2)), a) - - -def fast_multiply(a, n): - return from_jacobian(jacobian_multiply(to_jacobian(a), n)) - - -def fast_add(a, b): - return from_jacobian(jacobian_add(to_jacobian(a), to_jacobian(b))) - -# Functions for handling pubkey and privkey formats - - -def get_pubkey_format(pub): - if is_python2: - two = '\x02' - three = '\x03' - four = '\x04' - else: - two = 2 - three = 3 - four = 4 - - if isinstance(pub, (tuple, list)): return 'decimal' - elif len(pub) == 65 and pub[0] == four: return 'bin' - elif len(pub) == 130 and pub[0:2] == '04': return 'hex' - elif len(pub) == 33 and pub[0] in [two, three]: return 'bin_compressed' - elif len(pub) == 66 and pub[0:2] in ['02', '03']: return 'hex_compressed' - elif len(pub) == 64: return 'bin_electrum' - elif len(pub) == 128: return 'hex_electrum' - else: raise Exception("Pubkey not in recognized format") - - -def encode_pubkey(pub, formt): - if not isinstance(pub, (tuple, list)): - pub = decode_pubkey(pub) - if formt == 'decimal': return pub - elif formt == 'bin': return b'\x04' + encode(pub[0], 256, 32) + encode(pub[1], 256, 32) - elif formt == 'bin_compressed': - return from_int_to_byte(2+(pub[1] % 2)) + encode(pub[0], 256, 32) - elif formt == 'hex': return '04' + encode(pub[0], 16, 64) + encode(pub[1], 16, 64) - elif formt == 'hex_compressed': - return '0'+str(2+(pub[1] % 2)) + encode(pub[0], 16, 64) - elif formt == 'bin_electrum': return encode(pub[0], 256, 32) + encode(pub[1], 256, 32) - elif formt == 'hex_electrum': return encode(pub[0], 16, 64) + encode(pub[1], 16, 64) - else: raise Exception("Invalid format!") - - -def decode_pubkey(pub, formt=None): - if not formt: formt = get_pubkey_format(pub) - if formt == 'decimal': return pub - elif formt == 'bin': return (decode(pub[1:33], 256), decode(pub[33:65], 256)) - elif formt == 'bin_compressed': - x = decode(pub[1:33], 256) - beta = pow(int(x*x*x+A*x+B), int((P+1)//4), int(P)) - y = (P-beta) if ((beta + from_byte_to_int(pub[0])) % 2) else beta - return (x, y) - elif formt == 'hex': return (decode(pub[2:66], 16), decode(pub[66:130], 16)) - elif formt == 'hex_compressed': - return decode_pubkey(safe_from_hex(pub), 'bin_compressed') - elif formt == 'bin_electrum': - return (decode(pub[:32], 256), decode(pub[32:64], 256)) - elif formt == 'hex_electrum': - return (decode(pub[:64], 16), decode(pub[64:128], 16)) - else: raise Exception("Invalid format!") - -def get_privkey_format(priv): - if isinstance(priv, int_types): return 'decimal' - elif len(priv) == 32: return 'bin' - elif len(priv) == 33: return 'bin_compressed' - elif len(priv) == 64: return 'hex' - elif len(priv) == 66: return 'hex_compressed' - else: - bin_p = b58check_to_bin(priv) - if len(bin_p) == 32: return 'wif' - elif len(bin_p) == 33: return 'wif_compressed' - else: raise Exception("WIF does not represent privkey") - -def encode_privkey(priv, formt, vbyte=0): - if not isinstance(priv, int_types): - return encode_privkey(decode_privkey(priv), formt, vbyte) - if formt == 'decimal': return priv - elif formt == 'bin': return encode(priv, 256, 32) - elif formt == 'bin_compressed': return encode(priv, 256, 32)+b'\x01' - elif formt == 'hex': return encode(priv, 16, 64) - elif formt == 'hex_compressed': return encode(priv, 16, 64)+'01' - elif formt == 'wif': - return bin_to_b58check(encode(priv, 256, 32), 128+int(vbyte)) - elif formt == 'wif_compressed': - return bin_to_b58check(encode(priv, 256, 32)+b'\x01', 128+int(vbyte)) - else: raise Exception("Invalid format!") - -def decode_privkey(priv,formt=None): - if not formt: formt = get_privkey_format(priv) - if formt == 'decimal': return priv - elif formt == 'bin': return decode(priv, 256) - elif formt == 'bin_compressed': return decode(priv[:32], 256) - elif formt == 'hex': return decode(priv, 16) - elif formt == 'hex_compressed': return decode(priv[:64], 16) - elif formt == 'wif': return decode(b58check_to_bin(priv),256) - elif formt == 'wif_compressed': - return decode(b58check_to_bin(priv)[:32],256) - else: raise Exception("WIF does not represent privkey") - -def add_pubkeys(p1, p2): - f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2) - return encode_pubkey(fast_add(decode_pubkey(p1, f1), decode_pubkey(p2, f2)), f1) - -def add_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - return encode_privkey((decode_privkey(p1, f1) + decode_privkey(p2, f2)) % N, f1) - -def mul_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - return encode_privkey((decode_privkey(p1, f1) * decode_privkey(p2, f2)) % N, f1) - -def multiply(pubkey, privkey): - f1, f2 = get_pubkey_format(pubkey), get_privkey_format(privkey) - pubkey, privkey = decode_pubkey(pubkey, f1), decode_privkey(privkey, f2) - # http://safecurves.cr.yp.to/twist.html - if not isinf(pubkey) and (pubkey[0]**3+B-pubkey[1]*pubkey[1]) % P != 0: - raise Exception("Point not on curve") - return encode_pubkey(fast_multiply(pubkey, privkey), f1) - - -def divide(pubkey, privkey): - factor = inv(decode_privkey(privkey), N) - return multiply(pubkey, factor) - - -def compress(pubkey): - f = get_pubkey_format(pubkey) - if 'compressed' in f: return pubkey - elif f == 'bin': return encode_pubkey(decode_pubkey(pubkey, f), 'bin_compressed') - elif f == 'hex' or f == 'decimal': - return encode_pubkey(decode_pubkey(pubkey, f), 'hex_compressed') - - -def decompress(pubkey): - f = get_pubkey_format(pubkey) - if 'compressed' not in f: return pubkey - elif f == 'bin_compressed': return encode_pubkey(decode_pubkey(pubkey, f), 'bin') - elif f == 'hex_compressed' or f == 'decimal': - return encode_pubkey(decode_pubkey(pubkey, f), 'hex') - - -def privkey_to_pubkey(privkey): - f = get_privkey_format(privkey) - privkey = decode_privkey(privkey, f) - if privkey >= N: - raise Exception("Invalid privkey") - if f in ['bin', 'bin_compressed', 'hex', 'hex_compressed', 'decimal']: - return encode_pubkey(fast_multiply(G, privkey), f) - else: - return encode_pubkey(fast_multiply(G, privkey), f.replace('wif', 'hex')) - -privtopub = privkey_to_pubkey - - -def privkey_to_address(priv, magicbyte=0): - return pubkey_to_address(privkey_to_pubkey(priv), magicbyte) -privtoaddr = privkey_to_address - - -def neg_pubkey(pubkey): - f = get_pubkey_format(pubkey) - pubkey = decode_pubkey(pubkey, f) - return encode_pubkey((pubkey[0], (P-pubkey[1]) % P), f) - - -def neg_privkey(privkey): - f = get_privkey_format(privkey) - privkey = decode_privkey(privkey, f) - return encode_privkey((N - privkey) % N, f) - -def subtract_pubkeys(p1, p2): - f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2) - k2 = decode_pubkey(p2, f2) - return encode_pubkey(fast_add(decode_pubkey(p1, f1), (k2[0], (P - k2[1]) % P)), f1) - - -def subtract_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - k2 = decode_privkey(p2, f2) - return encode_privkey((decode_privkey(p1, f1) - k2) % N, f1) - -# Hashes - - -def bin_hash160(string): - intermed = hashlib.sha256(string).digest() - digest = '' - try: - digest = hashlib.new('ripemd160', intermed).digest() - except: - digest = RIPEMD160(intermed).digest() - return digest - - -def hash160(string): - return safe_hexlify(bin_hash160(string)) - - -def bin_sha256(string): - binary_data = string if isinstance(string, bytes) else bytes(string, 'utf-8') - return hashlib.sha256(binary_data).digest() - -def sha256(string): - return bytes_to_hex_string(bin_sha256(string)) - - -def bin_ripemd160(string): - try: - digest = hashlib.new('ripemd160', string).digest() - except: - digest = RIPEMD160(string).digest() - return digest - - -def ripemd160(string): - return safe_hexlify(bin_ripemd160(string)) - - -def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - -def dbl_sha256(string): - return safe_hexlify(bin_dbl_sha256(string)) - - -def bin_slowsha(string): - string = from_string_to_bytes(string) - orig_input = string - for i in range(100000): - string = hashlib.sha256(string + orig_input).digest() - return string - - -def slowsha(string): - return safe_hexlify(bin_slowsha(string)) - - -def hash_to_int(x): - if len(x) in [40, 64]: - return decode(x, 16) - return decode(x, 256) - - -def num_to_var_int(x): - x = int(x) - if x < 253: return from_int_to_byte(x) - elif x < 65536: return from_int_to_byte(253)+encode(x, 256, 2)[::-1] - elif x < 4294967296: return from_int_to_byte(254) + encode(x, 256, 4)[::-1] - else: return from_int_to_byte(255) + encode(x, 256, 8)[::-1] - - -# WTF, Electrum? -def electrum_sig_hash(message): - padded = b"\x18Bitcoin Signed Message:\n" + num_to_var_int(len(message)) + from_string_to_bytes(message) - return bin_dbl_sha256(padded) - - -def random_key(): - # Gotta be secure after that java.SecureRandom fiasco... - entropy = random_string(32) \ - + str(random.randrange(2**256)) \ - + str(int(time.time() * 1000000)) - return sha256(entropy) - - -def random_electrum_seed(): - entropy = os.urandom(32) \ - + str(random.randrange(2**256)) \ - + str(int(time.time() * 1000000)) - return sha256(entropy)[:32] - -# Encodings - -def b58check_to_bin(inp): - leadingzbytes = len(re.match('^1*', inp).group(0)) - data = b'\x00' * leadingzbytes + changebase(inp, 58, 256) - assert bin_dbl_sha256(data[:-4])[:4] == data[-4:] - return data[1:-4] - - -def get_version_byte(inp): - leadingzbytes = len(re.match('^1*', inp).group(0)) - data = b'\x00' * leadingzbytes + changebase(inp, 58, 256) - assert bin_dbl_sha256(data[:-4])[:4] == data[-4:] - return ord(data[0]) - - -def hex_to_b58check(inp, magicbyte=0): - return bin_to_b58check(binascii.unhexlify(inp), magicbyte) - - -def b58check_to_hex(inp): - return safe_hexlify(b58check_to_bin(inp)) - - -def pubkey_to_address(pubkey, magicbyte=0): - if isinstance(pubkey, (list, tuple)): - pubkey = encode_pubkey(pubkey, 'bin') - if len(pubkey) in [66, 130]: - return bin_to_b58check( - bin_hash160(binascii.unhexlify(pubkey)), magicbyte) - return bin_to_b58check(bin_hash160(pubkey), magicbyte) - -pubtoaddr = pubkey_to_address - - -def is_privkey(priv): - try: - get_privkey_format(priv) - return True - except: - return False - -def is_pubkey(pubkey): - try: - get_pubkey_format(pubkey) - return True - except: - return False - -def is_address(addr): - ADDR_RE = re.compile("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$") - return bool(ADDR_RE.match(addr)) - - -# EDCSA - - -def encode_sig(v, r, s): - vb, rb, sb = from_int_to_byte(v), encode(r, 256), encode(s, 256) - - result = base64.b64encode(vb+b'\x00'*(32-len(rb))+rb+b'\x00'*(32-len(sb))+sb) - return result if is_python2 else str(result, 'utf-8') - - -def decode_sig(sig): - bytez = base64.b64decode(sig) - return from_byte_to_int(bytez[0]), decode(bytez[1:33], 256), decode(bytez[33:], 256) - -# https://tools.ietf.org/html/rfc6979#section-3.2 - - -def deterministic_generate_k(msghash, priv): - v = b'\x01' * 32 - k = b'\x00' * 32 - priv = encode_privkey(priv, 'bin') - msghash = encode(hash_to_int(msghash), 256, 32) - k = hmac.new(k, v+b'\x00'+priv+msghash, hashlib.sha256).digest() - v = hmac.new(k, v, hashlib.sha256).digest() - k = hmac.new(k, v+b'\x01'+priv+msghash, hashlib.sha256).digest() - v = hmac.new(k, v, hashlib.sha256).digest() - return decode(hmac.new(k, v, hashlib.sha256).digest(), 256) - - -def ecdsa_raw_sign(msghash, priv): - - z = hash_to_int(msghash) - k = deterministic_generate_k(msghash, priv) - - r, y = fast_multiply(G, k) - s = inv(k, N) * (z + r*decode_privkey(priv)) % N - - v, r, s = 27+((y % 2) ^ (0 if s * 2 < N else 1)), r, s if s * 2 < N else N - s - if 'compressed' in get_privkey_format(priv): - v += 4 - return v, r, s - - -def ecdsa_sign(msg, priv): - v, r, s = ecdsa_raw_sign(electrum_sig_hash(msg), priv) - sig = encode_sig(v, r, s) - assert ecdsa_verify(msg, sig, - privtopub(priv)), "Bad Sig!\t %s\nv = %d\n,r = %d\ns = %d" % (sig, v, r, s) - return sig - - -def ecdsa_raw_verify(msghash, vrs, pub): - v, r, s = vrs - if not (27 <= v <= 34): - return False - - w = inv(s, N) - z = hash_to_int(msghash) - - u1, u2 = z*w % N, r*w % N - x, y = fast_add(fast_multiply(G, u1), fast_multiply(decode_pubkey(pub), u2)) - return bool(r == x and (r % N) and (s % N)) - - -# For BitcoinCore, (msg = addr or msg = "") be default -def ecdsa_verify_addr(msg, sig, addr): - assert is_address(addr) - Q = ecdsa_recover(msg, sig) - magic = get_version_byte(addr) - return (addr == pubtoaddr(Q, int(magic))) or (addr == pubtoaddr(compress(Q), int(magic))) - - -def ecdsa_verify(msg, sig, pub): - if is_address(pub): - return ecdsa_verify_addr(msg, sig, pub) - return ecdsa_raw_verify(electrum_sig_hash(msg), decode_sig(sig), pub) - - -def ecdsa_raw_recover(msghash, vrs): - v, r, s = vrs - if not (27 <= v <= 34): - raise ValueError("%d must in range 27-31" % v) - x = r - xcubedaxb = (x*x*x+A*x+B) % P - beta = pow(xcubedaxb, (P+1)//4, P) - y = beta if v % 2 ^ beta % 2 else (P - beta) - # If xcubedaxb is not a quadratic residue, then r cannot be the x coord - # for a point on the curve, and so the sig is invalid - if (xcubedaxb - y*y) % P != 0 or not (r % N) or not (s % N): - return False - z = hash_to_int(msghash) - Gz = jacobian_multiply((Gx, Gy, 1), (N - z) % N) - XY = jacobian_multiply((x, y, 1), s) - Qr = jacobian_add(Gz, XY) - Q = jacobian_multiply(Qr, inv(r, N)) - Q = from_jacobian(Q) - - # if ecdsa_raw_verify(msghash, vrs, Q): - return Q - # return False - - -def ecdsa_recover(msg, sig): - v,r,s = decode_sig(sig) - Q = ecdsa_raw_recover(electrum_sig_hash(msg), (v,r,s)) - return encode_pubkey(Q, 'hex_compressed') if v >= 31 else encode_pubkey(Q, 'hex') diff --git a/src/lib/pybitcointools/mnemonic.py b/src/lib/pybitcointools/mnemonic.py deleted file mode 100644 index a9df3617..00000000 --- a/src/lib/pybitcointools/mnemonic.py +++ /dev/null @@ -1,127 +0,0 @@ -import hashlib -import os.path -import binascii -import random -from bisect import bisect_left - -wordlist_english=list(open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'english.txt'),'r')) - -def eint_to_bytes(entint,entbits): - a=hex(entint)[2:].rstrip('L').zfill(32) - print(a) - return binascii.unhexlify(a) - -def mnemonic_int_to_words(mint,mint_num_words,wordlist=wordlist_english): - backwords=[wordlist[(mint >> (11*x)) & 0x7FF].strip() for x in range(mint_num_words)] - return backwords[::-1] - -def entropy_cs(entbytes): - entropy_size=8*len(entbytes) - checksum_size=entropy_size//32 - hd=hashlib.sha256(entbytes).hexdigest() - csint=int(hd,16) >> (256-checksum_size) - return csint,checksum_size - -def entropy_to_words(entbytes,wordlist=wordlist_english): - if(len(entbytes) < 4 or len(entbytes) % 4 != 0): - raise ValueError("The size of the entropy must be a multiple of 4 bytes (multiple of 32 bits)") - entropy_size=8*len(entbytes) - csint,checksum_size = entropy_cs(entbytes) - entint=int(binascii.hexlify(entbytes),16) - mint=(entint << checksum_size) | csint - mint_num_words=(entropy_size+checksum_size)//11 - - return mnemonic_int_to_words(mint,mint_num_words,wordlist) - -def words_bisect(word,wordlist=wordlist_english): - lo=bisect_left(wordlist,word) - hi=len(wordlist)-bisect_left(wordlist[:lo:-1],word) - - return lo,hi - -def words_split(wordstr,wordlist=wordlist_english): - def popword(wordstr,wordlist): - for fwl in range(1,9): - w=wordstr[:fwl].strip() - lo,hi=words_bisect(w,wordlist) - if(hi-lo == 1): - return w,wordstr[fwl:].lstrip() - wordlist=wordlist[lo:hi] - raise Exception("Wordstr %s not found in list" %(w)) - - words=[] - tail=wordstr - while(len(tail)): - head,tail=popword(tail,wordlist) - words.append(head) - return words - -def words_to_mnemonic_int(words,wordlist=wordlist_english): - if(isinstance(words,str)): - words=words_split(words,wordlist) - return sum([wordlist.index(w) << (11*x) for x,w in enumerate(words[::-1])]) - -def words_verify(words,wordlist=wordlist_english): - if(isinstance(words,str)): - words=words_split(words,wordlist) - - mint = words_to_mnemonic_int(words,wordlist) - mint_bits=len(words)*11 - cs_bits=mint_bits//32 - entropy_bits=mint_bits-cs_bits - eint=mint >> cs_bits - csint=mint & ((1 << cs_bits)-1) - ebytes=_eint_to_bytes(eint,entropy_bits) - return csint == entropy_cs(ebytes) - -def mnemonic_to_seed(mnemonic_phrase,passphrase=b''): - try: - from hashlib import pbkdf2_hmac - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return pbkdf2_hmac(hash_name='sha512',password=password,salt=salt,iterations=iters) - except: - try: - from Crypto.Protocol.KDF import PBKDF2 - from Crypto.Hash import SHA512,HMAC - - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return PBKDF2(password=password,salt=salt,dkLen=64,count=iters,prf=lambda p,s: HMAC.new(p,s,SHA512).digest()) - except: - try: - - from pbkdf2 import PBKDF2 - import hmac - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return PBKDF2(password,salt, iterations=iters, macmodule=hmac, digestmodule=hashlib.sha512).read(64) - except: - raise RuntimeError("No implementation of pbkdf2 was found!") - - return pbkdf2_hmac_sha256(password=mnemonic_phrase,salt=b'mnemonic'+passphrase) - -def words_mine(prefix,entbits,satisfunction,wordlist=wordlist_english,randombits=random.getrandbits): - prefix_bits=len(prefix)*11 - mine_bits=entbits-prefix_bits - pint=words_to_mnemonic_int(prefix,wordlist) - pint<<=mine_bits - dint=randombits(mine_bits) - count=0 - while(not satisfunction(entropy_to_words(eint_to_bytes(pint+dint,entbits)))): - dint=randombits(mine_bits) - if((count & 0xFFFF) == 0): - print("Searched %f percent of the space" % (float(count)/float(1 << mine_bits))) - - return entropy_to_words(eint_to_bytes(pint+dint,entbits)) - -if __name__=="__main__": - import json - testvectors=json.load(open('vectors.json','r')) - passed=True - for v in testvectors['english']: - ebytes=binascii.unhexlify(v[0]) - w=' '.join(entropy_to_words(ebytes)) - seed=mnemonic_to_seed(w,passphrase='TREZOR') - passed = passed and w==v[1] - passed = passed and binascii.hexlify(seed)==v[2] - print("Tests %s." % ("Passed" if passed else "Failed")) - - diff --git a/src/lib/pybitcointools/py2specials.py b/src/lib/pybitcointools/py2specials.py deleted file mode 100644 index 337154f3..00000000 --- a/src/lib/pybitcointools/py2specials.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys, re -import binascii -import os -import hashlib - - -if sys.version_info.major == 2: - string_types = (str, unicode) - string_or_bytes_types = string_types - int_types = (int, float, long) - - # Base switching - code_strings = { - 2: '01', - 10: '0123456789', - 16: '0123456789abcdef', - 32: 'abcdefghijklmnopqrstuvwxyz234567', - 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', - 256: ''.join([chr(x) for x in range(256)]) - } - - def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - def lpad(msg, symbol, length): - if len(msg) >= length: - return msg - return symbol * (length - len(msg)) + msg - - def get_code_string(base): - if base in code_strings: - return code_strings[base] - else: - raise ValueError("Invalid base!") - - def changebase(string, frm, to, minlen=0): - if frm == to: - return lpad(string, get_code_string(frm)[0], minlen) - return encode(decode(string, frm), to, minlen) - - def bin_to_b58check(inp, magicbyte=0): - if magicbyte == 0: - inp = '\x00' + inp - while magicbyte > 0: - inp = chr(int(magicbyte % 256)) + inp - magicbyte //= 256 - leadingzbytes = len(re.match('^\x00*', inp).group(0)) - checksum = bin_dbl_sha256(inp)[:4] - return '1' * leadingzbytes + changebase(inp+checksum, 256, 58) - - def bytes_to_hex_string(b): - return b.encode('hex') - - def safe_from_hex(s): - return s.decode('hex') - - def from_int_representation_to_bytes(a): - return str(a) - - def from_int_to_byte(a): - return chr(a) - - def from_byte_to_int(a): - return ord(a) - - def from_bytes_to_string(s): - return s - - def from_string_to_bytes(a): - return a - - def safe_hexlify(a): - return binascii.hexlify(a) - - def encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = get_code_string(base) - result = "" - while val > 0: - result = code_string[val % base] + result - val //= base - return code_string[0] * max(minlen - len(result), 0) + result - - def decode(string, base): - base = int(base) - code_string = get_code_string(base) - result = 0 - if base == 16: - string = string.lower() - while len(string) > 0: - result *= base - result += code_string.find(string[0]) - string = string[1:] - return result - - def random_string(x): - return os.urandom(x) diff --git a/src/lib/pybitcointools/py3specials.py b/src/lib/pybitcointools/py3specials.py deleted file mode 100644 index 7593b9a6..00000000 --- a/src/lib/pybitcointools/py3specials.py +++ /dev/null @@ -1,123 +0,0 @@ -import sys, os -import binascii -import hashlib - - -if sys.version_info.major == 3: - string_types = (str) - string_or_bytes_types = (str, bytes) - int_types = (int, float) - # Base switching - code_strings = { - 2: '01', - 10: '0123456789', - 16: '0123456789abcdef', - 32: 'abcdefghijklmnopqrstuvwxyz234567', - 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', - 256: ''.join([chr(x) for x in range(256)]) - } - - def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - def lpad(msg, symbol, length): - if len(msg) >= length: - return msg - return symbol * (length - len(msg)) + msg - - def get_code_string(base): - if base in code_strings: - return code_strings[base] - else: - raise ValueError("Invalid base!") - - def changebase(string, frm, to, minlen=0): - if frm == to: - return lpad(string, get_code_string(frm)[0], minlen) - return encode(decode(string, frm), to, minlen) - - def bin_to_b58check(inp, magicbyte=0): - if magicbyte == 0: - inp = from_int_to_byte(0) + inp - while magicbyte > 0: - inp = from_int_to_byte(magicbyte % 256) + inp - magicbyte //= 256 - - leadingzbytes = 0 - for x in inp: - if x != 0: - break - leadingzbytes += 1 - - checksum = bin_dbl_sha256(inp)[:4] - return '1' * leadingzbytes + changebase(inp+checksum, 256, 58) - - def bytes_to_hex_string(b): - if isinstance(b, str): - return b - - return ''.join('{:02x}'.format(y) for y in b) - - def safe_from_hex(s): - return bytes.fromhex(s) - - def from_int_representation_to_bytes(a): - return bytes(str(a), 'utf-8') - - def from_int_to_byte(a): - return bytes([a]) - - def from_byte_to_int(a): - return a - - def from_string_to_bytes(a): - return a if isinstance(a, bytes) else bytes(a, 'utf-8') - - def safe_hexlify(a): - return str(binascii.hexlify(a), 'utf-8') - - def encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = get_code_string(base) - result_bytes = bytes() - while val > 0: - curcode = code_string[val % base] - result_bytes = bytes([ord(curcode)]) + result_bytes - val //= base - - pad_size = minlen - len(result_bytes) - - padding_element = b'\x00' if base == 256 else b'1' \ - if base == 58 else b'0' - if (pad_size > 0): - result_bytes = padding_element*pad_size + result_bytes - - result_string = ''.join([chr(y) for y in result_bytes]) - result = result_bytes if base == 256 else result_string - - return result - - def decode(string, base): - if base == 256 and isinstance(string, str): - string = bytes(bytearray.fromhex(string)) - base = int(base) - code_string = get_code_string(base) - result = 0 - if base == 256: - def extract(d, cs): - return d - else: - def extract(d, cs): - return cs.find(d if isinstance(d, str) else chr(d)) - - if base == 16: - string = string.lower() - while len(string) > 0: - result *= base - result += extract(string[0], code_string) - string = string[1:] - return result - - def random_string(x): - return str(os.urandom(x)) diff --git a/src/lib/pybitcointools/ripemd.py b/src/lib/pybitcointools/ripemd.py deleted file mode 100644 index 4b0c6045..00000000 --- a/src/lib/pybitcointools/ripemd.py +++ /dev/null @@ -1,414 +0,0 @@ -## ripemd.py - pure Python implementation of the RIPEMD-160 algorithm. -## Bjorn Edstrom 16 december 2007. -## -## Copyrights -## ========== -## -## This code is a derived from an implementation by Markus Friedl which is -## subject to the following license. This Python implementation is not -## subject to any other license. -## -##/* -## * Copyright (c) 2001 Markus Friedl. All rights reserved. -## * -## * Redistribution and use in source and binary forms, with or without -## * modification, are permitted provided that the following conditions -## * are met: -## * 1. Redistributions of source code must retain the above copyright -## * notice, this list of conditions and the following disclaimer. -## * 2. Redistributions in binary form must reproduce the above copyright -## * notice, this list of conditions and the following disclaimer in the -## * documentation and/or other materials provided with the distribution. -## * -## * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -## * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -## * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -## * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -## * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -## * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -## * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -## * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -## * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -## * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -## */ -##/* -## * Preneel, Bosselaers, Dobbertin, "The Cryptographic Hash Function RIPEMD-160", -## * RSA Laboratories, CryptoBytes, Volume 3, Number 2, Autumn 1997, -## * ftp://ftp.rsasecurity.com/pub/cryptobytes/crypto3n2.pdf -## */ - -try: - import psyco - psyco.full() -except ImportError: - pass - -import sys - -is_python2 = sys.version_info.major == 2 -#block_size = 1 -digest_size = 20 -digestsize = 20 - -try: - range = xrange -except: - pass - -class RIPEMD160: - """Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed.""" - - def __init__(self, arg=None): - self.ctx = RMDContext() - if arg: - self.update(arg) - self.dig = None - - def update(self, arg): - """update(arg)""" - RMD160Update(self.ctx, arg, len(arg)) - self.dig = None - - def digest(self): - """digest()""" - if self.dig: - return self.dig - ctx = self.ctx.copy() - self.dig = RMD160Final(self.ctx) - self.ctx = ctx - return self.dig - - def hexdigest(self): - """hexdigest()""" - dig = self.digest() - hex_digest = '' - for d in dig: - if (is_python2): - hex_digest += '%02x' % ord(d) - else: - hex_digest += '%02x' % d - return hex_digest - - def copy(self): - """copy()""" - import copy - return copy.deepcopy(self) - - - -def new(arg=None): - """Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed.""" - return RIPEMD160(arg) - - - -# -# Private. -# - -class RMDContext: - def __init__(self): - self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, - 0x10325476, 0xC3D2E1F0] # uint32 - self.count = 0 # uint64 - self.buffer = [0]*64 # uchar - def copy(self): - ctx = RMDContext() - ctx.state = self.state[:] - ctx.count = self.count - ctx.buffer = self.buffer[:] - return ctx - -K0 = 0x00000000 -K1 = 0x5A827999 -K2 = 0x6ED9EBA1 -K3 = 0x8F1BBCDC -K4 = 0xA953FD4E - -KK0 = 0x50A28BE6 -KK1 = 0x5C4DD124 -KK2 = 0x6D703EF3 -KK3 = 0x7A6D76E9 -KK4 = 0x00000000 - -def ROL(n, x): - return ((x << n) & 0xffffffff) | (x >> (32 - n)) - -def F0(x, y, z): - return x ^ y ^ z - -def F1(x, y, z): - return (x & y) | (((~x) % 0x100000000) & z) - -def F2(x, y, z): - return (x | ((~y) % 0x100000000)) ^ z - -def F3(x, y, z): - return (x & z) | (((~z) % 0x100000000) & y) - -def F4(x, y, z): - return x ^ (y | ((~z) % 0x100000000)) - -def R(a, b, c, d, e, Fj, Kj, sj, rj, X): - a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e - c = ROL(10, c) - return a % 0x100000000, c - -PADDING = [0x80] + [0]*63 - -import sys -import struct - -def RMD160Transform(state, block): #uint32 state[5], uchar block[64] - x = [0]*16 - if sys.byteorder == 'little': - if is_python2: - x = struct.unpack('<16L', ''.join([chr(x) for x in block[0:64]])) - else: - x = struct.unpack('<16L', bytes(block[0:64])) - else: - raise "Error!!" - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - #/* Round 1 */ - a, c = R(a, b, c, d, e, F0, K0, 11, 0, x); - e, b = R(e, a, b, c, d, F0, K0, 14, 1, x); - d, a = R(d, e, a, b, c, F0, K0, 15, 2, x); - c, e = R(c, d, e, a, b, F0, K0, 12, 3, x); - b, d = R(b, c, d, e, a, F0, K0, 5, 4, x); - a, c = R(a, b, c, d, e, F0, K0, 8, 5, x); - e, b = R(e, a, b, c, d, F0, K0, 7, 6, x); - d, a = R(d, e, a, b, c, F0, K0, 9, 7, x); - c, e = R(c, d, e, a, b, F0, K0, 11, 8, x); - b, d = R(b, c, d, e, a, F0, K0, 13, 9, x); - a, c = R(a, b, c, d, e, F0, K0, 14, 10, x); - e, b = R(e, a, b, c, d, F0, K0, 15, 11, x); - d, a = R(d, e, a, b, c, F0, K0, 6, 12, x); - c, e = R(c, d, e, a, b, F0, K0, 7, 13, x); - b, d = R(b, c, d, e, a, F0, K0, 9, 14, x); - a, c = R(a, b, c, d, e, F0, K0, 8, 15, x); #/* #15 */ - #/* Round 2 */ - e, b = R(e, a, b, c, d, F1, K1, 7, 7, x); - d, a = R(d, e, a, b, c, F1, K1, 6, 4, x); - c, e = R(c, d, e, a, b, F1, K1, 8, 13, x); - b, d = R(b, c, d, e, a, F1, K1, 13, 1, x); - a, c = R(a, b, c, d, e, F1, K1, 11, 10, x); - e, b = R(e, a, b, c, d, F1, K1, 9, 6, x); - d, a = R(d, e, a, b, c, F1, K1, 7, 15, x); - c, e = R(c, d, e, a, b, F1, K1, 15, 3, x); - b, d = R(b, c, d, e, a, F1, K1, 7, 12, x); - a, c = R(a, b, c, d, e, F1, K1, 12, 0, x); - e, b = R(e, a, b, c, d, F1, K1, 15, 9, x); - d, a = R(d, e, a, b, c, F1, K1, 9, 5, x); - c, e = R(c, d, e, a, b, F1, K1, 11, 2, x); - b, d = R(b, c, d, e, a, F1, K1, 7, 14, x); - a, c = R(a, b, c, d, e, F1, K1, 13, 11, x); - e, b = R(e, a, b, c, d, F1, K1, 12, 8, x); #/* #31 */ - #/* Round 3 */ - d, a = R(d, e, a, b, c, F2, K2, 11, 3, x); - c, e = R(c, d, e, a, b, F2, K2, 13, 10, x); - b, d = R(b, c, d, e, a, F2, K2, 6, 14, x); - a, c = R(a, b, c, d, e, F2, K2, 7, 4, x); - e, b = R(e, a, b, c, d, F2, K2, 14, 9, x); - d, a = R(d, e, a, b, c, F2, K2, 9, 15, x); - c, e = R(c, d, e, a, b, F2, K2, 13, 8, x); - b, d = R(b, c, d, e, a, F2, K2, 15, 1, x); - a, c = R(a, b, c, d, e, F2, K2, 14, 2, x); - e, b = R(e, a, b, c, d, F2, K2, 8, 7, x); - d, a = R(d, e, a, b, c, F2, K2, 13, 0, x); - c, e = R(c, d, e, a, b, F2, K2, 6, 6, x); - b, d = R(b, c, d, e, a, F2, K2, 5, 13, x); - a, c = R(a, b, c, d, e, F2, K2, 12, 11, x); - e, b = R(e, a, b, c, d, F2, K2, 7, 5, x); - d, a = R(d, e, a, b, c, F2, K2, 5, 12, x); #/* #47 */ - #/* Round 4 */ - c, e = R(c, d, e, a, b, F3, K3, 11, 1, x); - b, d = R(b, c, d, e, a, F3, K3, 12, 9, x); - a, c = R(a, b, c, d, e, F3, K3, 14, 11, x); - e, b = R(e, a, b, c, d, F3, K3, 15, 10, x); - d, a = R(d, e, a, b, c, F3, K3, 14, 0, x); - c, e = R(c, d, e, a, b, F3, K3, 15, 8, x); - b, d = R(b, c, d, e, a, F3, K3, 9, 12, x); - a, c = R(a, b, c, d, e, F3, K3, 8, 4, x); - e, b = R(e, a, b, c, d, F3, K3, 9, 13, x); - d, a = R(d, e, a, b, c, F3, K3, 14, 3, x); - c, e = R(c, d, e, a, b, F3, K3, 5, 7, x); - b, d = R(b, c, d, e, a, F3, K3, 6, 15, x); - a, c = R(a, b, c, d, e, F3, K3, 8, 14, x); - e, b = R(e, a, b, c, d, F3, K3, 6, 5, x); - d, a = R(d, e, a, b, c, F3, K3, 5, 6, x); - c, e = R(c, d, e, a, b, F3, K3, 12, 2, x); #/* #63 */ - #/* Round 5 */ - b, d = R(b, c, d, e, a, F4, K4, 9, 4, x); - a, c = R(a, b, c, d, e, F4, K4, 15, 0, x); - e, b = R(e, a, b, c, d, F4, K4, 5, 5, x); - d, a = R(d, e, a, b, c, F4, K4, 11, 9, x); - c, e = R(c, d, e, a, b, F4, K4, 6, 7, x); - b, d = R(b, c, d, e, a, F4, K4, 8, 12, x); - a, c = R(a, b, c, d, e, F4, K4, 13, 2, x); - e, b = R(e, a, b, c, d, F4, K4, 12, 10, x); - d, a = R(d, e, a, b, c, F4, K4, 5, 14, x); - c, e = R(c, d, e, a, b, F4, K4, 12, 1, x); - b, d = R(b, c, d, e, a, F4, K4, 13, 3, x); - a, c = R(a, b, c, d, e, F4, K4, 14, 8, x); - e, b = R(e, a, b, c, d, F4, K4, 11, 11, x); - d, a = R(d, e, a, b, c, F4, K4, 8, 6, x); - c, e = R(c, d, e, a, b, F4, K4, 5, 15, x); - b, d = R(b, c, d, e, a, F4, K4, 6, 13, x); #/* #79 */ - - aa = a; - bb = b; - cc = c; - dd = d; - ee = e; - - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - #/* Parallel round 1 */ - a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) - e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) - d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) - c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) - b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) - a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) - e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) - d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) - c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) - b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) - a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) - e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) - d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) - c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) - b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) - a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) #/* #15 */ - #/* Parallel round 2 */ - e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) - d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) - c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) - a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) - e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) - d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) - c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) - a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) - e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) - d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) - c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) - b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) - a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) - e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) #/* #31 */ - #/* Parallel round 3 */ - d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) - c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) - b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) - a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) - e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) - d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) - c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) - b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) - a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) - e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) - c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) - b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) - a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) - e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) #/* #47 */ - #/* Parallel round 4 */ - c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) - b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) - a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) - e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) - d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) - c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) - b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) - a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) - e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) - d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) - c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) - b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) - a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) - e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) - d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) - c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) #/* #63 */ - #/* Parallel round 5 */ - b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) - e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) - d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) - c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) - b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) - a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) - e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) - d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) - c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) - b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) - e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) - d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) - c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) - b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) #/* #79 */ - - t = (state[1] + cc + d) % 0x100000000; - state[1] = (state[2] + dd + e) % 0x100000000; - state[2] = (state[3] + ee + a) % 0x100000000; - state[3] = (state[4] + aa + b) % 0x100000000; - state[4] = (state[0] + bb + c) % 0x100000000; - state[0] = t % 0x100000000; - - pass - - -def RMD160Update(ctx, inp, inplen): - if type(inp) == str: - inp = [ord(i)&0xff for i in inp] - - have = int((ctx.count // 8) % 64) - inplen = int(inplen) - need = 64 - have - ctx.count += 8 * inplen - off = 0 - if inplen >= need: - if have: - for i in range(need): - ctx.buffer[have+i] = inp[i] - RMD160Transform(ctx.state, ctx.buffer) - off = need - have = 0 - while off + 64 <= inplen: - RMD160Transform(ctx.state, inp[off:]) #<--- - off += 64 - if off < inplen: - # memcpy(ctx->buffer + have, input+off, len-off); - for i in range(inplen - off): - ctx.buffer[have+i] = inp[off+i] - -def RMD160Final(ctx): - size = struct.pack(" 73: return False - if (sig[0] != 0x30): return False - if (sig[1] != len(sig)-3): return False - rlen = sig[3] - if (5+rlen >= len(sig)): return False - slen = sig[5+rlen] - if (rlen + slen + 7 != len(sig)): return False - if (sig[2] != 0x02): return False - if (rlen == 0): return False - if (sig[4] & 0x80): return False - if (rlen > 1 and (sig[4] == 0x00) and not (sig[5] & 0x80)): return False - if (sig[4+rlen] != 0x02): return False - if (slen == 0): return False - if (sig[rlen+6] & 0x80): return False - if (slen > 1 and (sig[6+rlen] == 0x00) and not (sig[7+rlen] & 0x80)): - return False - return True - -def txhash(tx, hashcode=None): - if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx): - tx = changebase(tx, 16, 256) - if hashcode: - return dbl_sha256(from_string_to_bytes(tx) + encode(int(hashcode), 256, 4)[::-1]) - else: - return safe_hexlify(bin_dbl_sha256(tx)[::-1]) - - -def bin_txhash(tx, hashcode=None): - return binascii.unhexlify(txhash(tx, hashcode)) - - -def ecdsa_tx_sign(tx, priv, hashcode=SIGHASH_ALL): - rawsig = ecdsa_raw_sign(bin_txhash(tx, hashcode), priv) - return der_encode_sig(*rawsig)+encode(hashcode, 16, 2) - - -def ecdsa_tx_verify(tx, sig, pub, hashcode=SIGHASH_ALL): - return ecdsa_raw_verify(bin_txhash(tx, hashcode), der_decode_sig(sig), pub) - - -def ecdsa_tx_recover(tx, sig, hashcode=SIGHASH_ALL): - z = bin_txhash(tx, hashcode) - _, r, s = der_decode_sig(sig) - left = ecdsa_raw_recover(z, (0, r, s)) - right = ecdsa_raw_recover(z, (1, r, s)) - return (encode_pubkey(left, 'hex'), encode_pubkey(right, 'hex')) - -# Scripts - - -def mk_pubkey_script(addr): - # Keep the auxiliary functions around for altcoins' sake - return '76a914' + b58check_to_hex(addr) + '88ac' - - -def mk_scripthash_script(addr): - return 'a914' + b58check_to_hex(addr) + '87' - -# Address representation to output script - - -def address_to_script(addr): - if addr[0] == '3' or addr[0] == '2': - return mk_scripthash_script(addr) - else: - return mk_pubkey_script(addr) - -# Output script to address representation - - -def script_to_address(script, vbyte=0): - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - if script[:3] == b'\x76\xa9\x14' and script[-2:] == b'\x88\xac' and len(script) == 25: - return bin_to_b58check(script[3:-2], vbyte) # pubkey hash addresses - else: - if vbyte in [111, 196]: - # Testnet - scripthash_byte = 196 - elif vbyte == 0: - # Mainnet - scripthash_byte = 5 - else: - scripthash_byte = vbyte - # BIP0016 scripthash addresses - return bin_to_b58check(script[2:-1], scripthash_byte) - - -def p2sh_scriptaddr(script, magicbyte=5): - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - return hex_to_b58check(hash160(script), magicbyte) -scriptaddr = p2sh_scriptaddr - - -def deserialize_script(script): - if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script): - return json_changebase(deserialize_script(binascii.unhexlify(script)), - lambda x: safe_hexlify(x)) - out, pos = [], 0 - while pos < len(script): - code = from_byte_to_int(script[pos]) - if code == 0: - out.append(None) - pos += 1 - elif code <= 75: - out.append(script[pos+1:pos+1+code]) - pos += 1 + code - elif code <= 78: - szsz = pow(2, code - 76) - sz = decode(script[pos+szsz: pos:-1], 256) - out.append(script[pos + 1 + szsz:pos + 1 + szsz + sz]) - pos += 1 + szsz + sz - elif code <= 96: - out.append(code - 80) - pos += 1 - else: - out.append(code) - pos += 1 - return out - - -def serialize_script_unit(unit): - if isinstance(unit, int): - if unit < 16: - return from_int_to_byte(unit + 80) - else: - return from_int_to_byte(unit) - elif unit is None: - return b'\x00' - else: - if len(unit) <= 75: - return from_int_to_byte(len(unit))+unit - elif len(unit) < 256: - return from_int_to_byte(76)+from_int_to_byte(len(unit))+unit - elif len(unit) < 65536: - return from_int_to_byte(77)+encode(len(unit), 256, 2)[::-1]+unit - else: - return from_int_to_byte(78)+encode(len(unit), 256, 4)[::-1]+unit - - -if is_python2: - def serialize_script(script): - if json_is_base(script, 16): - return binascii.hexlify(serialize_script(json_changebase(script, - lambda x: binascii.unhexlify(x)))) - return ''.join(map(serialize_script_unit, script)) -else: - def serialize_script(script): - if json_is_base(script, 16): - return safe_hexlify(serialize_script(json_changebase(script, - lambda x: binascii.unhexlify(x)))) - - result = bytes() - for b in map(serialize_script_unit, script): - result += b if isinstance(b, bytes) else bytes(b, 'utf-8') - return result - - -def mk_multisig_script(*args): # [pubs],k or pub1,pub2...pub[n],k - if isinstance(args[0], list): - pubs, k = args[0], int(args[1]) - else: - pubs = list(filter(lambda x: len(str(x)) >= 32, args)) - k = int(args[len(pubs)]) - return serialize_script([k]+pubs+[len(pubs)]+[0xae]) - -# Signing and verifying - - -def verify_tx_input(tx, i, script, sig, pub): - if re.match('^[0-9a-fA-F]*$', tx): - tx = binascii.unhexlify(tx) - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - if not re.match('^[0-9a-fA-F]*$', sig): - sig = safe_hexlify(sig) - hashcode = decode(sig[-2:], 16) - modtx = signature_form(tx, int(i), script, hashcode) - return ecdsa_tx_verify(modtx, sig, pub, hashcode) - - -def sign(tx, i, priv, hashcode=SIGHASH_ALL): - i = int(i) - if (not is_python2 and isinstance(re, bytes)) or not re.match('^[0-9a-fA-F]*$', tx): - return binascii.unhexlify(sign(safe_hexlify(tx), i, priv)) - if len(priv) <= 33: - priv = safe_hexlify(priv) - pub = privkey_to_pubkey(priv) - address = pubkey_to_address(pub) - signing_tx = signature_form(tx, i, mk_pubkey_script(address), hashcode) - sig = ecdsa_tx_sign(signing_tx, priv, hashcode) - txobj = deserialize(tx) - txobj["ins"][i]["script"] = serialize_script([sig, pub]) - return serialize(txobj) - - -def signall(tx, priv): - # if priv is a dictionary, assume format is - # { 'txinhash:txinidx' : privkey } - if isinstance(priv, dict): - for e, i in enumerate(deserialize(tx)["ins"]): - k = priv["%s:%d" % (i["outpoint"]["hash"], i["outpoint"]["index"])] - tx = sign(tx, e, k) - else: - for i in range(len(deserialize(tx)["ins"])): - tx = sign(tx, i, priv) - return tx - - -def multisign(tx, i, script, pk, hashcode=SIGHASH_ALL): - if re.match('^[0-9a-fA-F]*$', tx): - tx = binascii.unhexlify(tx) - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - modtx = signature_form(tx, i, script, hashcode) - return ecdsa_tx_sign(modtx, pk, hashcode) - - -def apply_multisignatures(*args): - # tx,i,script,sigs OR tx,i,script,sig1,sig2...,sig[n] - tx, i, script = args[0], int(args[1]), args[2] - sigs = args[3] if isinstance(args[3], list) else list(args[3:]) - - if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - sigs = [binascii.unhexlify(x) if x[:2] == '30' else x for x in sigs] - if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx): - return safe_hexlify(apply_multisignatures(binascii.unhexlify(tx), i, script, sigs)) - - # Not pushing empty elements on the top of the stack if passing no - # script (in case of bare multisig inputs there is no script) - script_blob = [] if script.__len__() == 0 else [script] - - txobj = deserialize(tx) - txobj["ins"][i]["script"] = serialize_script([None]+sigs+script_blob) - return serialize(txobj) - - -def is_inp(arg): - return len(arg) > 64 or "output" in arg or "outpoint" in arg - - -def mktx(*args): - # [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ... - ins, outs = [], [] - for arg in args: - if isinstance(arg, list): - for a in arg: (ins if is_inp(a) else outs).append(a) - else: - (ins if is_inp(arg) else outs).append(arg) - - txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []} - for i in ins: - if isinstance(i, dict) and "outpoint" in i: - txobj["ins"].append(i) - else: - if isinstance(i, dict) and "output" in i: - i = i["output"] - txobj["ins"].append({ - "outpoint": {"hash": i[:64], "index": int(i[65:])}, - "script": "", - "sequence": 4294967295 - }) - for o in outs: - if isinstance(o, string_or_bytes_types): - addr = o[:o.find(':')] - val = int(o[o.find(':')+1:]) - o = {} - if re.match('^[0-9a-fA-F]*$', addr): - o["script"] = addr - else: - o["address"] = addr - o["value"] = val - - outobj = {} - if "address" in o: - outobj["script"] = address_to_script(o["address"]) - elif "script" in o: - outobj["script"] = o["script"] - else: - raise Exception("Could not find 'address' or 'script' in output.") - outobj["value"] = o["value"] - txobj["outs"].append(outobj) - - return serialize(txobj) - - -def select(unspent, value): - value = int(value) - high = [u for u in unspent if u["value"] >= value] - high.sort(key=lambda u: u["value"]) - low = [u for u in unspent if u["value"] < value] - low.sort(key=lambda u: -u["value"]) - if len(high): - return [high[0]] - i, tv = 0, 0 - while tv < value and i < len(low): - tv += low[i]["value"] - i += 1 - if tv < value: - raise Exception("Not enough funds") - return low[:i] - -# Only takes inputs of the form { "output": blah, "value": foo } - - -def mksend(*args): - argz, change, fee = args[:-2], args[-2], int(args[-1]) - ins, outs = [], [] - for arg in argz: - if isinstance(arg, list): - for a in arg: - (ins if is_inp(a) else outs).append(a) - else: - (ins if is_inp(arg) else outs).append(arg) - - isum = sum([i["value"] for i in ins]) - osum, outputs2 = 0, [] - for o in outs: - if isinstance(o, string_types): - o2 = { - "address": o[:o.find(':')], - "value": int(o[o.find(':')+1:]) - } - else: - o2 = o - outputs2.append(o2) - osum += o2["value"] - - if isum < osum+fee: - raise Exception("Not enough money") - elif isum > osum+fee+5430: - outputs2 += [{"address": change, "value": isum-osum-fee}] - - return mktx(ins, outputs2) diff --git a/src/lib/pyelliptic/LICENSE b/src/lib/pyelliptic/LICENSE deleted file mode 100644 index 94a9ed02..00000000 --- a/src/lib/pyelliptic/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/src/lib/pyelliptic/README.md b/src/lib/pyelliptic/README.md deleted file mode 100644 index 3acf819c..00000000 --- a/src/lib/pyelliptic/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# PyElliptic - -PyElliptic is a high level wrapper for the cryptographic library : OpenSSL. -Under the GNU General Public License - -Python3 compatible. For GNU/Linux and Windows. -Require OpenSSL - -## Version - -The [upstream pyelliptic](https://github.com/yann2192/pyelliptic) has been -deprecated by the author at 1.5.8 and ECC API has been removed. - -This version is a fork of the pyelliptic extracted from the [BitMessage source -tree](https://github.com/Bitmessage/PyBitmessage), and does contain the ECC -API. To minimize confusion but to avoid renaming the module, major version has -been bumped. - -BitMessage is actively maintained, and this fork of pyelliptic will track and -incorporate any changes to pyelliptic from BitMessage. Ideally, in the future, -BitMessage would import this module as a dependency instead of maintaining a -copy of the source in its repository. - -The BitMessage fork forked from v1.3 of upstream pyelliptic. The commits in -this repository are the commits extracted from the BitMessage repository and -applied to pyelliptic v1.3 upstream repository (i.e. to the base of the fork), -so history with athorship is preserved. - -Some of the changes in upstream pyelliptic between 1.3 and 1.5.8 came from -BitMessage, those changes are present in this fork. Other changes do not exist -in this fork (they may be added in the future). - -Also, a few minor changes exist in this fork but is not (yet) present in -BitMessage source. See: - - git log 1.3-PyBitmessage-37489cf7feff8d5047f24baa8f6d27f353a6d6ac..HEAD - -## Features - -### Asymmetric cryptography using Elliptic Curve Cryptography (ECC) - -* Key agreement : ECDH -* Digital signatures : ECDSA -* Hybrid encryption : ECIES (like RSA) - -### Symmetric cryptography - -* AES-128 (CBC, OFB, CFB, CTR) -* AES-256 (CBC, OFB, CFB, CTR) -* Blowfish (CFB and CBC) -* RC4 - -### Other - -* CSPRNG -* HMAC (using SHA512) -* PBKDF2 (SHA256 and SHA512) - -## Example - -```python -#!/usr/bin/python - -import pyelliptic - -# Symmetric encryption -iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') -ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') - -ciphertext = ctx.update('test1') -ciphertext += ctx.update('test2') -ciphertext += ctx.final() - -ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') -print ctx2.ciphering(ciphertext) - -# Asymmetric encryption -alice = pyelliptic.ECC() # default curve: sect283r1 -bob = pyelliptic.ECC(curve='sect571r1') - -ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) -print bob.decrypt(ciphertext) - -signature = bob.sign("Hello Alice") -# alice's job : -print pyelliptic.ECC(pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") - -# ERROR !!! -try: - key = alice.get_ecdh_key(bob.get_pubkey()) -except: print("For ECDH key agreement, the keys must be defined on the same curve !") - -alice = pyelliptic.ECC(curve='sect571r1') -print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') -print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') -``` diff --git a/src/lib/pyelliptic/__init__.py b/src/lib/pyelliptic/__init__.py deleted file mode 100644 index 761d08af..00000000 --- a/src/lib/pyelliptic/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (C) 2010 -# Author: Yann GUIBET -# Contact: - -__version__ = '1.3' - -__all__ = [ - 'OpenSSL', - 'ECC', - 'Cipher', - 'hmac_sha256', - 'hmac_sha512', - 'pbkdf2' -] - -from .openssl import OpenSSL -from .ecc import ECC -from .cipher import Cipher -from .hash import hmac_sha256, hmac_sha512, pbkdf2 diff --git a/src/lib/pyelliptic/arithmetic.py b/src/lib/pyelliptic/arithmetic.py deleted file mode 100644 index 95c85b93..00000000 --- a/src/lib/pyelliptic/arithmetic.py +++ /dev/null @@ -1,144 +0,0 @@ -# pylint: disable=missing-docstring,too-many-function-args - -import hashlib -import re - -P = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 -A = 0 -Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 -Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 -G = (Gx, Gy) - - -def inv(a, n): - lm, hm = 1, 0 - low, high = a % n, n - while low > 1: - r = high / low - nm, new = hm - lm * r, high - low * r - lm, low, hm, high = nm, new, lm, low - return lm % n - - -def get_code_string(base): - if base == 2: - return '01' - elif base == 10: - return '0123456789' - elif base == 16: - return "0123456789abcdef" - elif base == 58: - return "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - elif base == 256: - return ''.join([chr(x) for x in range(256)]) - else: - raise ValueError("Invalid base!") - - -def encode(val, base, minlen=0): - code_string = get_code_string(base) - result = "" - while val > 0: - result = code_string[val % base] + result - val /= base - if len(result) < minlen: - result = code_string[0] * (minlen - len(result)) + result - return result - - -def decode(string, base): - code_string = get_code_string(base) - result = 0 - if base == 16: - string = string.lower() - while string: - result *= base - result += code_string.find(string[0]) - string = string[1:] - return result - - -def changebase(string, frm, to, minlen=0): - return encode(decode(string, frm), to, minlen) - - -def base10_add(a, b): - if a is None: - return b[0], b[1] - if b is None: - return a[0], a[1] - if a[0] == b[0]: - if a[1] == b[1]: - return base10_double(a[0], a[1]) - return None - m = ((b[1] - a[1]) * inv(b[0] - a[0], P)) % P - x = (m * m - a[0] - b[0]) % P - y = (m * (a[0] - x) - a[1]) % P - return (x, y) - - -def base10_double(a): - if a is None: - return None - m = ((3 * a[0] * a[0] + A) * inv(2 * a[1], P)) % P - x = (m * m - 2 * a[0]) % P - y = (m * (a[0] - x) - a[1]) % P - return (x, y) - - -def base10_multiply(a, n): - if n == 0: - return G - if n == 1: - return a - if (n % 2) == 0: - return base10_double(base10_multiply(a, n / 2)) - if (n % 2) == 1: - return base10_add(base10_double(base10_multiply(a, n / 2)), a) - return None - - -def hex_to_point(h): - return (decode(h[2:66], 16), decode(h[66:], 16)) - - -def point_to_hex(p): - return '04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) - - -def multiply(privkey, pubkey): - return point_to_hex(base10_multiply(hex_to_point(pubkey), decode(privkey, 16))) - - -def privtopub(privkey): - return point_to_hex(base10_multiply(G, decode(privkey, 16))) - - -def add(p1, p2): - if len(p1) == 32: - return encode(decode(p1, 16) + decode(p2, 16) % P, 16, 32) - return point_to_hex(base10_add(hex_to_point(p1), hex_to_point(p2))) - - -def hash_160(string): - intermed = hashlib.sha256(string).digest() - ripemd160 = hashlib.new('ripemd160') - ripemd160.update(intermed) - return ripemd160.digest() - - -def dbl_sha256(string): - return hashlib.sha256(hashlib.sha256(string).digest()).digest() - - -def bin_to_b58check(inp): - inp_fmtd = '\x00' + inp - leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0)) - checksum = dbl_sha256(inp_fmtd)[:4] - return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58) - -# Convert a public key (in hex) to a Bitcoin address - - -def pubkey_to_address(pubkey): - return bin_to_b58check(hash_160(changebase(pubkey, 16, 256))) diff --git a/src/lib/pyelliptic/cipher.py b/src/lib/pyelliptic/cipher.py deleted file mode 100644 index 54ae7a09..00000000 --- a/src/lib/pyelliptic/cipher.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from .openssl import OpenSSL - - -class Cipher: - """ - Symmetric encryption - - import pyelliptic - iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') - ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') - ciphertext = ctx.update('test1') - ciphertext += ctx.update('test2') - ciphertext += ctx.final() - - ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') - print ctx2.ciphering(ciphertext) - """ - def __init__(self, key, iv, do, ciphername='aes-256-cbc'): - """ - do == 1 => Encrypt; do == 0 => Decrypt - """ - self.cipher = OpenSSL.get_cipher(ciphername) - self.ctx = OpenSSL.EVP_CIPHER_CTX_new() - if do == 1 or do == 0: - k = OpenSSL.malloc(key, len(key)) - IV = OpenSSL.malloc(iv, len(iv)) - OpenSSL.EVP_CipherInit_ex( - self.ctx, self.cipher.get_pointer(), 0, k, IV, do) - else: - raise Exception("RTFM ...") - - @staticmethod - def get_all_cipher(): - """ - static method, returns all ciphers available - """ - return OpenSSL.cipher_algo.keys() - - @staticmethod - def get_blocksize(ciphername): - cipher = OpenSSL.get_cipher(ciphername) - return cipher.get_blocksize() - - @staticmethod - def gen_IV(ciphername): - cipher = OpenSSL.get_cipher(ciphername) - return OpenSSL.rand(cipher.get_blocksize()) - - def update(self, input): - i = OpenSSL.c_int(0) - buffer = OpenSSL.malloc(b"", len(input) + self.cipher.get_blocksize()) - inp = OpenSSL.malloc(input, len(input)) - if OpenSSL.EVP_CipherUpdate(self.ctx, OpenSSL.byref(buffer), - OpenSSL.byref(i), inp, len(input)) == 0: - raise Exception("[OpenSSL] EVP_CipherUpdate FAIL ...") - return buffer.raw[0:i.value] - - def final(self): - i = OpenSSL.c_int(0) - buffer = OpenSSL.malloc(b"", self.cipher.get_blocksize()) - if (OpenSSL.EVP_CipherFinal_ex(self.ctx, OpenSSL.byref(buffer), - OpenSSL.byref(i))) == 0: - raise Exception("[OpenSSL] EVP_CipherFinal_ex FAIL ...") - return buffer.raw[0:i.value] - - def ciphering(self, input): - """ - Do update and final in one method - """ - buff = self.update(input) - return buff + self.final() - - def __del__(self): - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_CIPHER_CTX_reset(self.ctx) - else: - OpenSSL.EVP_CIPHER_CTX_cleanup(self.ctx) - OpenSSL.EVP_CIPHER_CTX_free(self.ctx) diff --git a/src/lib/pyelliptic/ecc.py b/src/lib/pyelliptic/ecc.py deleted file mode 100644 index a45f8d78..00000000 --- a/src/lib/pyelliptic/ecc.py +++ /dev/null @@ -1,505 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -pyelliptic/ecc.py -===================== -""" -# pylint: disable=protected-access - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from hashlib import sha512 -from struct import pack, unpack - -from .cipher import Cipher -from .hash import equals, hmac_sha256 -from .openssl import OpenSSL - - -class ECC(object): - """ - Asymmetric encryption with Elliptic Curve Cryptography (ECC) - ECDH, ECDSA and ECIES - - >>> import pyelliptic - - >>> alice = pyelliptic.ECC() # default curve: sect283r1 - >>> bob = pyelliptic.ECC(curve='sect571r1') - - >>> ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) - >>> print bob.decrypt(ciphertext) - - >>> signature = bob.sign("Hello Alice") - >>> # alice's job : - >>> print pyelliptic.ECC( - >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") - - >>> # ERROR !!! - >>> try: - >>> key = alice.get_ecdh_key(bob.get_pubkey()) - >>> except: - >>> print("For ECDH key agreement, the keys must be defined on the same curve !") - - >>> alice = pyelliptic.ECC(curve='sect571r1') - >>> print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') - >>> print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') - - """ - - def __init__( - self, - pubkey=None, - privkey=None, - pubkey_x=None, - pubkey_y=None, - raw_privkey=None, - curve='sect283r1', - ): # pylint: disable=too-many-arguments - """ - For a normal and High level use, specifie pubkey, - privkey (if you need) and the curve - """ - if isinstance(curve, str): - self.curve = OpenSSL.get_curve(curve) - else: - self.curve = curve - - if pubkey_x is not None and pubkey_y is not None: - self._set_keys(pubkey_x, pubkey_y, raw_privkey) - elif pubkey is not None: - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if privkey is not None: - curve2, raw_privkey, _ = ECC._decode_privkey(privkey) - if curve != curve2: - raise Exception("Bad ECC keys ...") - self.curve = curve - self._set_keys(pubkey_x, pubkey_y, raw_privkey) - else: - self.privkey, self.pubkey_x, self.pubkey_y = self._generate() - - def _set_keys(self, pubkey_x, pubkey_y, privkey): - if self.raw_check_key(privkey, pubkey_x, pubkey_y) < 0: - self.pubkey_x = None - self.pubkey_y = None - self.privkey = None - raise Exception("Bad ECC keys ...") - else: - self.pubkey_x = pubkey_x - self.pubkey_y = pubkey_y - self.privkey = privkey - - @staticmethod - def get_curves(): - """ - static method, returns the list of all the curves available - """ - return OpenSSL.curves.keys() - - def get_curve(self): - """Encryption object from curve name""" - return OpenSSL.get_curve_by_id(self.curve) - - def get_curve_id(self): - """Currently used curve""" - return self.curve - - def get_pubkey(self): - """ - High level function which returns : - curve(2) + len_of_pubkeyX(2) + pubkeyX + len_of_pubkeyY + pubkeyY - """ - return b''.join(( - pack('!H', self.curve), - pack('!H', len(self.pubkey_x)), - self.pubkey_x, - pack('!H', len(self.pubkey_y)), - self.pubkey_y, - )) - - def get_privkey(self): - """ - High level function which returns - curve(2) + len_of_privkey(2) + privkey - """ - return b''.join(( - pack('!H', self.curve), - pack('!H', len(self.privkey)), - self.privkey, - )) - - @staticmethod - def _decode_pubkey(pubkey): - i = 0 - curve = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - tmplen = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - pubkey_x = pubkey[i:i + tmplen] - i += tmplen - tmplen = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - pubkey_y = pubkey[i:i + tmplen] - i += tmplen - return curve, pubkey_x, pubkey_y, i - - @staticmethod - def _decode_privkey(privkey): - i = 0 - curve = unpack('!H', privkey[i:i + 2])[0] - i += 2 - tmplen = unpack('!H', privkey[i:i + 2])[0] - i += 2 - privkey = privkey[i:i + tmplen] - i += tmplen - return curve, privkey, i - - def _generate(self): - try: - pub_key_x = OpenSSL.BN_new() - pub_key_y = OpenSSL.BN_new() - - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - if (OpenSSL.EC_KEY_generate_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_generate_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - priv_key = OpenSSL.EC_KEY_get0_private_key(key) - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_KEY_get0_public_key(key) - - if OpenSSL.EC_POINT_get_affine_coordinates_GFp( - group, pub_key, pub_key_x, pub_key_y, 0) == 0: - raise Exception("[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ...") - - privkey = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(priv_key)) - pubkeyx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_x)) - pubkeyy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_y)) - OpenSSL.BN_bn2bin(priv_key, privkey) - privkey = privkey.raw - OpenSSL.BN_bn2bin(pub_key_x, pubkeyx) - pubkeyx = pubkeyx.raw - OpenSSL.BN_bn2bin(pub_key_y, pubkeyy) - pubkeyy = pubkeyy.raw - self.raw_check_key(privkey, pubkeyx, pubkeyy) - - return privkey, pubkeyx, pubkeyy - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - - def get_ecdh_key(self, pubkey): - """ - High level function. Compute public key with the local private key - and returns a 512bits shared key - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if curve != self.curve: - raise Exception("ECC keys must be from the same curve !") - return sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - - def raw_get_ecdh_key(self, pubkey_x, pubkey_y): - """ECDH key as binary data""" - try: - ecdh_keybuffer = OpenSSL.malloc(0, 32) - - other_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if other_key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) - other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) - - other_group = OpenSSL.EC_KEY_get0_group(other_key) - other_pub_key = OpenSSL.EC_POINT_new(other_group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group, - other_pub_key, - other_pub_key_x, - other_pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(other_key, other_pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(other_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - - own_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if own_key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - own_priv_key = OpenSSL.BN_bin2bn( - self.privkey, len(self.privkey), 0) - - if (OpenSSL.EC_KEY_set_private_key(own_key, own_priv_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") - - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EC_KEY_set_method(own_key, OpenSSL.EC_KEY_OpenSSL()) - else: - OpenSSL.ECDH_set_method(own_key, OpenSSL.ECDH_OpenSSL()) - ecdh_keylen = OpenSSL.ECDH_compute_key( - ecdh_keybuffer, 32, other_pub_key, own_key, 0) - - if ecdh_keylen != 32: - raise Exception("[OpenSSL] ECDH keylen FAIL ...") - - return ecdh_keybuffer.raw - - finally: - OpenSSL.EC_KEY_free(other_key) - OpenSSL.BN_free(other_pub_key_x) - OpenSSL.BN_free(other_pub_key_y) - OpenSSL.EC_POINT_free(other_pub_key) - OpenSSL.EC_KEY_free(own_key) - OpenSSL.BN_free(own_priv_key) - - def check_key(self, privkey, pubkey): - """ - Check the public key and the private key. - The private key is optional (replace by None) - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if privkey is None: - raw_privkey = None - curve2 = curve - else: - curve2, raw_privkey, _ = ECC._decode_privkey(privkey) - if curve != curve2: - raise Exception("Bad public and private key") - return self.raw_check_key(raw_privkey, pubkey_x, pubkey_y, curve) - - def raw_check_key(self, privkey, pubkey_x, pubkey_y, curve=None): - """Check key validity, key is supplied as binary data""" - # pylint: disable=too-many-branches - if curve is None: - curve = self.curve - elif isinstance(curve, str): - curve = OpenSSL.get_curve(curve) - else: - curve = curve - try: - key = OpenSSL.EC_KEY_new_by_curve_name(curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - if privkey is not None: - priv_key = OpenSSL.BN_bin2bn(privkey, len(privkey), 0) - pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) - - if privkey is not None: - if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: - raise Exception( - "[OpenSSL] EC_KEY_set_private_key FAIL ...") - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - return 0 - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.EC_POINT_free(pub_key) - if privkey is not None: - OpenSSL.BN_free(priv_key) - - def sign(self, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): - """ - Sign the input with ECDSA method and returns the signature - """ - # pylint: disable=too-many-branches,too-many-locals - try: - size = len(inputb) - buff = OpenSSL.malloc(inputb, size) - digest = OpenSSL.malloc(0, 64) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - md_ctx = OpenSSL.EVP_MD_CTX_new() - else: - md_ctx = OpenSSL.EVP_MD_CTX_create() - dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) - siglen = OpenSSL.pointer(OpenSSL.c_int(0)) - sig = OpenSSL.malloc(0, 151) - - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - priv_key = OpenSSL.BN_bin2bn(self.privkey, len(self.privkey), 0) - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) - - if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_new(md_ctx) - else: - OpenSSL.EVP_MD_CTX_init(md_ctx) - OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - - if (OpenSSL.EVP_DigestUpdate(md_ctx, buff, size)) == 0: - raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") - OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) - OpenSSL.ECDSA_sign(0, digest, dgst_len.contents, sig, siglen, key) - if (OpenSSL.ECDSA_verify(0, digest, dgst_len.contents, sig, - siglen.contents, key)) != 1: - raise Exception("[OpenSSL] ECDSA_verify FAIL ...") - - return sig.raw[:siglen.contents.value] - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.BN_free(priv_key) - OpenSSL.EC_POINT_free(pub_key) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_free(md_ctx) - else: - OpenSSL.EVP_MD_CTX_destroy(md_ctx) - - def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): - """ - Verify the signature with the input and the local public key. - Returns a boolean - """ - # pylint: disable=too-many-branches - try: - bsig = OpenSSL.malloc(sig, len(sig)) - binputb = OpenSSL.malloc(inputb, len(inputb)) - digest = OpenSSL.malloc(0, 64) - dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - md_ctx = OpenSSL.EVP_MD_CTX_new() - else: - md_ctx = OpenSSL.EVP_MD_CTX_create() - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_new(md_ctx) - else: - OpenSSL.EVP_MD_CTX_init(md_ctx) - OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - if (OpenSSL.EVP_DigestUpdate(md_ctx, binputb, len(inputb))) == 0: - raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") - - OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) - ret = OpenSSL.ECDSA_verify( - 0, digest, dgst_len.contents, bsig, len(sig), key) - - if ret == -1: - return False # Fail to Check - if ret == 0: - return False # Bad signature ! - return True # Good - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.EC_POINT_free(pub_key) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_free(md_ctx) - else: - OpenSSL.EVP_MD_CTX_destroy(md_ctx) - - @staticmethod - def encrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): - """ - Encrypt data with ECIES method using the public key of the recipient. - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - return ECC.raw_encrypt(data, pubkey_x, pubkey_y, curve=curve, - ephemcurve=ephemcurve, ciphername=ciphername) - - @staticmethod - def raw_encrypt( - data, - pubkey_x, - pubkey_y, - curve='sect283r1', - ephemcurve=None, - ciphername='aes-256-cbc', - ): # pylint: disable=too-many-arguments - """ECHD encryption, keys supplied in binary data format""" - - if ephemcurve is None: - ephemcurve = curve - ephem = ECC(curve=ephemcurve) - key = sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - pubkey = ephem.get_pubkey() - iv = OpenSSL.rand(OpenSSL.get_cipher(ciphername).get_blocksize()) - ctx = Cipher(key_e, iv, 1, ciphername) - ciphertext = iv + pubkey + ctx.ciphering(data) - mac = hmac_sha256(key_m, ciphertext) - return ciphertext + mac - - def decrypt(self, data, ciphername='aes-256-cbc'): - """ - Decrypt data with ECIES method using the local private key - """ - # pylint: disable=too-many-locals - blocksize = OpenSSL.get_cipher(ciphername).get_blocksize() - iv = data[:blocksize] - i = blocksize - _, pubkey_x, pubkey_y, i2 = ECC._decode_pubkey(data[i:]) - i += i2 - ciphertext = data[i:len(data) - 32] - i += len(ciphertext) - mac = data[i:] - key = sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - if not equals(hmac_sha256(key_m, data[:len(data) - 32]), mac): - raise RuntimeError("Fail to verify data") - ctx = Cipher(key_e, iv, 0, ciphername) - return ctx.ciphering(ciphertext) diff --git a/src/lib/pyelliptic/hash.py b/src/lib/pyelliptic/hash.py deleted file mode 100644 index d6a15811..00000000 --- a/src/lib/pyelliptic/hash.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from .openssl import OpenSSL - - -# For python3 -def _equals_bytes(a, b): - if len(a) != len(b): - return False - result = 0 - for x, y in zip(a, b): - result |= x ^ y - return result == 0 - - -def _equals_str(a, b): - if len(a) != len(b): - return False - result = 0 - for x, y in zip(a, b): - result |= ord(x) ^ ord(y) - return result == 0 - - -def equals(a, b): - if isinstance(a, str): - return _equals_str(a, b) - else: - return _equals_bytes(a, b) - - -def hmac_sha256(k, m): - """ - Compute the key and the message with HMAC SHA5256 - """ - key = OpenSSL.malloc(k, len(k)) - d = OpenSSL.malloc(m, len(m)) - md = OpenSSL.malloc(0, 32) - i = OpenSSL.pointer(OpenSSL.c_int(0)) - OpenSSL.HMAC(OpenSSL.EVP_sha256(), key, len(k), d, len(m), md, i) - return md.raw - - -def hmac_sha512(k, m): - """ - Compute the key and the message with HMAC SHA512 - """ - key = OpenSSL.malloc(k, len(k)) - d = OpenSSL.malloc(m, len(m)) - md = OpenSSL.malloc(0, 64) - i = OpenSSL.pointer(OpenSSL.c_int(0)) - OpenSSL.HMAC(OpenSSL.EVP_sha512(), key, len(k), d, len(m), md, i) - return md.raw - - -def pbkdf2(password, salt=None, i=10000, keylen=64): - if salt is None: - salt = OpenSSL.rand(8) - p_password = OpenSSL.malloc(password, len(password)) - p_salt = OpenSSL.malloc(salt, len(salt)) - output = OpenSSL.malloc(0, keylen) - OpenSSL.PKCS5_PBKDF2_HMAC(p_password, len(password), p_salt, - len(p_salt), i, OpenSSL.EVP_sha256(), - keylen, output) - return salt, output.raw diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py deleted file mode 100644 index bc4fe6a6..00000000 --- a/src/lib/pyelliptic/openssl.py +++ /dev/null @@ -1,553 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. -# -# Software slightly changed by Jonathan Warren - -import sys -import ctypes - -OpenSSL = None - - -class CipherName: - def __init__(self, name, pointer, blocksize): - self._name = name - self._pointer = pointer - self._blocksize = blocksize - - def __str__(self): - return "Cipher : " + self._name + " | Blocksize : " + str(self._blocksize) + " | Fonction pointer : " + str(self._pointer) - - def get_pointer(self): - return self._pointer() - - def get_name(self): - return self._name - - def get_blocksize(self): - return self._blocksize - - -def get_version(library): - version = None - hexversion = None - cflags = None - try: - #OpenSSL 1.1 - OPENSSL_VERSION = 0 - OPENSSL_CFLAGS = 1 - library.OpenSSL_version.argtypes = [ctypes.c_int] - library.OpenSSL_version.restype = ctypes.c_char_p - version = library.OpenSSL_version(OPENSSL_VERSION) - cflags = library.OpenSSL_version(OPENSSL_CFLAGS) - library.OpenSSL_version_num.restype = ctypes.c_long - hexversion = library.OpenSSL_version_num() - except AttributeError: - try: - #OpenSSL 1.0 - SSLEAY_VERSION = 0 - SSLEAY_CFLAGS = 2 - library.SSLeay.restype = ctypes.c_long - library.SSLeay_version.restype = ctypes.c_char_p - library.SSLeay_version.argtypes = [ctypes.c_int] - version = library.SSLeay_version(SSLEAY_VERSION) - cflags = library.SSLeay_version(SSLEAY_CFLAGS) - hexversion = library.SSLeay() - except AttributeError: - #raise NotImplementedError('Cannot determine version of this OpenSSL library.') - pass - return (version, hexversion, cflags) - - -class _OpenSSL: - """ - Wrapper for OpenSSL using ctypes - """ - def __init__(self, library): - """ - Build the wrapper - """ - self._lib = ctypes.CDLL(library) - self._version, self._hexversion, self._cflags = get_version(self._lib) - self._libreSSL = self._version.startswith(b"LibreSSL") - - self.pointer = ctypes.pointer - self.c_int = ctypes.c_int - self.byref = ctypes.byref - self.create_string_buffer = ctypes.create_string_buffer - - self.BN_new = self._lib.BN_new - self.BN_new.restype = ctypes.c_void_p - self.BN_new.argtypes = [] - - self.BN_free = self._lib.BN_free - self.BN_free.restype = None - self.BN_free.argtypes = [ctypes.c_void_p] - - self.BN_num_bits = self._lib.BN_num_bits - self.BN_num_bits.restype = ctypes.c_int - self.BN_num_bits.argtypes = [ctypes.c_void_p] - - self.BN_bn2bin = self._lib.BN_bn2bin - self.BN_bn2bin.restype = ctypes.c_int - self.BN_bn2bin.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.BN_bin2bn = self._lib.BN_bin2bn - self.BN_bin2bn.restype = ctypes.c_void_p - self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p] - - self.EC_KEY_free = self._lib.EC_KEY_free - self.EC_KEY_free.restype = None - self.EC_KEY_free.argtypes = [ctypes.c_void_p] - - self.EC_KEY_new_by_curve_name = self._lib.EC_KEY_new_by_curve_name - self.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p - self.EC_KEY_new_by_curve_name.argtypes = [ctypes.c_int] - - self.EC_KEY_generate_key = self._lib.EC_KEY_generate_key - self.EC_KEY_generate_key.restype = ctypes.c_int - self.EC_KEY_generate_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_check_key = self._lib.EC_KEY_check_key - self.EC_KEY_check_key.restype = ctypes.c_int - self.EC_KEY_check_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_private_key = self._lib.EC_KEY_get0_private_key - self.EC_KEY_get0_private_key.restype = ctypes.c_void_p - self.EC_KEY_get0_private_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_public_key = self._lib.EC_KEY_get0_public_key - self.EC_KEY_get0_public_key.restype = ctypes.c_void_p - self.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group - self.EC_KEY_get0_group.restype = ctypes.c_void_p - self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] - - self.EC_POINT_get_affine_coordinates_GFp = self._lib.EC_POINT_get_affine_coordinates_GFp - self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int - self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key - self.EC_KEY_set_private_key.restype = ctypes.c_int - self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - self.EC_KEY_set_public_key = self._lib.EC_KEY_set_public_key - self.EC_KEY_set_public_key.restype = ctypes.c_int - self.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - self.EC_KEY_set_group = self._lib.EC_KEY_set_group - self.EC_KEY_set_group.restype = ctypes.c_int - self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EC_POINT_set_affine_coordinates_GFp = self._lib.EC_POINT_set_affine_coordinates_GFp - self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int - self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_POINT_new = self._lib.EC_POINT_new - self.EC_POINT_new.restype = ctypes.c_void_p - self.EC_POINT_new.argtypes = [ctypes.c_void_p] - - self.EC_POINT_free = self._lib.EC_POINT_free - self.EC_POINT_free.restype = None - self.EC_POINT_free.argtypes = [ctypes.c_void_p] - - self.BN_CTX_free = self._lib.BN_CTX_free - self.BN_CTX_free.restype = None - self.BN_CTX_free.argtypes = [ctypes.c_void_p] - - self.EC_POINT_mul = self._lib.EC_POINT_mul - self.EC_POINT_mul.restype = ctypes.c_int - self.EC_POINT_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key - self.EC_KEY_set_private_key.restype = ctypes.c_int - self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL - self._lib.EC_KEY_OpenSSL.restype = ctypes.c_void_p - self._lib.EC_KEY_OpenSSL.argtypes = [] - - self.EC_KEY_set_method = self._lib.EC_KEY_set_method - self._lib.EC_KEY_set_method.restype = ctypes.c_int - self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - else: - self.ECDH_OpenSSL = self._lib.ECDH_OpenSSL - self._lib.ECDH_OpenSSL.restype = ctypes.c_void_p - self._lib.ECDH_OpenSSL.argtypes = [] - - self.ECDH_set_method = self._lib.ECDH_set_method - self._lib.ECDH_set_method.restype = ctypes.c_int - self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.BN_CTX_new = self._lib.BN_CTX_new - self._lib.BN_CTX_new.restype = ctypes.c_void_p - self._lib.BN_CTX_new.argtypes = [] - - self.ECDH_compute_key = self._lib.ECDH_compute_key - self.ECDH_compute_key.restype = ctypes.c_int - self.ECDH_compute_key.argtypes = [ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_CipherInit_ex = self._lib.EVP_CipherInit_ex - self.EVP_CipherInit_ex.restype = ctypes.c_int - self.EVP_CipherInit_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_CIPHER_CTX_new = self._lib.EVP_CIPHER_CTX_new - self.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p - self.EVP_CIPHER_CTX_new.argtypes = [] - - # Cipher - self.EVP_aes_128_cfb128 = self._lib.EVP_aes_128_cfb128 - self.EVP_aes_128_cfb128.restype = ctypes.c_void_p - self.EVP_aes_128_cfb128.argtypes = [] - - self.EVP_aes_256_cfb128 = self._lib.EVP_aes_256_cfb128 - self.EVP_aes_256_cfb128.restype = ctypes.c_void_p - self.EVP_aes_256_cfb128.argtypes = [] - - self.EVP_aes_128_cbc = self._lib.EVP_aes_128_cbc - self.EVP_aes_128_cbc.restype = ctypes.c_void_p - self.EVP_aes_128_cbc.argtypes = [] - - self.EVP_aes_256_cbc = self._lib.EVP_aes_256_cbc - self.EVP_aes_256_cbc.restype = ctypes.c_void_p - self.EVP_aes_256_cbc.argtypes = [] - - #self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr - #self.EVP_aes_128_ctr.restype = ctypes.c_void_p - #self.EVP_aes_128_ctr.argtypes = [] - - #self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr - #self.EVP_aes_256_ctr.restype = ctypes.c_void_p - #self.EVP_aes_256_ctr.argtypes = [] - - self.EVP_aes_128_ofb = self._lib.EVP_aes_128_ofb - self.EVP_aes_128_ofb.restype = ctypes.c_void_p - self.EVP_aes_128_ofb.argtypes = [] - - self.EVP_aes_256_ofb = self._lib.EVP_aes_256_ofb - self.EVP_aes_256_ofb.restype = ctypes.c_void_p - self.EVP_aes_256_ofb.argtypes = [] - - self.EVP_bf_cbc = self._lib.EVP_bf_cbc - self.EVP_bf_cbc.restype = ctypes.c_void_p - self.EVP_bf_cbc.argtypes = [] - - self.EVP_bf_cfb64 = self._lib.EVP_bf_cfb64 - self.EVP_bf_cfb64.restype = ctypes.c_void_p - self.EVP_bf_cfb64.argtypes = [] - - self.EVP_rc4 = self._lib.EVP_rc4 - self.EVP_rc4.restype = ctypes.c_void_p - self.EVP_rc4.argtypes = [] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EVP_CIPHER_CTX_reset = self._lib.EVP_CIPHER_CTX_reset - self.EVP_CIPHER_CTX_reset.restype = ctypes.c_int - self.EVP_CIPHER_CTX_reset.argtypes = [ctypes.c_void_p] - else: - self.EVP_CIPHER_CTX_cleanup = self._lib.EVP_CIPHER_CTX_cleanup - self.EVP_CIPHER_CTX_cleanup.restype = ctypes.c_int - self.EVP_CIPHER_CTX_cleanup.argtypes = [ctypes.c_void_p] - - self.EVP_CIPHER_CTX_free = self._lib.EVP_CIPHER_CTX_free - self.EVP_CIPHER_CTX_free.restype = None - self.EVP_CIPHER_CTX_free.argtypes = [ctypes.c_void_p] - - self.EVP_CipherUpdate = self._lib.EVP_CipherUpdate - self.EVP_CipherUpdate.restype = ctypes.c_int - self.EVP_CipherUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] - - self.EVP_CipherFinal_ex = self._lib.EVP_CipherFinal_ex - self.EVP_CipherFinal_ex.restype = ctypes.c_int - self.EVP_CipherFinal_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestInit = self._lib.EVP_DigestInit - self.EVP_DigestInit.restype = ctypes.c_int - self._lib.EVP_DigestInit.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex - self.EVP_DigestInit_ex.restype = ctypes.c_int - self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p] - - self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate - self.EVP_DigestUpdate.restype = ctypes.c_int - self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_int] - - self.EVP_DigestFinal = self._lib.EVP_DigestFinal - self.EVP_DigestFinal.restype = ctypes.c_int - self.EVP_DigestFinal.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestFinal_ex = self._lib.EVP_DigestFinal_ex - self.EVP_DigestFinal_ex.restype = ctypes.c_int - self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.ECDSA_sign = self._lib.ECDSA_sign - self.ECDSA_sign.restype = ctypes.c_int - self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.ECDSA_verify = self._lib.ECDSA_verify - self.ECDSA_verify.restype = ctypes.c_int - self.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EVP_MD_CTX_new = self._lib.EVP_MD_CTX_new - self.EVP_MD_CTX_new.restype = ctypes.c_void_p - self.EVP_MD_CTX_new.argtypes = [] - - self.EVP_MD_CTX_reset = self._lib.EVP_MD_CTX_reset - self.EVP_MD_CTX_reset.restype = None - self.EVP_MD_CTX_reset.argtypes = [ctypes.c_void_p] - - self.EVP_MD_CTX_free = self._lib.EVP_MD_CTX_free - self.EVP_MD_CTX_free.restype = None - self.EVP_MD_CTX_free.argtypes = [ctypes.c_void_p] - - self.EVP_sha1 = self._lib.EVP_sha1 - self.EVP_sha1.restype = ctypes.c_void_p - self.EVP_sha1.argtypes = [] - - self.digest_ecdsa_sha1 = self.EVP_sha1 - else: - self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create - self.EVP_MD_CTX_create.restype = ctypes.c_void_p - self.EVP_MD_CTX_create.argtypes = [] - - self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init - self.EVP_MD_CTX_init.restype = None - self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p] - - self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy - self.EVP_MD_CTX_destroy.restype = None - self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p] - - self.EVP_ecdsa = self._lib.EVP_ecdsa - self._lib.EVP_ecdsa.restype = ctypes.c_void_p - self._lib.EVP_ecdsa.argtypes = [] - - self.digest_ecdsa_sha1 = self.EVP_ecdsa - - self.RAND_bytes = self._lib.RAND_bytes - self.RAND_bytes.restype = ctypes.c_int - self.RAND_bytes.argtypes = [ctypes.c_void_p, ctypes.c_int] - - self.EVP_sha256 = self._lib.EVP_sha256 - self.EVP_sha256.restype = ctypes.c_void_p - self.EVP_sha256.argtypes = [] - - self.i2o_ECPublicKey = self._lib.i2o_ECPublicKey - self.i2o_ECPublicKey.restype = ctypes.c_int - self.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_sha512 = self._lib.EVP_sha512 - self.EVP_sha512.restype = ctypes.c_void_p - self.EVP_sha512.argtypes = [] - - self.HMAC = self._lib.HMAC - self.HMAC.restype = ctypes.c_void_p - self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] - - try: - self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC - except: - # The above is not compatible with all versions of OSX. - self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 - - self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int - self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_int, - ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p] - - self._set_ciphers() - self._set_curves() - - def _set_ciphers(self): - self.cipher_algo = { - 'aes-128-cbc': CipherName('aes-128-cbc', self.EVP_aes_128_cbc, 16), - 'aes-256-cbc': CipherName('aes-256-cbc', self.EVP_aes_256_cbc, 16), - 'aes-128-cfb': CipherName('aes-128-cfb', self.EVP_aes_128_cfb128, 16), - 'aes-256-cfb': CipherName('aes-256-cfb', self.EVP_aes_256_cfb128, 16), - 'aes-128-ofb': CipherName('aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), - 'aes-256-ofb': CipherName('aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), - #'aes-128-ctr': CipherName('aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), - #'aes-256-ctr': CipherName('aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), - 'bf-cfb': CipherName('bf-cfb', self.EVP_bf_cfb64, 8), - 'bf-cbc': CipherName('bf-cbc', self.EVP_bf_cbc, 8), - 'rc4': CipherName('rc4', self.EVP_rc4, 128), # 128 is the initialisation size not block size - } - - def _set_curves(self): - self.curves = { - 'secp112r1': 704, - 'secp112r2': 705, - 'secp128r1': 706, - 'secp128r2': 707, - 'secp160k1': 708, - 'secp160r1': 709, - 'secp160r2': 710, - 'secp192k1': 711, - 'secp224k1': 712, - 'secp224r1': 713, - 'secp256k1': 714, - 'secp384r1': 715, - 'secp521r1': 716, - 'sect113r1': 717, - 'sect113r2': 718, - 'sect131r1': 719, - 'sect131r2': 720, - 'sect163k1': 721, - 'sect163r1': 722, - 'sect163r2': 723, - 'sect193r1': 724, - 'sect193r2': 725, - 'sect233k1': 726, - 'sect233r1': 727, - 'sect239k1': 728, - 'sect283k1': 729, - 'sect283r1': 730, - 'sect409k1': 731, - 'sect409r1': 732, - 'sect571k1': 733, - 'sect571r1': 734, - } - - def BN_num_bytes(self, x): - """ - returns the length of a BN (OpenSSl API) - """ - return int((self.BN_num_bits(x) + 7) / 8) - - def get_cipher(self, name): - """ - returns the OpenSSL cipher instance - """ - if name not in self.cipher_algo: - raise Exception("Unknown cipher") - return self.cipher_algo[name] - - def get_curve(self, name): - """ - returns the id of a elliptic curve - """ - if name not in self.curves: - raise Exception("Unknown curve") - return self.curves[name] - - def get_curve_by_id(self, id): - """ - returns the name of a elliptic curve with his id - """ - res = None - for i in self.curves: - if self.curves[i] == id: - res = i - break - if res is None: - raise Exception("Unknown curve") - return res - - def rand(self, size): - """ - OpenSSL random function - """ - buffer = self.malloc(0, size) - # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is - # evidently possible that it returned an error and not-actually-random data. However, in - # tests on various operating systems, while generating hundreds of gigabytes of random - # strings of various sizes I could not get an error to occur. Also Bitcoin doesn't check - # the return value of RAND_bytes either. - # Fixed in Bitmessage version 0.4.2 (in source code on 2013-10-13) - while self.RAND_bytes(buffer, size) != 1: - import time - time.sleep(1) - return buffer.raw - - def malloc(self, data, size): - """ - returns a create_string_buffer (ctypes) - """ - buffer = None - if data != 0: - if sys.version_info.major == 3 and isinstance(data, type('')): - data = data.encode() - buffer = self.create_string_buffer(data, size) - else: - buffer = self.create_string_buffer(size) - return buffer - -def loadOpenSSL(): - global OpenSSL - from os import path, environ - from ctypes.util import find_library - - libdir = [] - - if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: - libdir.append(find_library('ssl')) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append(find_library('libeay32')) - - if getattr(sys,'frozen', None): - if 'darwin' in sys.platform: - libdir.extend([ - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.1.0.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.2.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.1.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.0.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.0.9.8.dylib'), - ]) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append(path.join(sys._MEIPASS, 'libeay32.dll')) - else: - libdir.extend([ - path.join(sys._MEIPASS, 'libcrypto.so'), - path.join(sys._MEIPASS, 'libssl.so'), - path.join(sys._MEIPASS, 'libcrypto.so.1.1.0'), - path.join(sys._MEIPASS, 'libssl.so.1.1.0'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.2'), - path.join(sys._MEIPASS, 'libssl.so.1.0.2'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.1'), - path.join(sys._MEIPASS, 'libssl.so.1.0.1'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.0'), - path.join(sys._MEIPASS, 'libssl.so.1.0.0'), - path.join(sys._MEIPASS, 'libcrypto.so.0.9.8'), - path.join(sys._MEIPASS, 'libssl.so.0.9.8'), - ]) - if 'darwin' in sys.platform: - libdir.extend(['libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append('libeay32.dll') - else: - libdir.append('libcrypto.so') - libdir.append('libssl.so') - libdir.append('libcrypto.so.1.0.0') - libdir.append('libssl.so.1.0.0') - for library in libdir: - try: - OpenSSL = _OpenSSL(library) - return - except: - pass - raise Exception("Failed to load OpenSSL library, searched for: " + " ".join(libdir)) - -loadOpenSSL() diff --git a/src/lib/pyelliptic/setup.py b/src/lib/pyelliptic/setup.py deleted file mode 100644 index cc9c0a21..00000000 --- a/src/lib/pyelliptic/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="pyelliptic", - version='2.0.1', - url='https://github.com/radfish/pyelliptic', - license='GPL', - description="Python OpenSSL wrapper for ECC (ECDSA, ECIES), AES, HMAC, Blowfish, ...", - author='Yann GUIBET', - author_email='yannguibet@gmail.com', - maintainer="redfish", - maintainer_email='redfish@galactica.pw', - packages=find_packages(), - classifiers=[ - 'Operating System :: Unix', - 'Operating System :: Microsoft :: Windows', - 'Environment :: MacOS X', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.7', - 'Topic :: Security :: Cryptography', - ], -) diff --git a/src/lib/sslcrypto/LICENSE b/src/lib/sslcrypto/LICENSE new file mode 100644 index 00000000..2feefc45 --- /dev/null +++ b/src/lib/sslcrypto/LICENSE @@ -0,0 +1,27 @@ +MIT License + +Copyright (c) 2019 Ivan Machugovskiy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Additionally, the following licenses must be preserved: + +- ripemd implementation is licensed under BSD-3 by Markus Friedl, see `_ripemd.py`; +- jacobian curve implementation is dual-licensed under MIT or public domain license, see `_jacobian.py`. diff --git a/src/lib/sslcrypto/__init__.py b/src/lib/sslcrypto/__init__.py new file mode 100644 index 00000000..77f9b3f3 --- /dev/null +++ b/src/lib/sslcrypto/__init__.py @@ -0,0 +1,6 @@ +__all__ = ["aes", "ecc", "rsa"] + +try: + from .openssl import aes, ecc, rsa +except OSError: + from .fallback import aes, ecc, rsa diff --git a/src/lib/sslcrypto/_aes.py b/src/lib/sslcrypto/_aes.py new file mode 100644 index 00000000..4f8d4ec2 --- /dev/null +++ b/src/lib/sslcrypto/_aes.py @@ -0,0 +1,53 @@ +# pylint: disable=import-outside-toplevel + +class AES: + def __init__(self, backend, fallback=None): + self._backend = backend + self._fallback = fallback + + + def get_algo_key_length(self, algo): + if algo.count("-") != 2: + raise ValueError("Invalid algorithm name") + try: + return int(algo.split("-")[1]) // 8 + except ValueError: + raise ValueError("Invalid algorithm name") from None + + + def new_key(self, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.new_key(algo) + return self._backend.random(self.get_algo_key_length(algo)) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.encrypt(data, key, algo) + + key_length = self.get_algo_key_length(algo) + if len(key) != key_length: + raise ValueError("Expected key to be {} bytes, got {} bytes".format(key_length, len(key))) + + return self._backend.encrypt(data, key, algo) + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.decrypt(ciphertext, iv, key, algo) + + key_length = self.get_algo_key_length(algo) + if len(key) != key_length: + raise ValueError("Expected key to be {} bytes, got {} bytes".format(key_length, len(key))) + + return self._backend.decrypt(ciphertext, iv, key, algo) + + + def get_backend(self): + return self._backend.get_backend() diff --git a/src/lib/sslcrypto/_ecc.py b/src/lib/sslcrypto/_ecc.py new file mode 100644 index 00000000..9831d688 --- /dev/null +++ b/src/lib/sslcrypto/_ecc.py @@ -0,0 +1,334 @@ +import hashlib +import struct +import hmac +import base58 + + +try: + hashlib.new("ripemd160") +except ValueError: + # No native implementation + from . import _ripemd + def ripemd160(*args): + return _ripemd.new(*args) +else: + # Use OpenSSL + def ripemd160(*args): + return hashlib.new("ripemd160", *args) + + +class ECC: + CURVES = { + "secp112r1": 704, + "secp112r2": 705, + "secp128r1": 706, + "secp128r2": 707, + "secp160k1": 708, + "secp160r1": 709, + "secp160r2": 710, + "secp192k1": 711, + "prime192v1": 409, + "secp224k1": 712, + "secp224r1": 713, + "secp256k1": 714, + "prime256v1": 415, + "secp384r1": 715, + "secp521r1": 716 + } + + def __init__(self, backend, aes): + self._backend = backend + self._aes = aes + + + def get_curve(self, name): + if name not in self.CURVES: + raise ValueError("Unknown curve {}".format(name)) + nid = self.CURVES[name] + return EllipticCurve(self._backend(nid), self._aes, nid) + + + def get_backend(self): + return self._backend.get_backend() + + +class EllipticCurve: + def __init__(self, backend, aes, nid): + self._backend = backend + self._aes = aes + self.nid = nid + + + def _encode_public_key(self, x, y, is_compressed=True, raw=True): + if raw: + if is_compressed: + return bytes([0x02 + (y[-1] % 2)]) + x + else: + return bytes([0x04]) + x + y + else: + return struct.pack("!HH", self.nid, len(x)) + x + struct.pack("!H", len(y)) + y + + + def _decode_public_key(self, public_key, partial=False): + if not public_key: + raise ValueError("No public key") + + if public_key[0] == 0x04: + # Uncompressed + expected_length = 1 + 2 * self._backend.public_key_length + if partial: + if len(public_key) < expected_length: + raise ValueError("Invalid uncompressed public key length") + else: + if len(public_key) != expected_length: + raise ValueError("Invalid uncompressed public key length") + x = public_key[1:1 + self._backend.public_key_length] + y = public_key[1 + self._backend.public_key_length:expected_length] + if partial: + return (x, y), expected_length + else: + return x, y + elif public_key[0] in (0x02, 0x03): + # Compressed + expected_length = 1 + self._backend.public_key_length + if partial: + if len(public_key) < expected_length: + raise ValueError("Invalid compressed public key length") + else: + if len(public_key) != expected_length: + raise ValueError("Invalid compressed public key length") + + x, y = self._backend.decompress_point(public_key[:expected_length]) + # Sanity check + if x != public_key[1:expected_length]: + raise ValueError("Incorrect compressed public key") + if partial: + return (x, y), expected_length + else: + return x, y + else: + raise ValueError("Invalid public key prefix") + + + def _decode_public_key_openssl(self, public_key, partial=False): + if not public_key: + raise ValueError("No public key") + + i = 0 + + nid, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if nid != self.nid: + raise ValueError("Wrong curve") + + xlen, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if len(public_key) - i < xlen: + raise ValueError("Too short public key") + x = public_key[i:i + xlen] + i += xlen + + ylen, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if len(public_key) - i < ylen: + raise ValueError("Too short public key") + y = public_key[i:i + ylen] + i += ylen + + if partial: + return (x, y), i + else: + if i < len(public_key): + raise ValueError("Too long public key") + return x, y + + + def new_private_key(self): + return self._backend.new_private_key() + + + def private_to_public(self, private_key, is_compressed=True): + x, y = self._backend.private_to_public(private_key) + return self._encode_public_key(x, y, is_compressed=is_compressed) + + + def private_to_wif(self, private_key): + return base58.b58encode_check(b"\x80" + private_key) + + + def wif_to_private(self, wif): + dec = base58.b58decode_check(wif) + if dec[0] != 0x80: + raise ValueError("Invalid network (expected mainnet)") + return dec[1:] + + + def public_to_address(self, public_key): + h = hashlib.sha256(public_key).digest() + hash160 = ripemd160(h).digest() + return base58.b58encode_check(b"\x00" + hash160) + + + def private_to_address(self, private_key, is_compressed=True): + # Kinda useless but left for quick migration from pybitcointools + return self.public_to_address(self.private_to_public(private_key, is_compressed=is_compressed)) + + + def derive(self, private_key, public_key): + if not isinstance(public_key, tuple): + public_key = self._decode_public_key(public_key) + return self._backend.ecdh(private_key, public_key) + + + def _digest(self, data, hash): + if hash is None: + return data + elif callable(hash): + return hash(data) + elif hash == "sha1": + return hashlib.sha1(data).digest() + elif hash == "sha256": + return hashlib.sha256(data).digest() + elif hash == "sha512": + return hashlib.sha512(data).digest() + else: + raise ValueError("Unknown hash/derivation method") + + + # High-level functions + def encrypt(self, data, public_key, algo="aes-256-cbc", derivation="sha256", mac="hmac-sha256", return_aes_key=False): + # Generate ephemeral private key + private_key = self.new_private_key() + + # Derive key + ecdh = self.derive(private_key, public_key) + key = self._digest(ecdh, derivation) + k_enc_len = self._aes.get_algo_key_length(algo) + if len(key) < k_enc_len: + raise ValueError("Too short digest") + k_enc, k_mac = key[:k_enc_len], key[k_enc_len:] + + # Encrypt + ciphertext, iv = self._aes.encrypt(data, k_enc, algo=algo) + ephem_public_key = self.private_to_public(private_key) + ephem_public_key = self._decode_public_key(ephem_public_key) + ephem_public_key = self._encode_public_key(*ephem_public_key, raw=False) + ciphertext = iv + ephem_public_key + ciphertext + + # Add MAC tag + if callable(mac): + tag = mac(k_mac, ciphertext) + elif mac == "hmac-sha256": + h = hmac.new(k_mac, digestmod="sha256") + h.update(ciphertext) + tag = h.digest() + elif mac == "hmac-sha512": + h = hmac.new(k_mac, digestmod="sha512") + h.update(ciphertext) + tag = h.digest() + elif mac is None: + tag = b"" + else: + raise ValueError("Unsupported MAC") + + if return_aes_key: + return ciphertext + tag, k_enc + else: + return ciphertext + tag + + + def decrypt(self, ciphertext, private_key, algo="aes-256-cbc", derivation="sha256", mac="hmac-sha256"): + # Get MAC tag + if callable(mac): + tag_length = mac.digest_size + elif mac == "hmac-sha256": + tag_length = hmac.new(b"", digestmod="sha256").digest_size + elif mac == "hmac-sha512": + tag_length = hmac.new(b"", digestmod="sha512").digest_size + elif mac is None: + tag_length = 0 + else: + raise ValueError("Unsupported MAC") + + if len(ciphertext) < tag_length: + raise ValueError("Ciphertext is too small to contain MAC tag") + if tag_length == 0: + tag = b"" + else: + ciphertext, tag = ciphertext[:-tag_length], ciphertext[-tag_length:] + + orig_ciphertext = ciphertext + + if len(ciphertext) < 16: + raise ValueError("Ciphertext is too small to contain IV") + iv, ciphertext = ciphertext[:16], ciphertext[16:] + + public_key, pos = self._decode_public_key_openssl(ciphertext, partial=True) + ciphertext = ciphertext[pos:] + + # Derive key + ecdh = self.derive(private_key, public_key) + key = self._digest(ecdh, derivation) + k_enc_len = self._aes.get_algo_key_length(algo) + if len(key) < k_enc_len: + raise ValueError("Too short digest") + k_enc, k_mac = key[:k_enc_len], key[k_enc_len:] + + # Verify MAC tag + if callable(mac): + expected_tag = mac(k_mac, orig_ciphertext) + elif mac == "hmac-sha256": + h = hmac.new(k_mac, digestmod="sha256") + h.update(orig_ciphertext) + expected_tag = h.digest() + elif mac == "hmac-sha512": + h = hmac.new(k_mac, digestmod="sha512") + h.update(orig_ciphertext) + expected_tag = h.digest() + elif mac is None: + expected_tag = b"" + + if not hmac.compare_digest(tag, expected_tag): + raise ValueError("Invalid MAC tag") + + return self._aes.decrypt(ciphertext, iv, k_enc, algo=algo) + + + def sign(self, data, private_key, hash="sha256", recoverable=False, is_compressed=True, entropy=None): + data = self._digest(data, hash) + if not entropy: + v = b"\x01" * len(data) + k = b"\x00" * len(data) + k = hmac.new(k, v + b"\x00" + private_key + data, "sha256").digest() + v = hmac.new(k, v, "sha256").digest() + k = hmac.new(k, v + b"\x01" + private_key + data, "sha256").digest() + v = hmac.new(k, v, "sha256").digest() + entropy = hmac.new(k, v, "sha256").digest() + return self._backend.sign(data, private_key, recoverable, is_compressed, entropy=entropy) + + + def recover(self, signature, data, hash="sha256"): + # Sanity check: is this signature recoverable? + if len(signature) != 1 + 2 * self._backend.public_key_length: + raise ValueError("Cannot recover an unrecoverable signature") + x, y = self._backend.recover(signature, self._digest(data, hash)) + is_compressed = signature[0] >= 31 + return self._encode_public_key(x, y, is_compressed=is_compressed) + + + def verify(self, signature, data, public_key, hash="sha256"): + if len(signature) == 1 + 2 * self._backend.public_key_length: + # Recoverable signature + signature = signature[1:] + if len(signature) != 2 * self._backend.public_key_length: + raise ValueError("Invalid signature format") + if not isinstance(public_key, tuple): + public_key = self._decode_public_key(public_key) + return self._backend.verify(signature, self._digest(data, hash), public_key) + + + def derive_child(self, seed, child): + # Based on BIP32 + if not 0 <= child < 2 ** 31: + raise ValueError("Invalid child index") + return self._backend.derive_child(seed, child) diff --git a/src/lib/sslcrypto/_ripemd.py b/src/lib/sslcrypto/_ripemd.py new file mode 100644 index 00000000..89377cc2 --- /dev/null +++ b/src/lib/sslcrypto/_ripemd.py @@ -0,0 +1,375 @@ +# Copyright (c) 2001 Markus Friedl. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# pylint: skip-file + +import sys + +digest_size = 20 +digestsize = 20 + +class RIPEMD160: + """ + Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed. + """ + + def __init__(self, arg=None): + self.ctx = RMDContext() + if arg: + self.update(arg) + self.dig = None + + def update(self, arg): + RMD160Update(self.ctx, arg, len(arg)) + self.dig = None + + def digest(self): + if self.dig: + return self.dig + ctx = self.ctx.copy() + self.dig = RMD160Final(self.ctx) + self.ctx = ctx + return self.dig + + def hexdigest(self): + dig = self.digest() + hex_digest = "" + for d in dig: + hex_digest += "%02x" % d + return hex_digest + + def copy(self): + import copy + return copy.deepcopy(self) + + + +def new(arg=None): + """ + Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed. + """ + return RIPEMD160(arg) + + + +# +# Private. +# + +class RMDContext: + def __init__(self): + self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, + 0x10325476, 0xC3D2E1F0] # uint32 + self.count = 0 # uint64 + self.buffer = [0] * 64 # uchar + def copy(self): + ctx = RMDContext() + ctx.state = self.state[:] + ctx.count = self.count + ctx.buffer = self.buffer[:] + return ctx + +K0 = 0x00000000 +K1 = 0x5A827999 +K2 = 0x6ED9EBA1 +K3 = 0x8F1BBCDC +K4 = 0xA953FD4E + +KK0 = 0x50A28BE6 +KK1 = 0x5C4DD124 +KK2 = 0x6D703EF3 +KK3 = 0x7A6D76E9 +KK4 = 0x00000000 + +def ROL(n, x): + return ((x << n) & 0xffffffff) | (x >> (32 - n)) + +def F0(x, y, z): + return x ^ y ^ z + +def F1(x, y, z): + return (x & y) | (((~x) % 0x100000000) & z) + +def F2(x, y, z): + return (x | ((~y) % 0x100000000)) ^ z + +def F3(x, y, z): + return (x & z) | (((~z) % 0x100000000) & y) + +def F4(x, y, z): + return x ^ (y | ((~z) % 0x100000000)) + +def R(a, b, c, d, e, Fj, Kj, sj, rj, X): + a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e + c = ROL(10, c) + return a % 0x100000000, c + +PADDING = [0x80] + [0] * 63 + +import sys +import struct + +def RMD160Transform(state, block): # uint32 state[5], uchar block[64] + x = [0] * 16 + if sys.byteorder == "little": + x = struct.unpack("<16L", bytes(block[0:64])) + else: + raise ValueError("Big-endian platforms are not supported") + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + # Round 1 + a, c = R(a, b, c, d, e, F0, K0, 11, 0, x) + e, b = R(e, a, b, c, d, F0, K0, 14, 1, x) + d, a = R(d, e, a, b, c, F0, K0, 15, 2, x) + c, e = R(c, d, e, a, b, F0, K0, 12, 3, x) + b, d = R(b, c, d, e, a, F0, K0, 5, 4, x) + a, c = R(a, b, c, d, e, F0, K0, 8, 5, x) + e, b = R(e, a, b, c, d, F0, K0, 7, 6, x) + d, a = R(d, e, a, b, c, F0, K0, 9, 7, x) + c, e = R(c, d, e, a, b, F0, K0, 11, 8, x) + b, d = R(b, c, d, e, a, F0, K0, 13, 9, x) + a, c = R(a, b, c, d, e, F0, K0, 14, 10, x) + e, b = R(e, a, b, c, d, F0, K0, 15, 11, x) + d, a = R(d, e, a, b, c, F0, K0, 6, 12, x) + c, e = R(c, d, e, a, b, F0, K0, 7, 13, x) + b, d = R(b, c, d, e, a, F0, K0, 9, 14, x) + a, c = R(a, b, c, d, e, F0, K0, 8, 15, x) # #15 + # Round 2 + e, b = R(e, a, b, c, d, F1, K1, 7, 7, x) + d, a = R(d, e, a, b, c, F1, K1, 6, 4, x) + c, e = R(c, d, e, a, b, F1, K1, 8, 13, x) + b, d = R(b, c, d, e, a, F1, K1, 13, 1, x) + a, c = R(a, b, c, d, e, F1, K1, 11, 10, x) + e, b = R(e, a, b, c, d, F1, K1, 9, 6, x) + d, a = R(d, e, a, b, c, F1, K1, 7, 15, x) + c, e = R(c, d, e, a, b, F1, K1, 15, 3, x) + b, d = R(b, c, d, e, a, F1, K1, 7, 12, x) + a, c = R(a, b, c, d, e, F1, K1, 12, 0, x) + e, b = R(e, a, b, c, d, F1, K1, 15, 9, x) + d, a = R(d, e, a, b, c, F1, K1, 9, 5, x) + c, e = R(c, d, e, a, b, F1, K1, 11, 2, x) + b, d = R(b, c, d, e, a, F1, K1, 7, 14, x) + a, c = R(a, b, c, d, e, F1, K1, 13, 11, x) + e, b = R(e, a, b, c, d, F1, K1, 12, 8, x) # #31 + # Round 3 + d, a = R(d, e, a, b, c, F2, K2, 11, 3, x) + c, e = R(c, d, e, a, b, F2, K2, 13, 10, x) + b, d = R(b, c, d, e, a, F2, K2, 6, 14, x) + a, c = R(a, b, c, d, e, F2, K2, 7, 4, x) + e, b = R(e, a, b, c, d, F2, K2, 14, 9, x) + d, a = R(d, e, a, b, c, F2, K2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, K2, 13, 8, x) + b, d = R(b, c, d, e, a, F2, K2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, K2, 14, 2, x) + e, b = R(e, a, b, c, d, F2, K2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, K2, 13, 0, x) + c, e = R(c, d, e, a, b, F2, K2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, K2, 5, 13, x) + a, c = R(a, b, c, d, e, F2, K2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, K2, 7, 5, x) + d, a = R(d, e, a, b, c, F2, K2, 5, 12, x) # #47 + # Round 4 + c, e = R(c, d, e, a, b, F3, K3, 11, 1, x) + b, d = R(b, c, d, e, a, F3, K3, 12, 9, x) + a, c = R(a, b, c, d, e, F3, K3, 14, 11, x) + e, b = R(e, a, b, c, d, F3, K3, 15, 10, x) + d, a = R(d, e, a, b, c, F3, K3, 14, 0, x) + c, e = R(c, d, e, a, b, F3, K3, 15, 8, x) + b, d = R(b, c, d, e, a, F3, K3, 9, 12, x) + a, c = R(a, b, c, d, e, F3, K3, 8, 4, x) + e, b = R(e, a, b, c, d, F3, K3, 9, 13, x) + d, a = R(d, e, a, b, c, F3, K3, 14, 3, x) + c, e = R(c, d, e, a, b, F3, K3, 5, 7, x) + b, d = R(b, c, d, e, a, F3, K3, 6, 15, x) + a, c = R(a, b, c, d, e, F3, K3, 8, 14, x) + e, b = R(e, a, b, c, d, F3, K3, 6, 5, x) + d, a = R(d, e, a, b, c, F3, K3, 5, 6, x) + c, e = R(c, d, e, a, b, F3, K3, 12, 2, x) # #63 + # Round 5 + b, d = R(b, c, d, e, a, F4, K4, 9, 4, x) + a, c = R(a, b, c, d, e, F4, K4, 15, 0, x) + e, b = R(e, a, b, c, d, F4, K4, 5, 5, x) + d, a = R(d, e, a, b, c, F4, K4, 11, 9, x) + c, e = R(c, d, e, a, b, F4, K4, 6, 7, x) + b, d = R(b, c, d, e, a, F4, K4, 8, 12, x) + a, c = R(a, b, c, d, e, F4, K4, 13, 2, x) + e, b = R(e, a, b, c, d, F4, K4, 12, 10, x) + d, a = R(d, e, a, b, c, F4, K4, 5, 14, x) + c, e = R(c, d, e, a, b, F4, K4, 12, 1, x) + b, d = R(b, c, d, e, a, F4, K4, 13, 3, x) + a, c = R(a, b, c, d, e, F4, K4, 14, 8, x) + e, b = R(e, a, b, c, d, F4, K4, 11, 11, x) + d, a = R(d, e, a, b, c, F4, K4, 8, 6, x) + c, e = R(c, d, e, a, b, F4, K4, 5, 15, x) + b, d = R(b, c, d, e, a, F4, K4, 6, 13, x) # #79 + + aa = a + bb = b + cc = c + dd = d + ee = e + + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + # Parallel round 1 + a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) + e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) + d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) + c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) + b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) + a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) + e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) + d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) + c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) + b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) + a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) + e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) + d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) + c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) + b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) + a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) # #15 + # Parallel round 2 + e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) + d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) + c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) + a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) + e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) + d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) + c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) + a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) + e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) + d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) + c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) + b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) + a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) + e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) # #31 + # Parallel round 3 + d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) + b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) + e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) + c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) + a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) + c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) + b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) + a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) + e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) # #47 + # Parallel round 4 + c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) + b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) + a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) + e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) + d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) + c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) + b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) + a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) + e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) + d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) + c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) + b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) + a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) + e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) + d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) + c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) # #63 + # Parallel round 5 + b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) + e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) + d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) + c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) + b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) + a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) + e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) + d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) + c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) + b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) + e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) + d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) + c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) + b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) # #79 + + t = (state[1] + cc + d) % 0x100000000 + state[1] = (state[2] + dd + e) % 0x100000000 + state[2] = (state[3] + ee + a) % 0x100000000 + state[3] = (state[4] + aa + b) % 0x100000000 + state[4] = (state[0] + bb + c) % 0x100000000 + state[0] = t % 0x100000000 + + +def RMD160Update(ctx, inp, inplen): + if type(inp) == str: + inp = [ord(i)&0xff for i in inp] + + have = int((ctx.count // 8) % 64) + inplen = int(inplen) + need = 64 - have + ctx.count += 8 * inplen + off = 0 + if inplen >= need: + if have: + for i in range(need): + ctx.buffer[have + i] = inp[i] + RMD160Transform(ctx.state, ctx.buffer) + off = need + have = 0 + while off + 64 <= inplen: + RMD160Transform(ctx.state, inp[off:]) #<--- + off += 64 + if off < inplen: + # memcpy(ctx->buffer + have, input+off, len-off) + for i in range(inplen - off): + ctx.buffer[have + i] = inp[off + i] + +def RMD160Final(ctx): + size = struct.pack("= self.n: + return self.jacobian_multiply(a, n % self.n, secret) + half = self.jacobian_multiply(a, n // 2, secret) + half_sq = self.jacobian_double(half) + if secret: + # A constant-time implementation + half_sq_a = self.jacobian_add(half_sq, a) + if n % 2 == 0: + result = half_sq + if n % 2 == 1: + result = half_sq_a + return result + else: + if n % 2 == 0: + return half_sq + return self.jacobian_add(half_sq, a) + + + def jacobian_shamir(self, a, n, b, m): + ab = self.jacobian_add(a, b) + if n < 0 or n >= self.n: + n %= self.n + if m < 0 or m >= self.n: + m %= self.n + res = 0, 0, 1 # point on infinity + for i in range(self.n_length - 1, -1, -1): + res = self.jacobian_double(res) + has_n = n & (1 << i) + has_m = m & (1 << i) + if has_n: + if has_m == 0: + res = self.jacobian_add(res, a) + if has_m != 0: + res = self.jacobian_add(res, ab) + else: + if has_m == 0: + res = self.jacobian_add(res, (0, 0, 1)) # Try not to leak + if has_m != 0: + res = self.jacobian_add(res, b) + return res + + + def fast_multiply(self, a, n, secret=False): + return self.from_jacobian(self.jacobian_multiply(self.to_jacobian(a), n, secret)) + + + def fast_add(self, a, b): + return self.from_jacobian(self.jacobian_add(self.to_jacobian(a), self.to_jacobian(b))) + + + def fast_shamir(self, a, n, b, m): + return self.from_jacobian(self.jacobian_shamir(self.to_jacobian(a), n, self.to_jacobian(b), m)) + + + def is_on_curve(self, a): + x, y = a + # Simple arithmetic check + if (pow(x, 3, self.p) + self.a * x + self.b) % self.p != y * y % self.p: + return False + # nP = point-at-infinity + return self.isinf(self.jacobian_multiply(self.to_jacobian(a), self.n)) diff --git a/src/lib/sslcrypto/fallback/_util.py b/src/lib/sslcrypto/fallback/_util.py new file mode 100644 index 00000000..2236ebee --- /dev/null +++ b/src/lib/sslcrypto/fallback/_util.py @@ -0,0 +1,79 @@ +def int_to_bytes(raw, length): + data = [] + for _ in range(length): + data.append(raw % 256) + raw //= 256 + return bytes(data[::-1]) + + +def bytes_to_int(data): + raw = 0 + for byte in data: + raw = raw * 256 + byte + return raw + + +def legendre(a, p): + res = pow(a, (p - 1) // 2, p) + if res == p - 1: + return -1 + else: + return res + + +def inverse(a, n): + if a == 0: + return 0 + lm, hm = 1, 0 + low, high = a % n, n + while low > 1: + r = high // low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return lm % n + + +def square_root_mod_prime(n, p): + if n == 0: + return 0 + if p == 2: + return n # We should never get here but it might be useful + if legendre(n, p) != 1: + raise ValueError("No square root") + # Optimizations + if p % 4 == 3: + return pow(n, (p + 1) // 4, p) + # 1. By factoring out powers of 2, find Q and S such that p - 1 = + # Q * 2 ** S with Q odd + q = p - 1 + s = 0 + while q % 2 == 0: + q //= 2 + s += 1 + # 2. Search for z in Z/pZ which is a quadratic non-residue + z = 1 + while legendre(z, p) != -1: + z += 1 + m, c, t, r = s, pow(z, q, p), pow(n, q, p), pow(n, (q + 1) // 2, p) + while True: + if t == 0: + return 0 + elif t == 1: + return r + # Use repeated squaring to find the least i, 0 < i < M, such + # that t ** (2 ** i) = 1 + t_sq = t + i = 0 + for i in range(1, m): + t_sq = t_sq * t_sq % p + if t_sq == 1: + break + else: + raise ValueError("Should never get here") + # Let b = c ** (2 ** (m - i - 1)) + b = pow(c, 2 ** (m - i - 1), p) + m = i + c = b * b % p + t = t * b * b % p + r = r * b % p + return r diff --git a/src/lib/sslcrypto/fallback/aes.py b/src/lib/sslcrypto/fallback/aes.py new file mode 100644 index 00000000..e168bf34 --- /dev/null +++ b/src/lib/sslcrypto/fallback/aes.py @@ -0,0 +1,101 @@ +import os +import pyaes +from .._aes import AES + + +__all__ = ["aes"] + +class AESBackend: + def _get_algo_cipher_type(self, algo): + if not algo.startswith("aes-") or algo.count("-") != 2: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + key_length, cipher_type = algo[4:].split("-") + if key_length not in ("128", "192", "256"): + raise ValueError("Unknown cipher algorithm {}".format(algo)) + if cipher_type not in ("cbc", "ctr", "cfb", "ofb"): + raise ValueError("Unknown cipher algorithm {}".format(algo)) + return cipher_type + + + def is_algo_supported(self, algo): + try: + self._get_algo_cipher_type(algo) + return True + except ValueError: + return False + + + def random(self, length): + return os.urandom(length) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + cipher_type = self._get_algo_cipher_type(algo) + + # Generate random IV + iv = os.urandom(16) + + if cipher_type == "cbc": + cipher = pyaes.AESModeOfOperationCBC(key, iv=iv) + elif cipher_type == "ctr": + # The IV is actually a counter, not an IV but it does almost the + # same. Notice: pyaes always uses 1 as initial counter! Make sure + # not to call pyaes directly. + + # We kinda do two conversions here: from byte array to int here, and + # from int to byte array in pyaes internals. It's possible to fix that + # but I didn't notice any performance changes so I'm keeping clean code. + iv_int = 0 + for byte in iv: + iv_int = (iv_int * 256) + byte + counter = pyaes.Counter(iv_int) + cipher = pyaes.AESModeOfOperationCTR(key, counter=counter) + elif cipher_type == "cfb": + # Change segment size from default 8 bytes to 16 bytes for OpenSSL + # compatibility + cipher = pyaes.AESModeOfOperationCFB(key, iv, segment_size=16) + elif cipher_type == "ofb": + cipher = pyaes.AESModeOfOperationOFB(key, iv) + + encrypter = pyaes.Encrypter(cipher) + ciphertext = encrypter.feed(data) + ciphertext += encrypter.feed() + return ciphertext, iv + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + cipher_type = self._get_algo_cipher_type(algo) + + if cipher_type == "cbc": + cipher = pyaes.AESModeOfOperationCBC(key, iv=iv) + elif cipher_type == "ctr": + # The IV is actually a counter, not an IV but it does almost the + # same. Notice: pyaes always uses 1 as initial counter! Make sure + # not to call pyaes directly. + + # We kinda do two conversions here: from byte array to int here, and + # from int to byte array in pyaes internals. It's possible to fix that + # but I didn't notice any performance changes so I'm keeping clean code. + iv_int = 0 + for byte in iv: + iv_int = (iv_int * 256) + byte + counter = pyaes.Counter(iv_int) + cipher = pyaes.AESModeOfOperationCTR(key, counter=counter) + elif cipher_type == "cfb": + # Change segment size from default 8 bytes to 16 bytes for OpenSSL + # compatibility + cipher = pyaes.AESModeOfOperationCFB(key, iv, segment_size=16) + elif cipher_type == "ofb": + cipher = pyaes.AESModeOfOperationOFB(key, iv) + + decrypter = pyaes.Decrypter(cipher) + data = decrypter.feed(ciphertext) + data += decrypter.feed() + return data + + + def get_backend(self): + return "fallback" + + +aes = AES(AESBackend()) diff --git a/src/lib/sslcrypto/fallback/ecc.py b/src/lib/sslcrypto/fallback/ecc.py new file mode 100644 index 00000000..3a438d4d --- /dev/null +++ b/src/lib/sslcrypto/fallback/ecc.py @@ -0,0 +1,371 @@ +import hmac +import os +from ._jacobian import JacobianCurve +from .._ecc import ECC +from .aes import aes +from ._util import int_to_bytes, bytes_to_int, inverse, square_root_mod_prime + + +# pylint: disable=line-too-long +CURVES = { + # nid: (p, n, a, b, (Gx, Gy)), + 704: ( + # secp112r1 + 0xDB7C2ABF62E35E668076BEAD208B, + 0xDB7C2ABF62E35E7628DFAC6561C5, + 0xDB7C2ABF62E35E668076BEAD2088, + 0x659EF8BA043916EEDE8911702B22, + ( + 0x09487239995A5EE76B55F9C2F098, + 0xA89CE5AF8724C0A23E0E0FF77500 + ) + ), + 705: ( + # secp112r2 + 0xDB7C2ABF62E35E668076BEAD208B, + 0x36DF0AAFD8B8D7597CA10520D04B, + 0x6127C24C05F38A0AAAF65C0EF02C, + 0x51DEF1815DB5ED74FCC34C85D709, + ( + 0x4BA30AB5E892B4E1649DD0928643, + 0xADCD46F5882E3747DEF36E956E97 + ) + ), + 706: ( + # secp128r1 + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFE0000000075A30D1B9038A115, + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC, + 0xE87579C11079F43DD824993C2CEE5ED3, + ( + 0x161FF7528B899B2D0C28607CA52C5B86, + 0xCF5AC8395BAFEB13C02DA292DDED7A83 + ) + ), + 707: ( + # secp128r2 + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0x3FFFFFFF7FFFFFFFBE0024720613B5A3, + 0xD6031998D1B3BBFEBF59CC9BBFF9AEE1, + 0x5EEEFCA380D02919DC2C6558BB6D8A5D, + ( + 0x7B6AA5D85E572983E6FB32A7CDEBC140, + 0x27B6916A894D3AEE7106FE805FC34B44 + ) + ), + 708: ( + # secp160k1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, + 0x0100000000000000000001B8FA16DFAB9ACA16B6B3, + 0, + 7, + ( + 0x3B4C382CE37AA192A4019E763036F4F5DD4D7EBB, + 0x938CF935318FDCED6BC28286531733C3F03C4FEE + ) + ), + 709: ( + # secp160r1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF, + 0x0100000000000000000001F4C8F927AED3CA752257, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC, + 0x001C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45, + ( + 0x4A96B5688EF573284664698968C38BB913CBFC82, + 0x23A628553168947D59DCC912042351377AC5FB32 + ) + ), + 710: ( + # secp160r2 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, + 0x0100000000000000000000351EE786A818F3A1A16B, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC70, + 0x00B4E134D3FB59EB8BAB57274904664D5AF50388BA, + ( + 0x52DCB034293A117E1F4FF11B30F7199D3144CE6D, + 0xFEAFFEF2E331F296E071FA0DF9982CFEA7D43F2E + ) + ), + 711: ( + # secp192k1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37, + 0xFFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D, + 0, + 3, + ( + 0xDB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D, + 0x9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D + ) + ), + 409: ( + # prime192v1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC, + 0x64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1, + ( + 0x188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012, + 0x07192B95FFC8DA78631011ED6B24CDD573F977A11E794811 + ) + ), + 712: ( + # secp224k1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFE56D, + 0x010000000000000000000000000001DCE8D2EC6184CAF0A971769FB1F7, + 0, + 5, + ( + 0xA1455B334DF099DF30FC28A169A467E9E47075A90F7E650EB6B7A45C, + 0x7E089FED7FBA344282CAFBD6F7E319F7C0B0BD59E2CA4BDB556D61A5 + ) + ), + 713: ( + # secp224r1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE, + 0xB4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4, + ( + 0xB70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21, + 0xBD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34 + ) + ), + 714: ( + # secp256k1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, + 0, + 7, + ( + 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, + 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 + ) + ), + 415: ( + # prime256v1 + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, + 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B, + ( + 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, + 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5 + ) + ), + 715: ( + # secp384r1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC, + 0xB3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF, + ( + 0xAA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7, + 0x3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F + ) + ), + 716: ( + # secp521r1 + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC, + 0x0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00, + ( + 0x00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66, + 0x011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650 + ) + ) +} +# pylint: enable=line-too-long + + +class EllipticCurveBackend: + def __init__(self, nid): + self.p, self.n, self.a, self.b, self.g = CURVES[nid] + self.jacobian = JacobianCurve(*CURVES[nid]) + + self.public_key_length = (len(bin(self.p).replace("0b", "")) + 7) // 8 + self.order_bitlength = len(bin(self.n).replace("0b", "")) + + + def _int_to_bytes(self, raw, len=None): + return int_to_bytes(raw, len or self.public_key_length) + + + def decompress_point(self, public_key): + # Parse & load data + x = bytes_to_int(public_key[1:]) + # Calculate Y + y_square = (pow(x, 3, self.p) + self.a * x + self.b) % self.p + try: + y = square_root_mod_prime(y_square, self.p) + except Exception: + raise ValueError("Invalid public key") from None + if y % 2 != public_key[0] - 0x02: + y = self.p - y + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def new_private_key(self): + while True: + private_key = os.urandom(self.public_key_length) + if bytes_to_int(private_key) >= self.n: + continue + return private_key + + + def private_to_public(self, private_key): + raw = bytes_to_int(private_key) + x, y = self.jacobian.fast_multiply(self.g, raw) + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def ecdh(self, private_key, public_key): + x, y = public_key + x, y = bytes_to_int(x), bytes_to_int(y) + private_key = bytes_to_int(private_key) + x, _ = self.jacobian.fast_multiply((x, y), private_key, secret=True) + return self._int_to_bytes(x) + + + def _subject_to_int(self, subject): + return bytes_to_int(subject[:(self.order_bitlength + 7) // 8]) + + + def sign(self, subject, raw_private_key, recoverable, is_compressed, entropy): + z = self._subject_to_int(subject) + private_key = bytes_to_int(raw_private_key) + k = bytes_to_int(entropy) + + # Fix k length to prevent Minerva. Increasing multiplier by a + # multiple of order doesn't break anything. This fix was ported + # from python-ecdsa + ks = k + self.n + kt = ks + self.n + ks_len = len(bin(ks).replace("0b", "")) // 8 + kt_len = len(bin(kt).replace("0b", "")) // 8 + if ks_len == kt_len: + k = kt + else: + k = ks + px, py = self.jacobian.fast_multiply(self.g, k, secret=True) + + r = px % self.n + if r == 0: + # Invalid k + raise ValueError("Invalid k") + + s = (inverse(k, self.n) * (z + (private_key * r))) % self.n + if s == 0: + # Invalid k + raise ValueError("Invalid k") + + inverted = False + if s * 2 >= self.n: + s = self.n - s + inverted = True + rs_buf = self._int_to_bytes(r) + self._int_to_bytes(s) + + if recoverable: + recid = (py % 2) ^ inverted + recid += 2 * int(px // self.n) + if is_compressed: + return bytes([31 + recid]) + rs_buf + else: + if recid >= 4: + raise ValueError("Too big recovery ID, use compressed address instead") + return bytes([27 + recid]) + rs_buf + else: + return rs_buf + + + def recover(self, signature, subject): + z = self._subject_to_int(subject) + + recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 + r = bytes_to_int(signature[1:self.public_key_length + 1]) + s = bytes_to_int(signature[self.public_key_length + 1:]) + + # Verify bounds + if not 0 <= recid < 2 * (self.p // self.n + 1): + raise ValueError("Invalid recovery ID") + if r >= self.n: + raise ValueError("r is out of bounds") + if s >= self.n: + raise ValueError("s is out of bounds") + + rinv = inverse(r, self.n) + u1 = (-z * rinv) % self.n + u2 = (s * rinv) % self.n + + # Recover R + rx = r + (recid // 2) * self.n + if rx >= self.p: + raise ValueError("Rx is out of bounds") + + # Almost copied from decompress_point + ry_square = (pow(rx, 3, self.p) + self.a * rx + self.b) % self.p + try: + ry = square_root_mod_prime(ry_square, self.p) + except Exception: + raise ValueError("Invalid recovered public key") from None + + # Ensure the point is correct + if ry % 2 != recid % 2: + # Fix Ry sign + ry = self.p - ry + + x, y = self.jacobian.fast_shamir(self.g, u1, (rx, ry), u2) + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def verify(self, signature, subject, public_key): + z = self._subject_to_int(subject) + + r = bytes_to_int(signature[:self.public_key_length]) + s = bytes_to_int(signature[self.public_key_length:]) + + # Verify bounds + if r >= self.n: + raise ValueError("r is out of bounds") + if s >= self.n: + raise ValueError("s is out of bounds") + + public_key = [bytes_to_int(c) for c in public_key] + + # Ensure that the public key is correct + if not self.jacobian.is_on_curve(public_key): + raise ValueError("Public key is not on curve") + + sinv = inverse(s, self.n) + u1 = (z * sinv) % self.n + u2 = (r * sinv) % self.n + + x1, _ = self.jacobian.fast_shamir(self.g, u1, public_key, u2) + if r != x1 % self.n: + raise ValueError("Invalid signature") + + return True + + + def derive_child(self, seed, child): + # Round 1 + h = hmac.new(key=b"Bitcoin seed", msg=seed, digestmod="sha512").digest() + private_key1 = h[:32] + x, y = self.private_to_public(private_key1) + public_key1 = bytes([0x02 + (y[-1] % 2)]) + x + private_key1 = bytes_to_int(private_key1) + + # Round 2 + msg = public_key1 + self._int_to_bytes(child, 4) + h = hmac.new(key=h[32:], msg=msg, digestmod="sha512").digest() + private_key2 = bytes_to_int(h[:32]) + + return self._int_to_bytes((private_key1 + private_key2) % self.n) + + + @classmethod + def get_backend(cls): + return "fallback" + + +ecc = ECC(EllipticCurveBackend, aes) diff --git a/src/lib/sslcrypto/fallback/rsa.py b/src/lib/sslcrypto/fallback/rsa.py new file mode 100644 index 00000000..54b8d2cb --- /dev/null +++ b/src/lib/sslcrypto/fallback/rsa.py @@ -0,0 +1,8 @@ +# pylint: disable=too-few-public-methods + +class RSA: + def get_backend(self): + return "fallback" + + +rsa = RSA() diff --git a/src/lib/sslcrypto/openssl/__init__.py b/src/lib/sslcrypto/openssl/__init__.py new file mode 100644 index 00000000..a32ae692 --- /dev/null +++ b/src/lib/sslcrypto/openssl/__init__.py @@ -0,0 +1,3 @@ +from .aes import aes +from .ecc import ecc +from .rsa import rsa diff --git a/src/lib/sslcrypto/openssl/aes.py b/src/lib/sslcrypto/openssl/aes.py new file mode 100644 index 00000000..c58451d5 --- /dev/null +++ b/src/lib/sslcrypto/openssl/aes.py @@ -0,0 +1,156 @@ +import ctypes +import threading +from .._aes import AES +from ..fallback.aes import aes as fallback_aes +from .library import lib, openssl_backend + + +# Initialize functions +try: + lib.EVP_CIPHER_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +except AttributeError: + pass +lib.EVP_get_cipherbyname.restype = ctypes.POINTER(ctypes.c_char) + + +thread_local = threading.local() + + +class Context: + def __init__(self, ptr, do_free): + self.lib = lib + self.ptr = ptr + self.do_free = do_free + + + def __del__(self): + if self.do_free: + self.lib.EVP_CIPHER_CTX_free(self.ptr) + + +class AESBackend: + ALGOS = ( + "aes-128-cbc", "aes-192-cbc", "aes-256-cbc", + "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", + "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", + "aes-128-ofb", "aes-192-ofb", "aes-256-ofb" + ) + + def __init__(self): + self.is_supported_ctx_new = hasattr(lib, "EVP_CIPHER_CTX_new") + self.is_supported_ctx_reset = hasattr(lib, "EVP_CIPHER_CTX_reset") + + + def _get_ctx(self): + if not hasattr(thread_local, "ctx"): + if self.is_supported_ctx_new: + thread_local.ctx = Context(lib.EVP_CIPHER_CTX_new(), True) + else: + # 1 KiB ought to be enough for everybody. We don't know the real + # size of the context buffer because we are unsure about padding and + # pointer size + thread_local.ctx = Context(ctypes.create_string_buffer(1024), False) + return thread_local.ctx.ptr + + + def get_backend(self): + return openssl_backend + + + def _get_cipher(self, algo): + if algo not in self.ALGOS: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + cipher = lib.EVP_get_cipherbyname(algo.encode()) + if not cipher: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + return cipher + + + def is_algo_supported(self, algo): + try: + self._get_cipher(algo) + return True + except ValueError: + return False + + + def random(self, length): + entropy = ctypes.create_string_buffer(length) + lib.RAND_bytes(entropy, length) + return bytes(entropy) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + # Initialize context + ctx = self._get_ctx() + if not self.is_supported_ctx_new: + lib.EVP_CIPHER_CTX_init(ctx) + try: + lib.EVP_EncryptInit_ex(ctx, self._get_cipher(algo), None, None, None) + + # Generate random IV + iv_length = 16 + iv = self.random(iv_length) + + # Set key and IV + lib.EVP_EncryptInit_ex(ctx, None, None, key, iv) + + # Actually encrypt + block_size = 16 + output = ctypes.create_string_buffer((len(data) // block_size + 1) * block_size) + output_len = ctypes.c_int() + + if not lib.EVP_CipherUpdate(ctx, output, ctypes.byref(output_len), data, len(data)): + raise ValueError("Could not feed cipher with data") + + new_output = ctypes.byref(output, output_len.value) + output_len2 = ctypes.c_int() + if not lib.EVP_CipherFinal_ex(ctx, new_output, ctypes.byref(output_len2)): + raise ValueError("Could not finalize cipher") + + ciphertext = output[:output_len.value + output_len2.value] + return ciphertext, iv + finally: + if self.is_supported_ctx_reset: + lib.EVP_CIPHER_CTX_reset(ctx) + else: + lib.EVP_CIPHER_CTX_cleanup(ctx) + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + # Initialize context + ctx = self._get_ctx() + if not self.is_supported_ctx_new: + lib.EVP_CIPHER_CTX_init(ctx) + try: + lib.EVP_DecryptInit_ex(ctx, self._get_cipher(algo), None, None, None) + + # Make sure IV length is correct + iv_length = 16 + if len(iv) != iv_length: + raise ValueError("Expected IV to be {} bytes, got {} bytes".format(iv_length, len(iv))) + + # Set key and IV + lib.EVP_DecryptInit_ex(ctx, None, None, key, iv) + + # Actually decrypt + output = ctypes.create_string_buffer(len(ciphertext)) + output_len = ctypes.c_int() + + if not lib.EVP_DecryptUpdate(ctx, output, ctypes.byref(output_len), ciphertext, len(ciphertext)): + raise ValueError("Could not feed decipher with ciphertext") + + new_output = ctypes.byref(output, output_len.value) + output_len2 = ctypes.c_int() + if not lib.EVP_DecryptFinal_ex(ctx, new_output, ctypes.byref(output_len2)): + raise ValueError("Could not finalize decipher") + + return output[:output_len.value + output_len2.value] + finally: + if self.is_supported_ctx_reset: + lib.EVP_CIPHER_CTX_reset(ctx) + else: + lib.EVP_CIPHER_CTX_cleanup(ctx) + + +aes = AES(AESBackend(), fallback_aes) diff --git a/src/lib/sslcrypto/openssl/ecc.py b/src/lib/sslcrypto/openssl/ecc.py new file mode 100644 index 00000000..4284646b --- /dev/null +++ b/src/lib/sslcrypto/openssl/ecc.py @@ -0,0 +1,551 @@ +import ctypes +import hmac +import threading +from .._ecc import ECC +from .aes import aes +from .library import lib, openssl_backend + + +# Initialize functions +lib.BN_new.restype = ctypes.POINTER(ctypes.c_char) +lib.BN_bin2bn.restype = ctypes.POINTER(ctypes.c_char) +lib.BN_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_GROUP_new_by_curve_name.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_KEY_new_by_curve_name.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_POINT_new.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_KEY_get0_private_key.restype = ctypes.POINTER(ctypes.c_char) +lib.EVP_PKEY_new.restype = ctypes.POINTER(ctypes.c_char) +try: + lib.EVP_PKEY_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +except AttributeError: + pass + + +thread_local = threading.local() + + +class BN: + # BN_CTX + class Context: + def __init__(self): + self.ptr = lib.BN_CTX_new() + self.lib = lib # For finalizer + + + def __del__(self): + self.lib.BN_CTX_free(self.ptr) + + + @classmethod + def get(cls): + # Get thread-safe contexf + if not hasattr(thread_local, "bn_ctx"): + thread_local.bn_ctx = cls() + return thread_local.bn_ctx.ptr + + + def __init__(self, value=None, link_only=False): + if link_only: + self.bn = value + self._free = False + else: + if value is None: + self.bn = lib.BN_new() + self._free = True + elif isinstance(value, bytes): + self.bn = lib.BN_bin2bn(value, len(value), None) + self._free = True + else: + self.bn = lib.BN_new() + lib.BN_clear(self.bn) + lib.BN_add_word(self.bn, value) + self._free = True + + + def __del__(self): + if self._free: + lib.BN_free(self.bn) + + + def bytes(self, length=None): + buf = ctypes.create_string_buffer((len(self) + 7) // 8) + lib.BN_bn2bin(self.bn, buf) + buf = bytes(buf) + if length is None: + return buf + else: + if length < len(buf): + raise ValueError("Too little space for BN") + return b"\x00" * (length - len(buf)) + buf + + def __int__(self): + value = 0 + for byte in self.bytes(): + value = value * 256 + byte + return value + + def __len__(self): + return lib.BN_num_bits(self.bn) + + + def inverse(self, modulo): + result = BN() + if not lib.BN_mod_inverse(result.bn, self.bn, modulo.bn, BN.Context.get()): + raise ValueError("Could not compute inverse") + return result + + + def __floordiv__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only divide BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_div(result.bn, None, self.bn, other.bn, BN.Context.get()): + raise ZeroDivisionError("Division by zero") + return result + + def __mod__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only divide BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_div(None, result.bn, self.bn, other.bn, BN.Context.get()): + raise ZeroDivisionError("Division by zero") + return result + + def __add__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only sum BN's, not BN and {}".format(other)) + result = BN() + if not lib.BN_add(result.bn, self.bn, other.bn): + raise ValueError("Could not sum two BN's") + return result + + def __sub__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only subtract BN's, not BN and {}".format(other)) + result = BN() + if not lib.BN_sub(result.bn, self.bn, other.bn): + raise ValueError("Could not subtract BN from BN") + return result + + def __mul__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only multiply BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_mul(result.bn, self.bn, other.bn, BN.Context.get()): + raise ValueError("Could not multiply two BN's") + return result + + def __neg__(self): + return BN(0) - self + + + # A dirty but nice way to update current BN and free old BN at the same time + def __imod__(self, other): + res = self % other + self.bn, res.bn = res.bn, self.bn + return self + def __iadd__(self, other): + res = self + other + self.bn, res.bn = res.bn, self.bn + return self + def __isub__(self, other): + res = self - other + self.bn, res.bn = res.bn, self.bn + return self + def __imul__(self, other): + res = self * other + self.bn, res.bn = res.bn, self.bn + return self + + + def cmp(self, other): + if not isinstance(other, BN): + raise TypeError("Can only compare BN with BN, not {}".format(other)) + return lib.BN_cmp(self.bn, other.bn) + + def __eq__(self, other): + return self.cmp(other) == 0 + def __lt__(self, other): + return self.cmp(other) < 0 + def __gt__(self, other): + return self.cmp(other) > 0 + def __ne__(self, other): + return self.cmp(other) != 0 + def __le__(self, other): + return self.cmp(other) <= 0 + def __ge__(self, other): + return self.cmp(other) >= 0 + + + def __repr__(self): + return "".format(int(self)) + + def __str__(self): + return str(int(self)) + + +class EllipticCurveBackend: + def __init__(self, nid): + self.lib = lib # For finalizer + self.nid = nid + self.group = lib.EC_GROUP_new_by_curve_name(self.nid) + if not self.group: + raise ValueError("The curve is not supported by OpenSSL") + + self.order = BN() + self.p = BN() + bn_ctx = BN.Context.get() + lib.EC_GROUP_get_order(self.group, self.order.bn, bn_ctx) + lib.EC_GROUP_get_curve_GFp(self.group, self.p.bn, None, None, bn_ctx) + + self.public_key_length = (len(self.p) + 7) // 8 + + self.is_supported_evp_pkey_ctx = hasattr(lib, "EVP_PKEY_CTX_new") + + + def __del__(self): + self.lib.EC_GROUP_free(self.group) + + + def _private_key_to_ec_key(self, private_key): + eckey = lib.EC_KEY_new_by_curve_name(self.nid) + if not eckey: + raise ValueError("Failed to allocate EC_KEY") + private_key = BN(private_key) + if not lib.EC_KEY_set_private_key(eckey, private_key.bn): + lib.EC_KEY_free(eckey) + raise ValueError("Invalid private key") + return eckey, private_key + + + def _public_key_to_point(self, public_key): + x = BN(public_key[0]) + y = BN(public_key[1]) + # EC_KEY_set_public_key_affine_coordinates is not supported by + # OpenSSL 1.0.0 so we can't use it + point = lib.EC_POINT_new(self.group) + if not lib.EC_POINT_set_affine_coordinates_GFp(self.group, point, x.bn, y.bn, BN.Context.get()): + raise ValueError("Could not set public key affine coordinates") + return point + + + def _public_key_to_ec_key(self, public_key): + eckey = lib.EC_KEY_new_by_curve_name(self.nid) + if not eckey: + raise ValueError("Failed to allocate EC_KEY") + try: + # EC_KEY_set_public_key_affine_coordinates is not supported by + # OpenSSL 1.0.0 so we can't use it + point = self._public_key_to_point(public_key) + if not lib.EC_KEY_set_public_key(eckey, point): + raise ValueError("Could not set point") + lib.EC_POINT_free(point) + return eckey + except Exception as e: + lib.EC_KEY_free(eckey) + raise e from None + + + def _point_to_affine(self, point): + # Convert to affine coordinates + x = BN() + y = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, point, x.bn, y.bn, BN.Context.get()) != 1: + raise ValueError("Failed to convert public key to affine coordinates") + # Convert to binary + if (len(x) + 7) // 8 > self.public_key_length: + raise ValueError("Public key X coordinate is too large") + if (len(y) + 7) // 8 > self.public_key_length: + raise ValueError("Public key Y coordinate is too large") + return x.bytes(self.public_key_length), y.bytes(self.public_key_length) + + + def decompress_point(self, public_key): + point = lib.EC_POINT_new(self.group) + if not point: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_oct2point(self.group, point, public_key, len(public_key), BN.Context.get()): + raise ValueError("Invalid compressed public key") + return self._point_to_affine(point) + finally: + lib.EC_POINT_free(point) + + + def new_private_key(self): + # Create random key + eckey = lib.EC_KEY_new_by_curve_name(self.nid) + lib.EC_KEY_generate_key(eckey) + # To big integer + private_key = BN(lib.EC_KEY_get0_private_key(eckey), link_only=True) + # To binary + private_key_buf = private_key.bytes() + # Cleanup + lib.EC_KEY_free(eckey) + return private_key_buf + + + def private_to_public(self, private_key): + eckey, private_key = self._private_key_to_ec_key(private_key) + try: + # Derive public key + point = lib.EC_POINT_new(self.group) + try: + if not lib.EC_POINT_mul(self.group, point, private_key.bn, None, None, BN.Context.get()): + raise ValueError("Failed to derive public key") + return self._point_to_affine(point) + finally: + lib.EC_POINT_free(point) + finally: + lib.EC_KEY_free(eckey) + + + def ecdh(self, private_key, public_key): + if not self.is_supported_evp_pkey_ctx: + # Use ECDH_compute_key instead + # Create EC_KEY from private key + eckey, _ = self._private_key_to_ec_key(private_key) + try: + # Create EC_POINT from public key + point = self._public_key_to_point(public_key) + try: + key = ctypes.create_string_buffer(self.public_key_length) + if lib.ECDH_compute_key(key, self.public_key_length, point, eckey, None) == -1: + raise ValueError("Could not compute shared secret") + return bytes(key) + finally: + lib.EC_POINT_free(point) + finally: + lib.EC_KEY_free(eckey) + + # Private key: + # Create EC_KEY + eckey, _ = self._private_key_to_ec_key(private_key) + try: + # Convert to EVP_PKEY + pkey = lib.EVP_PKEY_new() + if not pkey: + raise ValueError("Could not create private key object") + try: + lib.EVP_PKEY_set1_EC_KEY(pkey, eckey) + + # Public key: + # Create EC_KEY + peer_eckey = self._public_key_to_ec_key(public_key) + try: + # Convert to EVP_PKEY + peer_pkey = lib.EVP_PKEY_new() + if not peer_pkey: + raise ValueError("Could not create public key object") + try: + lib.EVP_PKEY_set1_EC_KEY(peer_pkey, peer_eckey) + + # Create context + ctx = lib.EVP_PKEY_CTX_new(pkey, None) + if not ctx: + raise ValueError("Could not create EVP context") + try: + if lib.EVP_PKEY_derive_init(ctx) != 1: + raise ValueError("Could not initialize key derivation") + if not lib.EVP_PKEY_derive_set_peer(ctx, peer_pkey): + raise ValueError("Could not set peer") + + # Actually derive + key_len = ctypes.c_int(0) + lib.EVP_PKEY_derive(ctx, None, ctypes.byref(key_len)) + key = ctypes.create_string_buffer(key_len.value) + lib.EVP_PKEY_derive(ctx, key, ctypes.byref(key_len)) + + return bytes(key) + finally: + lib.EVP_PKEY_CTX_free(ctx) + finally: + lib.EVP_PKEY_free(peer_pkey) + finally: + lib.EC_KEY_free(peer_eckey) + finally: + lib.EVP_PKEY_free(pkey) + finally: + lib.EC_KEY_free(eckey) + + + def _subject_to_bn(self, subject): + return BN(subject[:(len(self.order) + 7) // 8]) + + + def sign(self, subject, private_key, recoverable, is_compressed, entropy): + z = self._subject_to_bn(subject) + private_key = BN(private_key) + k = BN(entropy) + + rp = lib.EC_POINT_new(self.group) + bn_ctx = BN.Context.get() + try: + # Fix Minerva + k1 = k + self.order + k2 = k1 + self.order + if len(k1) == len(k2): + k = k2 + else: + k = k1 + if not lib.EC_POINT_mul(self.group, rp, k.bn, None, None, bn_ctx): + raise ValueError("Could not generate R") + # Convert to affine coordinates + rx = BN() + ry = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, rp, rx.bn, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to convert R to affine coordinates") + r = rx % self.order + if r == BN(0): + raise ValueError("Invalid k") + # Calculate s = k^-1 * (z + r * private_key) mod n + s = (k.inverse(self.order) * (z + r * private_key)) % self.order + if s == BN(0): + raise ValueError("Invalid k") + + inverted = False + if s * BN(2) >= self.order: + s = self.order - s + inverted = True + + r_buf = r.bytes(self.public_key_length) + s_buf = s.bytes(self.public_key_length) + if recoverable: + # Generate recid + recid = int(ry % BN(2)) ^ inverted + # The line below is highly unlikely to matter in case of + # secp256k1 but might make sense for other curves + recid += 2 * int(rx // self.order) + if is_compressed: + return bytes([31 + recid]) + r_buf + s_buf + else: + if recid >= 4: + raise ValueError("Too big recovery ID, use compressed address instead") + return bytes([27 + recid]) + r_buf + s_buf + else: + return r_buf + s_buf + finally: + lib.EC_POINT_free(rp) + + + def recover(self, signature, subject): + recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 + r = BN(signature[1:self.public_key_length + 1]) + s = BN(signature[self.public_key_length + 1:]) + + # Verify bounds + if r >= self.order: + raise ValueError("r is out of bounds") + if s >= self.order: + raise ValueError("s is out of bounds") + + bn_ctx = BN.Context.get() + + z = self._subject_to_bn(subject) + + rinv = r.inverse(self.order) + u1 = (-z * rinv) % self.order + u2 = (s * rinv) % self.order + + # Recover R + rx = r + BN(recid // 2) * self.order + if rx >= self.p: + raise ValueError("Rx is out of bounds") + rp = lib.EC_POINT_new(self.group) + if not rp: + raise ValueError("Could not create R") + try: + init_buf = b"\x02" + rx.bytes(self.public_key_length) + if not lib.EC_POINT_oct2point(self.group, rp, init_buf, len(init_buf), bn_ctx): + raise ValueError("Could not use Rx to initialize point") + ry = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, rp, None, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to convert R to affine coordinates") + if int(ry % BN(2)) != recid % 2: + # Fix Ry sign + ry = self.p - ry + if lib.EC_POINT_set_affine_coordinates_GFp(self.group, rp, rx.bn, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to update R coordinates") + + # Recover public key + result = lib.EC_POINT_new(self.group) + if not result: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_mul(self.group, result, u1.bn, rp, u2.bn, bn_ctx): + raise ValueError("Could not recover public key") + return self._point_to_affine(result) + finally: + lib.EC_POINT_free(result) + finally: + lib.EC_POINT_free(rp) + + + def verify(self, signature, subject, public_key): + r_raw = signature[:self.public_key_length] + r = BN(r_raw) + s = BN(signature[self.public_key_length:]) + if r >= self.order: + raise ValueError("r is out of bounds") + if s >= self.order: + raise ValueError("s is out of bounds") + + bn_ctx = BN.Context.get() + + z = self._subject_to_bn(subject) + + pub_p = lib.EC_POINT_new(self.group) + if not pub_p: + raise ValueError("Could not create public key point") + try: + init_buf = b"\x04" + public_key[0] + public_key[1] + if not lib.EC_POINT_oct2point(self.group, pub_p, init_buf, len(init_buf), bn_ctx): + raise ValueError("Could initialize point") + + sinv = s.inverse(self.order) + u1 = (z * sinv) % self.order + u2 = (r * sinv) % self.order + + # Recover public key + result = lib.EC_POINT_new(self.group) + if not result: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_mul(self.group, result, u1.bn, pub_p, u2.bn, bn_ctx): + raise ValueError("Could not recover public key") + if BN(self._point_to_affine(result)[0]) % self.order != r: + raise ValueError("Invalid signature") + return True + finally: + lib.EC_POINT_free(result) + finally: + lib.EC_POINT_free(pub_p) + + + def derive_child(self, seed, child): + # Round 1 + h = hmac.new(key=b"Bitcoin seed", msg=seed, digestmod="sha512").digest() + private_key1 = h[:32] + x, y = self.private_to_public(private_key1) + public_key1 = bytes([0x02 + (y[-1] % 2)]) + x + private_key1 = BN(private_key1) + + # Round 2 + child_bytes = [] + for _ in range(4): + child_bytes.append(child & 255) + child >>= 8 + child_bytes = bytes(child_bytes[::-1]) + msg = public_key1 + child_bytes + h = hmac.new(key=h[32:], msg=msg, digestmod="sha512").digest() + private_key2 = BN(h[:32]) + + return ((private_key1 + private_key2) % self.order).bytes(self.public_key_length) + + + @classmethod + def get_backend(cls): + return openssl_backend + + +ecc = ECC(EllipticCurveBackend, aes) diff --git a/src/lib/sslcrypto/openssl/library.py b/src/lib/sslcrypto/openssl/library.py new file mode 100644 index 00000000..8b3e1d2f --- /dev/null +++ b/src/lib/sslcrypto/openssl/library.py @@ -0,0 +1,93 @@ +import os +import sys +import ctypes +import ctypes.util + + +# Disable false-positive _MEIPASS +# pylint: disable=no-member,protected-access + +# Discover OpenSSL library +def discover_paths(): + # Search local files first + if "win" in sys.platform: + # Windows + names = [ + "libeay32.dll" + ] + openssl_paths = [os.path.abspath(path) for path in names] + if hasattr(sys, "_MEIPASS"): + openssl_paths += [os.path.join(sys._MEIPASS, path) for path in openssl_paths] + openssl_paths.append(ctypes.util.find_library("libeay32")) + elif "darwin" in sys.platform: + # Mac OS + names = [ + "libcrypto.dylib", + "libcrypto.1.1.0.dylib", + "libcrypto.1.0.2.dylib", + "libcrypto.1.0.1.dylib", + "libcrypto.1.0.0.dylib", + "libcrypto.0.9.8.dylib" + ] + openssl_paths = [os.path.abspath(path) for path in names] + openssl_paths += names + openssl_paths += [ + "/usr/local/opt/openssl/lib/libcrypto.dylib" + ] + if hasattr(sys, "_MEIPASS") and "RESOURCEPATH" in os.environ: + openssl_paths += [ + os.path.join(os.environ["RESOURCEPATH"], "..", "Frameworks", name) + for name in names + ] + openssl_paths.append(ctypes.util.find_library("ssl")) + else: + # Linux, BSD and such + names = [ + "libcrypto.so", + "libssl.so", + "libcrypto.so.1.1.0", + "libssl.so.1.1.0", + "libcrypto.so.1.0.2", + "libssl.so.1.0.2", + "libcrypto.so.1.0.1", + "libssl.so.1.0.1", + "libcrypto.so.1.0.0", + "libssl.so.1.0.0", + "libcrypto.so.0.9.8", + "libssl.so.0.9.8" + ] + openssl_paths = [os.path.abspath(path) for path in names] + openssl_paths += names + if hasattr(sys, "_MEIPASS"): + openssl_paths += [os.path.join(sys._MEIPASS, path) for path in names] + openssl_paths.append(ctypes.util.find_library("ssl")) + + return openssl_paths + + +def discover_library(): + for path in discover_paths(): + if path: + try: + return ctypes.CDLL(path) + except OSError: + pass + raise OSError("OpenSSL is unavailable") + + +lib = discover_library() + +# Initialize internal state +try: + lib.OPENSSL_add_all_algorithms_conf() +except AttributeError: + pass + +try: + lib.OpenSSL_version.restype = ctypes.c_char_p + openssl_backend = lib.OpenSSL_version(0).decode() +except AttributeError: + lib.SSLeay_version.restype = ctypes.c_char_p + openssl_backend = lib.SSLeay_version(0).decode() + +openssl_backend += " at " + lib._name diff --git a/src/lib/sslcrypto/openssl/rsa.py b/src/lib/sslcrypto/openssl/rsa.py new file mode 100644 index 00000000..afd8b51c --- /dev/null +++ b/src/lib/sslcrypto/openssl/rsa.py @@ -0,0 +1,11 @@ +# pylint: disable=too-few-public-methods + +from .library import openssl_backend + + +class RSA: + def get_backend(self): + return openssl_backend + + +rsa = RSA() diff --git a/src/util/Electrum.py b/src/util/Electrum.py new file mode 100644 index 00000000..112151aa --- /dev/null +++ b/src/util/Electrum.py @@ -0,0 +1,39 @@ +import hashlib +import struct + + +# Electrum, the heck?! + +def bchr(i): + return struct.pack("B", i) + +def encode(val, base, minlen=0): + base, minlen = int(base), int(minlen) + code_string = b"".join([bchr(x) for x in range(256)]) + result = b"" + while val > 0: + index = val % base + result = code_string[index:index + 1] + result + val //= base + return code_string[0:1] * max(minlen - len(result), 0) + result + +def insane_int(x): + x = int(x) + if x < 253: + return bchr(x) + elif x < 65536: + return bchr(253) + encode(x, 256, 2)[::-1] + elif x < 4294967296: + return bchr(254) + encode(x, 256, 4)[::-1] + else: + return bchr(255) + encode(x, 256, 8)[::-1] + + +def magic(message): + return b"\x18Bitcoin Signed Message:\n" + insane_int(len(message)) + message + +def format(message): + return hashlib.sha256(magic(message)).digest() + +def dbl_format(message): + return hashlib.sha256(format(message)).digest() From 958882c1c5cf4730b21b1d72684d3400889133c4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sun, 15 Dec 2019 18:30:42 +0100 Subject: [PATCH 288/483] Revert "Switch to sslcrypto for cryptography tasks (#2338)" This reverts commit fbc7b6fc4f1466bbc7df9b922cf58ed108e96938. --- .gitlab-ci.yml | 2 +- .travis.yml | 2 +- plugins/Benchmark/BenchmarkPlugin.py | 4 +- plugins/CryptMessage/CryptMessage.py | 75 +- plugins/CryptMessage/CryptMessagePlugin.py | 56 +- plugins/CryptMessage/Test/TestCrypt.py | 7 +- requirements.txt | 1 + src/Crypt/CryptBitcoin.py | 86 +- src/Test/conftest.py | 2 +- .../libsecp256k1message.py | 43 +- src/lib/pyaes/README.md | 363 --- src/lib/pyaes/__init__.py | 53 - src/lib/pyaes/aes.py | 589 ----- src/lib/pyaes/blockfeeder.py | 227 -- src/lib/pyaes/util.py | 60 - .../LICENSE.txt => pybitcointools/LICENSE} | 9 +- src/lib/pybitcointools/__init__.py | 10 + src/lib/pybitcointools/bci.py | 528 +++++ src/lib/pybitcointools/blocks.py | 50 + src/lib/pybitcointools/composite.py | 128 ++ src/lib/pybitcointools/deterministic.py | 199 ++ src/lib/pybitcointools/english.txt | 2048 +++++++++++++++++ src/lib/pybitcointools/main.py | 581 +++++ src/lib/pybitcointools/mnemonic.py | 127 + src/lib/pybitcointools/py2specials.py | 98 + src/lib/pybitcointools/py3specials.py | 123 + src/lib/pybitcointools/ripemd.py | 414 ++++ src/lib/pybitcointools/stealth.py | 100 + src/lib/pybitcointools/transaction.py | 514 +++++ src/lib/pyelliptic/LICENSE | 674 ++++++ src/lib/pyelliptic/README.md | 96 + src/lib/pyelliptic/__init__.py | 19 + src/lib/pyelliptic/arithmetic.py | 144 ++ src/lib/pyelliptic/cipher.py | 84 + src/lib/pyelliptic/ecc.py | 505 ++++ src/lib/pyelliptic/hash.py | 69 + src/lib/pyelliptic/openssl.py | 553 +++++ src/lib/pyelliptic/setup.py | 23 + src/lib/sslcrypto/LICENSE | 27 - src/lib/sslcrypto/__init__.py | 6 - src/lib/sslcrypto/_aes.py | 53 - src/lib/sslcrypto/_ecc.py | 334 --- src/lib/sslcrypto/_ripemd.py | 375 --- src/lib/sslcrypto/fallback/__init__.py | 3 - src/lib/sslcrypto/fallback/_jacobian.py | 159 -- src/lib/sslcrypto/fallback/_util.py | 79 - src/lib/sslcrypto/fallback/aes.py | 101 - src/lib/sslcrypto/fallback/ecc.py | 371 --- src/lib/sslcrypto/fallback/rsa.py | 8 - src/lib/sslcrypto/openssl/__init__.py | 3 - src/lib/sslcrypto/openssl/aes.py | 156 -- src/lib/sslcrypto/openssl/ecc.py | 551 ----- src/lib/sslcrypto/openssl/library.py | 93 - src/lib/sslcrypto/openssl/rsa.py | 11 - src/util/Electrum.py | 39 - 55 files changed, 7287 insertions(+), 3748 deletions(-) delete mode 100644 src/lib/pyaes/README.md delete mode 100644 src/lib/pyaes/__init__.py delete mode 100644 src/lib/pyaes/aes.py delete mode 100644 src/lib/pyaes/blockfeeder.py delete mode 100644 src/lib/pyaes/util.py rename src/lib/{pyaes/LICENSE.txt => pybitcointools/LICENSE} (80%) create mode 100644 src/lib/pybitcointools/__init__.py create mode 100644 src/lib/pybitcointools/bci.py create mode 100644 src/lib/pybitcointools/blocks.py create mode 100644 src/lib/pybitcointools/composite.py create mode 100644 src/lib/pybitcointools/deterministic.py create mode 100644 src/lib/pybitcointools/english.txt create mode 100644 src/lib/pybitcointools/main.py create mode 100644 src/lib/pybitcointools/mnemonic.py create mode 100644 src/lib/pybitcointools/py2specials.py create mode 100644 src/lib/pybitcointools/py3specials.py create mode 100644 src/lib/pybitcointools/ripemd.py create mode 100644 src/lib/pybitcointools/stealth.py create mode 100644 src/lib/pybitcointools/transaction.py create mode 100644 src/lib/pyelliptic/LICENSE create mode 100644 src/lib/pyelliptic/README.md create mode 100644 src/lib/pyelliptic/__init__.py create mode 100644 src/lib/pyelliptic/arithmetic.py create mode 100644 src/lib/pyelliptic/cipher.py create mode 100644 src/lib/pyelliptic/ecc.py create mode 100644 src/lib/pyelliptic/hash.py create mode 100644 src/lib/pyelliptic/openssl.py create mode 100644 src/lib/pyelliptic/setup.py delete mode 100644 src/lib/sslcrypto/LICENSE delete mode 100644 src/lib/sslcrypto/__init__.py delete mode 100644 src/lib/sslcrypto/_aes.py delete mode 100644 src/lib/sslcrypto/_ecc.py delete mode 100644 src/lib/sslcrypto/_ripemd.py delete mode 100644 src/lib/sslcrypto/fallback/__init__.py delete mode 100644 src/lib/sslcrypto/fallback/_jacobian.py delete mode 100644 src/lib/sslcrypto/fallback/_util.py delete mode 100644 src/lib/sslcrypto/fallback/aes.py delete mode 100644 src/lib/sslcrypto/fallback/ecc.py delete mode 100644 src/lib/sslcrypto/fallback/rsa.py delete mode 100644 src/lib/sslcrypto/openssl/__init__.py delete mode 100644 src/lib/sslcrypto/openssl/aes.py delete mode 100644 src/lib/sslcrypto/openssl/ecc.py delete mode 100644 src/lib/sslcrypto/openssl/library.py delete mode 100644 src/lib/sslcrypto/openssl/rsa.py delete mode 100644 src/util/Electrum.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f3e1ed29..b62c7b0c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ stages: - python -m pytest -x plugins/Multiuser/Test --color=yes - mv plugins/disabled-Bootstrapper plugins/Bootstrapper - python -m pytest -x plugins/Bootstrapper/Test --color=yes - - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ test:py3.4: image: python:3.4.3 diff --git a/.travis.yml b/.travis.yml index c67ecf88..1f81b60a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ script: - python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini - mv plugins/disabled-Multiuser plugins/Multiuser && python -m pytest -x plugins/Multiuser/Test - mv plugins/disabled-Bootstrapper plugins/Bootstrapper && python -m pytest -x plugins/Bootstrapper/Test - - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ after_success: - codecov - coveralls --rcfile=src/Test/coverage.ini diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index 72f8b6ac..73b95d22 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -103,8 +103,8 @@ class ActionsPlugin: tests.extend([ {"func": self.testHdPrivatekey, "num": 50, "time_standard": 0.57}, {"func": self.testSign, "num": 20, "time_standard": 0.46}, - {"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto_fallback"}, "num": 20, "time_standard": 0.38}, - {"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto"}, "num": 200, "time_standard": 0.30}, + {"func": self.testVerify, "kwargs": {"lib_verify": "btctools"}, "num": 20, "time_standard": 0.38}, + {"func": self.testVerify, "kwargs": {"lib_verify": "openssl"}, "num": 200, "time_standard": 0.30}, {"func": self.testVerify, "kwargs": {"lib_verify": "libsecp256k1"}, "num": 200, "time_standard": 0.10}, {"func": self.testPackMsgpack, "num": 100, "time_standard": 0.35}, diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index 2bb61d03..b6c65673 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -1,21 +1,28 @@ import hashlib import base64 -import sslcrypto +import binascii + +import lib.pybitcointools as btctools +from util import ThreadPool from Crypt import Crypt +ecc_cache = {} -curve = sslcrypto.ecc.get_curve("secp256k1") - - -def eciesEncrypt(data, pubkey, ciphername="aes-256-cbc"): - ciphertext, key_e = curve.encrypt( - data, - base64.b64decode(pubkey), - algo=ciphername, - derivation="sha512", - return_aes_key=True - ) - return key_e, ciphertext +def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): + from lib import pyelliptic + pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey)) + curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey_openssl) + if ephemcurve is None: + ephemcurve = curve + ephem = pyelliptic.ECC(curve=ephemcurve) + key = hashlib.sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() + key_e, key_m = key[:32], key[32:] + pubkey = ephem.get_pubkey() + iv = pyelliptic.OpenSSL.rand(pyelliptic.OpenSSL.get_cipher(ciphername).get_blocksize()) + ctx = pyelliptic.Cipher(key_e, iv, 1, ciphername) + ciphertext = iv + pubkey + ctx.ciphering(data) + mac = pyelliptic.hmac_sha256(key_m, ciphertext) + return key_e, ciphertext + mac @Crypt.thread_pool_crypt.wrap @@ -30,12 +37,38 @@ def eciesDecryptMulti(encrypted_datas, privatekey): return texts -def eciesDecrypt(ciphertext, privatekey): - return curve.decrypt( - base64.b64decode(ciphertext), - curve.wif_to_private(privatekey), - derivation="sha512" - ) +def eciesDecrypt(encrypted_data, privatekey): + ecc_key = getEcc(privatekey) + return ecc_key.decrypt(base64.b64decode(encrypted_data)) -def split(ciphertext): - return ciphertext[:16], ciphertext[86:-32] +def split(encrypted): + iv = encrypted[0:16] + ciphertext = encrypted[16 + 70:-32] + + return iv, ciphertext + + +def getEcc(privatekey=None): + from lib import pyelliptic + global ecc_cache + if privatekey not in ecc_cache: + if privatekey: + publickey_bin = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin") + publickey_openssl = toOpensslPublickey(publickey_bin) + privatekey_openssl = toOpensslPrivatekey(privatekey) + ecc_cache[privatekey] = pyelliptic.ECC(curve='secp256k1', privkey=privatekey_openssl, pubkey=publickey_openssl) + else: + ecc_cache[None] = pyelliptic.ECC() + return ecc_cache[privatekey] + + +def toOpensslPrivatekey(privatekey): + privatekey_bin = btctools.encode_privkey(privatekey, "bin") + return b'\x02\xca\x00\x20' + privatekey_bin + + +def toOpensslPublickey(publickey): + publickey_bin = btctools.encode_pubkey(publickey, "bin") + publickey_bin = publickey_bin[1:] + publickey_openssl = b'\x02\xca\x00 ' + publickey_bin[:32] + b'\x00 ' + publickey_bin[32:] + return publickey_openssl diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index 150bf8be..45afe184 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -5,22 +5,24 @@ import gevent from Plugin import PluginManager from Crypt import CryptBitcoin, CryptHash +import lib.pybitcointools as btctools from Config import config -import sslcrypto - from . import CryptMessage -curve = sslcrypto.ecc.get_curve("secp256k1") - @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): + def eciesDecrypt(self, encrypted, privatekey): + back = CryptMessage.getEcc(privatekey).decrypt(encrypted) + return back.decode("utf8") + # - Actions - # Returns user's public key unique to site # Return: Public key def actionUserPublickey(self, to, index=0): - self.response(to, self.user.getEncryptPublickey(self.site.address, index)) + publickey = self.user.getEncryptPublickey(self.site.address, index) + self.response(to, publickey) # Encrypt a text using the publickey or user's sites unique publickey # Return: Encrypted text using base64 encoding @@ -53,16 +55,23 @@ class UiWebsocketPlugin(object): # Encrypt a text using AES # Return: Iv, AES key, Encrypted text - def actionAesEncrypt(self, to, text, key=None): + def actionAesEncrypt(self, to, text, key=None, iv=None): + from lib import pyelliptic + if key: key = base64.b64decode(key) else: - key = sslcrypto.aes.new_key() + key = os.urandom(32) + + if iv: # Generate new AES key if not definied + iv = base64.b64decode(iv) + else: + iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') if text: - encrypted, iv = sslcrypto.aes.encrypt(text.encode("utf8"), key) + encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(text.encode("utf8")) else: - encrypted, iv = b"", b"" + encrypted = b"" res = [base64.b64encode(item).decode("utf8") for item in [key, iv, encrypted]] self.response(to, res) @@ -70,6 +79,8 @@ class UiWebsocketPlugin(object): # Decrypt a text using AES # Return: Decrypted text def actionAesDecrypt(self, to, *args): + from lib import pyelliptic + if len(args) == 3: # Single decrypt encrypted_texts = [(args[0], args[1])] keys = [args[2]] @@ -82,8 +93,9 @@ class UiWebsocketPlugin(object): iv = base64.b64decode(iv) text = None for key in keys: + ctx = pyelliptic.Cipher(base64.b64decode(key), iv, 0, ciphername='aes-256-cbc') try: - decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, base64.b64decode(key)) + decrypted = ctx.ciphering(encrypted_text) if decrypted and decrypted.decode("utf8"): # Valid text decoded text = decrypted.decode("utf8") except Exception as err: @@ -110,11 +122,12 @@ class UiWebsocketPlugin(object): # Gets the publickey of a given privatekey def actionEccPrivToPub(self, to, privatekey): - self.response(to, curve.private_to_public(curve.wif_to_private(privatekey))) + self.response(to, btctools.privtopub(privatekey)) # Gets the address of a given publickey def actionEccPubToAddr(self, to, publickey): - self.response(to, curve.public_to_address(bytes.fromhex(publickey))) + address = btctools.pubtoaddr(btctools.decode_pubkey(publickey)) + self.response(to, address) @PluginManager.registerTo("User") @@ -150,7 +163,7 @@ class UserPlugin(object): if "encrypt_publickey_%s" % index not in site_data: privatekey = self.getEncryptPrivatekey(address, param_index) - publickey = curve.private_to_public(curve.wif_to_private(privatekey)) + publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed") site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey).decode("utf8") return site_data["encrypt_publickey_%s" % index] @@ -187,8 +200,8 @@ class ActionsPlugin: aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) for i in range(num_run): assert len(aes_key) == 32 - decrypted = CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) - assert decrypted == self.utf8_text.encode("utf8"), "%s != %s" % (decrypted, self.utf8_text.encode("utf8")) + ecc = CryptMessage.getEcc(self.privatekey) + assert ecc.decrypt(encrypted) == self.utf8_text.encode("utf8"), "%s != %s" % (ecc.decrypt(encrypted), self.utf8_text.encode("utf8")) yield "." def testCryptEciesDecryptMulti(self, num_run=1): @@ -210,16 +223,23 @@ class ActionsPlugin: gevent.joinall(threads) def testCryptAesEncrypt(self, num_run=1): + from lib import pyelliptic + for i in range(num_run): key = os.urandom(32) - encrypted = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) + iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') + encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) yield "." def testCryptAesDecrypt(self, num_run=1): + from lib import pyelliptic + key = os.urandom(32) - encrypted_text, iv = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) + iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') + encrypted_text = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) for i in range(num_run): - decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, key).decode("utf8") + ctx = pyelliptic.Cipher(key, iv, 0, ciphername='aes-256-cbc') + decrypted = ctx.ciphering(encrypted_text).decode("utf8") assert decrypted == self.utf8_text yield "." diff --git a/plugins/CryptMessage/Test/TestCrypt.py b/plugins/CryptMessage/Test/TestCrypt.py index 4315a260..05cc6e44 100644 --- a/plugins/CryptMessage/Test/TestCrypt.py +++ b/plugins/CryptMessage/Test/TestCrypt.py @@ -18,10 +18,13 @@ class TestCrypt: assert len(aes_key) == 32 # assert len(encrypted) == 134 + int(len(text) / 16) * 16 # Not always true - assert CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) == text_repeated + ecc = CryptMessage.getEcc(self.privatekey) + assert ecc.decrypt(encrypted) == text_repeated def testDecryptEcies(self, user): - assert CryptMessage.eciesDecrypt(self.ecies_encrypted_text, self.privatekey) == b"hello" + encrypted = base64.b64decode(self.ecies_encrypted_text) + ecc = CryptMessage.getEcc(self.privatekey) + assert ecc.decrypt(encrypted) == b"hello" def testPublickey(self, ui_websocket): pub = ui_websocket.testAction("UserPublickey", 0) diff --git a/requirements.txt b/requirements.txt index db243c4c..7c063e3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ pyasn1 websocket_client gevent-websocket coincurve +python-bitcoinlib maxminddb diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index 18bdd2f5..b6bfaa77 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -1,22 +1,16 @@ import logging import base64 -import binascii import time -import hashlib from util import OpensslFindPatch -from util.Electrum import dbl_format +from lib import pybitcointools as btctools from Config import config -lib_verify_best = "sslcrypto" +lib_verify_best = "btctools" -import sslcrypto -sslcurve_native = sslcrypto.ecc.get_curve("secp256k1") -sslcurve_fallback = sslcrypto.fallback.ecc.get_curve("secp256k1") -sslcurve = sslcurve_native def loadLib(lib_name, silent=False): - global sslcurve, libsecp256k1message, lib_verify_best + global bitcoin, libsecp256k1message, lib_verify_best if lib_name == "libsecp256k1": s = time.time() from lib import libsecp256k1message @@ -27,10 +21,24 @@ def loadLib(lib_name, silent=False): "Libsecpk256k1 loaded: %s in %.3fs" % (type(coincurve._libsecp256k1.lib).__name__, time.time() - s) ) - elif lib_name == "sslcrypto": - sslcurve = sslcurve_native - elif lib_name == "sslcrypto_fallback": - sslcurve = sslcurve_fallback + elif lib_name == "openssl": + s = time.time() + import bitcoin.signmessage + import bitcoin.core.key + import bitcoin.wallet + + try: + # OpenSSL 1.1.0 + ssl_version = bitcoin.core.key._ssl.SSLeay() + except AttributeError: + # OpenSSL 1.1.1+ + ssl_version = bitcoin.core.key._ssl.OpenSSL_version_num() + + if not silent: + logging.info( + "OpenSSL loaded: %s, version: %.9X in %.3fs" % + (bitcoin.core.key._ssl, ssl_version, time.time() - s) + ) try: if not config.use_libsecp256k1: @@ -38,30 +46,35 @@ try: loadLib("libsecp256k1") lib_verify_best = "libsecp256k1" except Exception as err: - logging.info("Libsecp256k1 load failed: %s" % err) + logging.info("Libsecp256k1 load failed: %s, try to load OpenSSL" % err) + try: + if not config.use_openssl: + raise Exception("Disabled by config") + loadLib("openssl") + lib_verify_best = "openssl" + except Exception as err: + logging.info("OpenSSL load failed: %s, falling back to slow bitcoin verify" % err) -def newPrivatekey(): # Return new private key - return sslcurve.private_to_wif(sslcurve.new_private_key()).decode() +def newPrivatekey(uncompressed=True): # Return new private key + privatekey = btctools.encode_privkey(btctools.random_key(), "wif") + return privatekey def newSeed(): - return binascii.hexlify(sslcurve.new_private_key()).decode() + return btctools.random_key() def hdPrivatekey(seed, child): - # Too large child id could cause problems - privatekey_bin = sslcurve.derive_child(seed.encode(), child % 100000000) - return sslcurve.private_to_wif(privatekey_bin).decode() + masterkey = btctools.bip32_master_key(bytes(seed, "ascii")) + childkey = btctools.bip32_ckd(masterkey, child % 100000000) # Too large child id could cause problems + key = btctools.bip32_extract_key(childkey) + return btctools.encode_privkey(key, "wif") def privatekeyToAddress(privatekey): # Return address from private key try: - if len(privatekey) == 64: - privatekey_bin = bytes.fromhex(privatekey) - else: - privatekey_bin = sslcurve.wif_to_private(privatekey.encode()) - return sslcurve.private_to_address(privatekey_bin, is_compressed=False).decode() + return btctools.privkey_to_address(privatekey) except Exception: # Invalid privatekey return False @@ -69,13 +82,8 @@ def privatekeyToAddress(privatekey): # Return address from private key def sign(data, privatekey): # Return sign to data using private key if privatekey.startswith("23") and len(privatekey) > 52: return None # Old style private key not supported - return base64.b64encode(sslcurve.sign( - data.encode(), - sslcurve.wif_to_private(privatekey.encode()), - is_compressed=False, - recoverable=True, - hash=dbl_format - )).decode() + sign = btctools.ecdsa_sign(data, privatekey) + return sign def verify(data, valid_address, sign, lib_verify=None): # Verify data using address and sign @@ -87,9 +95,17 @@ def verify(data, valid_address, sign, lib_verify=None): # Verify data using add if lib_verify == "libsecp256k1": sign_address = libsecp256k1message.recover_address(data.encode("utf8"), sign).decode("utf8") - elif lib_verify in ("sslcrypto", "sslcrypto_fallback"): - publickey = sslcurve.recover(base64.b64decode(sign), data.encode(), hash=dbl_format) - sign_address = sslcurve.public_to_address(publickey).decode() + elif lib_verify == "openssl": + sig = base64.b64decode(sign) + message = bitcoin.signmessage.BitcoinMessage(data) + hash = message.GetHash() + + pubkey = bitcoin.core.key.CPubKey.recover_compact(hash, sig) + + sign_address = str(bitcoin.wallet.P2PKHBitcoinAddress.from_pubkey(pubkey)) + elif lib_verify == "btctools": # Use pure-python + pub = btctools.ecdsa_recover(data, sign) + sign_address = btctools.pubtoaddr(pub) else: raise Exception("No library enabled for signature verification") diff --git a/src/Test/conftest.py b/src/Test/conftest.py index d93ee33a..74f27712 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -407,7 +407,7 @@ def db(request): return db -@pytest.fixture(params=["sslcrypto", "sslcrypto_fallback", "libsecp256k1"]) +@pytest.fixture(params=["btctools", "openssl", "libsecp256k1"]) def crypt_bitcoin_lib(request, monkeypatch): monkeypatch.setattr(CryptBitcoin, "lib_verify_best", request.param) CryptBitcoin.loadLib(request.param) diff --git a/src/lib/libsecp256k1message/libsecp256k1message.py b/src/lib/libsecp256k1message/libsecp256k1message.py index 59768b88..92802e1c 100644 --- a/src/lib/libsecp256k1message/libsecp256k1message.py +++ b/src/lib/libsecp256k1message/libsecp256k1message.py @@ -1,9 +1,9 @@ import hashlib +import struct import base64 from coincurve import PrivateKey, PublicKey from base58 import b58encode_check, b58decode_check from hmac import compare_digest -from util.Electrum import format as zero_format RECID_MIN = 0 RECID_MAX = 3 @@ -97,7 +97,7 @@ def sign_data(secretkey, byte_string): """Sign [byte_string] with [secretkey]. Return serialized signature compatible with Electrum (ZeroNet).""" # encode the message - encoded = zero_format(byte_string) + encoded = _zero_format(byte_string) # sign the message and get a coincurve signature signature = secretkey.sign_recoverable(encoded) # reserialize signature and return it @@ -110,7 +110,7 @@ def verify_data(key_digest, electrum_signature, byte_string): # reserialize signature signature = coincurve_sig(electrum_signature) # encode the message - encoded = zero_format(byte_string) + encoded = _zero_format(byte_string) # recover full public key from signature # "which guarantees a correct signature" publickey = recover_public_key(signature, encoded) @@ -130,10 +130,45 @@ def verify_sig(publickey, signature, byte_string): def verify_key(publickey, key_digest): return compare_digest(key_digest, public_digest(publickey)) + +# Electrum, the heck?! + +def bchr(i): + return struct.pack('B', i) + +def _zero_encode(val, base, minlen=0): + base, minlen = int(base), int(minlen) + code_string = b''.join([bchr(x) for x in range(256)]) + result = b'' + while val > 0: + index = val % base + result = code_string[index:index + 1] + result + val //= base + return code_string[0:1] * max(minlen - len(result), 0) + result + +def _zero_insane_int(x): + x = int(x) + if x < 253: + return bchr(x) + elif x < 65536: + return bchr(253) + _zero_encode(x, 256, 2)[::-1] + elif x < 4294967296: + return bchr(254) + _zero_encode(x, 256, 4)[::-1] + else: + return bchr(255) + _zero_encode(x, 256, 8)[::-1] + + +def _zero_magic(message): + return b'\x18Bitcoin Signed Message:\n' + _zero_insane_int(len(message)) + message + +def _zero_format(message): + padded = _zero_magic(message) + return hashlib.sha256(padded).digest() + def recover_address(data, sign): sign_bytes = base64.b64decode(sign) is_compressed = ((sign_bytes[0] - 27) & 4) != 0 - publickey = recover_public_key(coincurve_sig(sign_bytes), zero_format(data)) + publickey = recover_public_key(coincurve_sig(sign_bytes), _zero_format(data)) return compute_public_address(publickey, compressed=is_compressed) __all__ = [ diff --git a/src/lib/pyaes/README.md b/src/lib/pyaes/README.md deleted file mode 100644 index 26e3b2ba..00000000 --- a/src/lib/pyaes/README.md +++ /dev/null @@ -1,363 +0,0 @@ -pyaes -===== - -A pure-Python implementation of the AES block cipher algorithm and the common modes of operation (CBC, CFB, CTR, ECB and OFB). - - -Features --------- - -* Supports all AES key sizes -* Supports all AES common modes -* Pure-Python (no external dependencies) -* BlockFeeder API allows streams to easily be encrypted and decrypted -* Python 2.x and 3.x support (make sure you pass in bytes(), not strings for Python 3) - - -API ---- - -All keys may be 128 bits (16 bytes), 192 bits (24 bytes) or 256 bits (32 bytes) long. - -To generate a random key use: -```python -import os - -# 128 bit, 192 bit and 256 bit keys -key_128 = os.urandom(16) -key_192 = os.urandom(24) -key_256 = os.urandom(32) -``` - -To generate keys from simple-to-remember passwords, consider using a _password-based key-derivation function_ such as [scrypt](https://github.com/ricmoo/pyscrypt). - - -### Common Modes of Operation - -There are many modes of operations, each with various pros and cons. In general though, the **CBC** and **CTR** modes are recommended. The **ECB is NOT recommended.**, and is included primarily for completeness. - -Each of the following examples assumes the following key: -```python -import pyaes - -# A 256 bit (32 byte) key -key = "This_key_for_demo_purposes_only!" - -# For some modes of operation we need a random initialization vector -# of 16 bytes -iv = "InitializationVe" -``` - - -#### Counter Mode of Operation (recommended) - -```python -aes = pyaes.AESModeOfOperationCTR(key) -plaintext = "Text may be any length you wish, no padding is required" -ciphertext = aes.encrypt(plaintext) - -# '''\xb6\x99\x10=\xa4\x96\x88\xd1\x89\x1co\xe6\x1d\xef;\x11\x03\xe3\xee -# \xa9V?wY\xbfe\xcdO\xe3\xdf\x9dV\x19\xe5\x8dk\x9fh\xb87>\xdb\xa3\xd6 -# \x86\xf4\xbd\xb0\x97\xf1\t\x02\xe9 \xed''' -print repr(ciphertext) - -# The counter mode of operation maintains state, so decryption requires -# a new instance be created -aes = pyaes.AESModeOfOperationCTR(key) -decrypted = aes.decrypt(ciphertext) - -# True -print decrypted == plaintext - -# To use a custom initial value -counter = pyaes.Counter(initial_value = 100) -aes = pyaes.AESModeOfOperationCTR(key, counter = counter) -ciphertext = aes.encrypt(plaintext) - -# '''WZ\x844\x02\xbfoY\x1f\x12\xa6\xce\x03\x82Ei)\xf6\x97mX\x86\xe3\x9d -# _1\xdd\xbd\x87\xb5\xccEM_4\x01$\xa6\x81\x0b\xd5\x04\xd7Al\x07\xe5 -# \xb2\x0e\\\x0f\x00\x13,\x07''' -print repr(ciphertext) -``` - - -#### Cipher-Block Chaining (recommended) - -```python -aes = pyaes.AESModeOfOperationCBC(key, iv = iv) -plaintext = "TextMustBe16Byte" -ciphertext = aes.encrypt(plaintext) - -# '\xd6:\x18\xe6\xb1\xb3\xc3\xdc\x87\xdf\xa7|\x08{k\xb6' -print repr(ciphertext) - - -# The cipher-block chaining mode of operation maintains state, so -# decryption requires a new instance be created -aes = pyaes.AESModeOfOperationCBC(key, iv = iv) -decrypted = aes.decrypt(ciphertext) - -# True -print decrypted == plaintext -``` - - -#### Cipher Feedback - -```python -# Each block into the mode of operation must be a multiple of the segment -# size. For this example we choose 8 bytes. -aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) -plaintext = "TextMustBeAMultipleOfSegmentSize" -ciphertext = aes.encrypt(plaintext) - -# '''v\xa9\xc1w"\x8aL\x93\xcb\xdf\xa0/\xf8Y\x0b\x8d\x88i\xcb\x85rmp -# \x85\xfe\xafM\x0c)\xd5\xeb\xaf''' -print repr(ciphertext) - - -# The cipher-block chaining mode of operation maintains state, so -# decryption requires a new instance be created -aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) -decrypted = aes.decrypt(ciphertext) - -# True -print decrypted == plaintext -``` - - -#### Output Feedback Mode of Operation - -```python -aes = pyaes.AESModeOfOperationOFB(key, iv = iv) -plaintext = "Text may be any length you wish, no padding is required" -ciphertext = aes.encrypt(plaintext) - -# '''v\xa9\xc1wO\x92^\x9e\rR\x1e\xf7\xb1\xa2\x9d"l1\xc7\xe7\x9d\x87(\xc26s -# \xdd8\xc8@\xb6\xd9!\xf5\x0cM\xaa\x9b\xc4\xedLD\xe4\xb9\xd8\xdf\x9e\xac -# \xa1\xb8\xea\x0f\x8ev\xb5''' -print repr(ciphertext) - -# The counter mode of operation maintains state, so decryption requires -# a new instance be created -aes = pyaes.AESModeOfOperationOFB(key, iv = iv) -decrypted = aes.decrypt(ciphertext) - -# True -print decrypted == plaintext -``` - - -#### Electronic Codebook (NOT recommended) - -```python -aes = pyaes.AESModeOfOperationECB(key) -plaintext = "TextMustBe16Byte" -ciphertext = aes.encrypt(plaintext) - -# 'L6\x95\x85\xe4\xd9\xf1\x8a\xfb\xe5\x94X\x80|\x19\xc3' -print repr(ciphertext) - -# Since there is no state stored in this mode of operation, it -# is not necessary to create a new aes object for decryption. -#aes = pyaes.AESModeOfOperationECB(key) -decrypted = aes.decrypt(ciphertext) - -# True -print decrypted == plaintext -``` - - -### BlockFeeder - -Since most of the modes of operations require data in specific block-sized or segment-sized blocks, it can be difficult when working with large arbitrary streams or strings of data. - -The BlockFeeder class is meant to make life easier for you, by buffering bytes across multiple calls and returning bytes as they are available, as well as padding or stripping the output when finished, if necessary. - -```python -import pyaes - -# Any mode of operation can be used; for this example CBC -key = "This_key_for_demo_purposes_only!" -iv = "InitializationVe" - -ciphertext = '' - -# We can encrypt one line at a time, regardles of length -encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv)) -for line in file('/etc/passwd'): - ciphertext += encrypter.feed(line) - -# Make a final call to flush any remaining bytes and add paddin -ciphertext += encrypter.feed() - -# We can decrypt the cipher text in chunks (here we split it in half) -decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv)) -decrypted = decrypter.feed(ciphertext[:len(ciphertext) / 2]) -decrypted += decrypter.feed(ciphertext[len(ciphertext) / 2:]) - -# Again, make a final call to flush any remaining bytes and strip padding -decrypted += decrypter.feed() - -print file('/etc/passwd').read() == decrypted -``` - -### Stream Feeder - -This is meant to make it even easier to encrypt and decrypt streams and large files. - -```python -import pyaes - -# Any mode of operation can be used; for this example CTR -key = "This_key_for_demo_purposes_only!" - -# Create the mode of operation to encrypt with -mode = pyaes.AESModeOfOperationCTR(key) - -# The input and output files -file_in = file('/etc/passwd') -file_out = file('/tmp/encrypted.bin', 'wb') - -# Encrypt the data as a stream, the file is read in 8kb chunks, be default -pyaes.encrypt_stream(mode, file_in, file_out) - -# Close the files -file_in.close() -file_out.close() -``` - -Decrypting is identical, except you would use `pyaes.decrypt_stream`, and the encrypted file would be the `file_in` and target for decryption the `file_out`. - -### AES block cipher - -Generally you should use one of the modes of operation above. This may however be useful for experimenting with a custom mode of operation or dealing with encrypted blocks. - -The block cipher requires exactly one block of data to encrypt or decrypt, and each block should be an array with each element an integer representation of a byte. - -```python -import pyaes - -# 16 byte block of plain text -plaintext = "Hello World!!!!!" -plaintext_bytes = [ ord(c) for c in plaintext ] - -# 32 byte key (256 bit) -key = "This_key_for_demo_purposes_only!" - -# Our AES instance -aes = pyaes.AES(key) - -# Encrypt! -ciphertext = aes.encrypt(plaintext_bytes) - -# [55, 250, 182, 25, 185, 208, 186, 95, 206, 115, 50, 115, 108, 58, 174, 115] -print repr(ciphertext) - -# Decrypt! -decrypted = aes.decrypt(ciphertext) - -# True -print decrypted == plaintext_bytes -``` - -What is a key? --------------- - -This seems to be a point of confusion for many people new to using encryption. You can think of the key as the *"password"*. However, these algorithms require the *"password"* to be a specific length. - -With AES, there are three possible key lengths, 16-bytes, 24-bytes or 32-bytes. When you create an AES object, the key size is automatically detected, so it is important to pass in a key of the correct length. - -Often, you wish to provide a password of arbitrary length, for example, something easy to remember or write down. In these cases, you must come up with a way to transform the password into a key, of a specific length. A **Password-Based Key Derivation Function** (PBKDF) is an algorithm designed for this exact purpose. - -Here is an example, using the popular (possibly obsolete?) *crypt* PBKDF: - -``` -# See: https://www.dlitz.net/software/python-pbkdf2/ -import pbkdf2 - -password = "HelloWorld" - -# The crypt PBKDF returns a 48-byte string -key = pbkdf2.crypt(password) - -# A 16-byte, 24-byte and 32-byte key, respectively -key_16 = key[:16] -key_24 = key[:24] -key_32 = key[:32] -``` - -The [scrypt](https://github.com/ricmoo/pyscrypt) PBKDF is intentionally slow, to make it more difficult to brute-force guess a password: - -``` -# See: https://github.com/ricmoo/pyscrypt -import pyscrypt - -password = "HelloWorld" - -# Salt is required, and prevents Rainbow Table attacks -salt = "SeaSalt" - -# N, r, and p are parameters to specify how difficult it should be to -# generate a key; bigger numbers take longer and more memory -N = 1024 -r = 1 -p = 1 - -# A 16-byte, 24-byte and 32-byte key, respectively; the scrypt algorithm takes -# a 6-th parameter, indicating key length -key_16 = pyscrypt.hash(password, salt, N, r, p, 16) -key_24 = pyscrypt.hash(password, salt, N, r, p, 24) -key_32 = pyscrypt.hash(password, salt, N, r, p, 32) -``` - -Another possibility, is to use a hashing function, such as SHA256 to hash the password, but this method may be vulnerable to [Rainbow Attacks](http://en.wikipedia.org/wiki/Rainbow_table), unless you use a [salt](http://en.wikipedia.org/wiki/Salt_(cryptography)). - -```python -import hashlib - -password = "HelloWorld" - -# The SHA256 hash algorithm returns a 32-byte string -hashed = hashlib.sha256(password).digest() - -# A 16-byte, 24-byte and 32-byte key, respectively -key_16 = hashed[:16] -key_24 = hashed[:24] -key_32 = hashed -``` - - - - -Performance ------------ - -There is a test case provided in _/tests/test-aes.py_ which does some basic performance testing (its primary purpose is moreso as a regression test). - -Based on that test, in **CPython**, this library is about 30x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 80x slower for CFB; and 300x slower for CTR. - -Based on that same test, in **Pypy**, this library is about 4x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 12x slower for CFB; and 19x slower for CTR. - -The PyCrypto documentation makes reference to the counter call being responsible for the speed problems of the counter (CTR) mode of operation, which is why they use a specially optimized counter. I will investigate this problem further in the future. - - -FAQ ---- - -#### Why do this? - -The short answer, *why not?* - -The longer answer, is for my [pyscrypt](https://github.com/ricmoo/pyscrypt) library. I required a pure-Python AES implementation that supported 256-bit keys with the counter (CTR) mode of operation. After searching, I found several implementations, but all were missing CTR or only supported 128 bit keys. After all the work of learning AES inside and out to implement the library, it was only a marginal amount of extra work to library-ify a more general solution. So, *why not?* - -#### How do I get a question I have added? - -E-mail me at pyaes@ricmoo.com with any questions, suggestions, comments, et cetera. - - -#### Can I give you my money? - -Umm... Ok? :-) - -_Bitcoin_ - `18UDs4qV1shu2CgTS2tKojhCtM69kpnWg9` diff --git a/src/lib/pyaes/__init__.py b/src/lib/pyaes/__init__.py deleted file mode 100644 index 5712f794..00000000 --- a/src/lib/pyaes/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2014 Richard Moore -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# This is a pure-Python implementation of the AES algorithm and AES common -# modes of operation. - -# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard -# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation - - -# Supported key sizes: -# 128-bit -# 192-bit -# 256-bit - - -# Supported modes of operation: -# ECB - Electronic Codebook -# CBC - Cipher-Block Chaining -# CFB - Cipher Feedback -# OFB - Output Feedback -# CTR - Counter - -# See the README.md for API details and general information. - -# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: -# https://www.dlitz.net/software/pycrypto/ - - -VERSION = [1, 3, 0] - -from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter -from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter -from .blockfeeder import PADDING_NONE, PADDING_DEFAULT diff --git a/src/lib/pyaes/aes.py b/src/lib/pyaes/aes.py deleted file mode 100644 index c6e8bc02..00000000 --- a/src/lib/pyaes/aes.py +++ /dev/null @@ -1,589 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2014 Richard Moore -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# This is a pure-Python implementation of the AES algorithm and AES common -# modes of operation. - -# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard - -# Honestly, the best description of the modes of operations are the wonderful -# diagrams on Wikipedia. They explain in moments what my words could never -# achieve. Hence the inline documentation here is sparer than I'd prefer. -# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation - -# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: -# https://www.dlitz.net/software/pycrypto/ - - -# Supported key sizes: -# 128-bit -# 192-bit -# 256-bit - - -# Supported modes of operation: -# ECB - Electronic Codebook -# CBC - Cipher-Block Chaining -# CFB - Cipher Feedback -# OFB - Output Feedback -# CTR - Counter - - -# See the README.md for API details and general information. - - -import copy -import struct - -__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB", - "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"] - - -def _compact_word(word): - return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3] - -def _string_to_bytes(text): - return list(ord(c) for c in text) - -def _bytes_to_string(binary): - return "".join(chr(b) for b in binary) - -def _concat_list(a, b): - return a + b - - -# Python 3 compatibility -try: - xrange -except Exception: - xrange = range - - # Python 3 supports bytes, which is already an array of integers - def _string_to_bytes(text): - if isinstance(text, bytes): - return text - return [ord(c) for c in text] - - # In Python 3, we return bytes - def _bytes_to_string(binary): - return bytes(binary) - - # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first - def _concat_list(a, b): - return a + bytes(b) - - -# Based *largely* on the Rijndael implementation -# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf -class AES(object): - '''Encapsulates the AES block cipher. - - You generally should not need this. Use the AESModeOfOperation classes - below instead.''' - - # Number of rounds by keysize - number_of_rounds = {16: 10, 24: 12, 32: 14} - - # Round constant words - rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ] - - # S-box and Inverse S-box (S is for Substitution) - S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] - Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] - - # Transformations for encryption - T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ] - T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ] - T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ] - T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ] - - # Transformations for decryption - T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ] - T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ] - T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ] - T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ] - - # Transformations for decryption key expansion - U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ] - U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ] - U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ] - U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ] - - def __init__(self, key): - - if len(key) not in (16, 24, 32): - raise ValueError('Invalid key size') - - rounds = self.number_of_rounds[len(key)] - - # Encryption round keys - self._Ke = [[0] * 4 for i in xrange(rounds + 1)] - - # Decryption round keys - self._Kd = [[0] * 4 for i in xrange(rounds + 1)] - - round_key_count = (rounds + 1) * 4 - KC = len(key) // 4 - - # Convert the key into ints - tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ] - - # Copy values into round key arrays - for i in xrange(0, KC): - self._Ke[i // 4][i % 4] = tk[i] - self._Kd[rounds - (i // 4)][i % 4] = tk[i] - - # Key expansion (fips-197 section 5.2) - rconpointer = 0 - t = KC - while t < round_key_count: - - tt = tk[KC - 1] - tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^ - (self.S[(tt >> 8) & 0xFF] << 16) ^ - (self.S[ tt & 0xFF] << 8) ^ - self.S[(tt >> 24) & 0xFF] ^ - (self.rcon[rconpointer] << 24)) - rconpointer += 1 - - if KC != 8: - for i in xrange(1, KC): - tk[i] ^= tk[i - 1] - - # Key expansion for 256-bit keys is "slightly different" (fips-197) - else: - for i in xrange(1, KC // 2): - tk[i] ^= tk[i - 1] - tt = tk[KC // 2 - 1] - - tk[KC // 2] ^= (self.S[ tt & 0xFF] ^ - (self.S[(tt >> 8) & 0xFF] << 8) ^ - (self.S[(tt >> 16) & 0xFF] << 16) ^ - (self.S[(tt >> 24) & 0xFF] << 24)) - - for i in xrange(KC // 2 + 1, KC): - tk[i] ^= tk[i - 1] - - # Copy values into round key arrays - j = 0 - while j < KC and t < round_key_count: - self._Ke[t // 4][t % 4] = tk[j] - self._Kd[rounds - (t // 4)][t % 4] = tk[j] - j += 1 - t += 1 - - # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3) - for r in xrange(1, rounds): - for j in xrange(0, 4): - tt = self._Kd[r][j] - self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^ - self.U2[(tt >> 16) & 0xFF] ^ - self.U3[(tt >> 8) & 0xFF] ^ - self.U4[ tt & 0xFF]) - - def encrypt(self, plaintext): - 'Encrypt a block of plain text using the AES block cipher.' - - if len(plaintext) != 16: - raise ValueError('wrong block length') - - rounds = len(self._Ke) - 1 - (s1, s2, s3) = [1, 2, 3] - a = [0, 0, 0, 0] - - # Convert plaintext to (ints ^ key) - t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)] - - # Apply round transforms - for r in xrange(1, rounds): - for i in xrange(0, 4): - a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^ - self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^ - self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^ - self.T4[ t[(i + s3) % 4] & 0xFF] ^ - self._Ke[r][i]) - t = copy.copy(a) - - # The last round is special - result = [ ] - for i in xrange(0, 4): - tt = self._Ke[rounds][i] - result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) - result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) - result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) - result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) - - return result - - def decrypt(self, ciphertext): - 'Decrypt a block of cipher text using the AES block cipher.' - - if len(ciphertext) != 16: - raise ValueError('wrong block length') - - rounds = len(self._Kd) - 1 - (s1, s2, s3) = [3, 2, 1] - a = [0, 0, 0, 0] - - # Convert ciphertext to (ints ^ key) - t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)] - - # Apply round transforms - for r in xrange(1, rounds): - for i in xrange(0, 4): - a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^ - self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^ - self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^ - self.T8[ t[(i + s3) % 4] & 0xFF] ^ - self._Kd[r][i]) - t = copy.copy(a) - - # The last round is special - result = [ ] - for i in xrange(0, 4): - tt = self._Kd[rounds][i] - result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) - result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) - result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) - result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) - - return result - - -class Counter(object): - '''A counter object for the Counter (CTR) mode of operation. - - To create a custom counter, you can usually just override the - increment method.''' - - def __init__(self, initial_value = 1): - - # Convert the value into an array of bytes long - self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ] - - value = property(lambda s: s._counter) - - def increment(self): - '''Increment the counter (overflow rolls back to 0).''' - - for i in xrange(len(self._counter) - 1, -1, -1): - self._counter[i] += 1 - - if self._counter[i] < 256: break - - # Carry the one - self._counter[i] = 0 - - # Overflow - else: - self._counter = [ 0 ] * len(self._counter) - - -class AESBlockModeOfOperation(object): - '''Super-class for AES modes of operation that require blocks.''' - def __init__(self, key): - self._aes = AES(key) - - def decrypt(self, ciphertext): - raise Exception('not implemented') - - def encrypt(self, plaintext): - raise Exception('not implemented') - - -class AESStreamModeOfOperation(AESBlockModeOfOperation): - '''Super-class for AES modes of operation that are stream-ciphers.''' - -class AESSegmentModeOfOperation(AESStreamModeOfOperation): - '''Super-class for AES modes of operation that segment data.''' - - segment_bytes = 16 - - - -class AESModeOfOperationECB(AESBlockModeOfOperation): - '''AES Electronic Codebook Mode of Operation. - - o Block-cipher, so data must be padded to 16 byte boundaries - - Security Notes: - o This mode is not recommended - o Any two identical blocks produce identical encrypted values, - exposing data patterns. (See the image of Tux on wikipedia) - - Also see: - o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29 - o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1''' - - - name = "Electronic Codebook (ECB)" - - def encrypt(self, plaintext): - if len(plaintext) != 16: - raise ValueError('plaintext block must be 16 bytes') - - plaintext = _string_to_bytes(plaintext) - return _bytes_to_string(self._aes.encrypt(plaintext)) - - def decrypt(self, ciphertext): - if len(ciphertext) != 16: - raise ValueError('ciphertext block must be 16 bytes') - - ciphertext = _string_to_bytes(ciphertext) - return _bytes_to_string(self._aes.decrypt(ciphertext)) - - - -class AESModeOfOperationCBC(AESBlockModeOfOperation): - '''AES Cipher-Block Chaining Mode of Operation. - - o The Initialization Vector (IV) - o Block-cipher, so data must be padded to 16 byte boundaries - o An incorrect initialization vector will only cause the first - block to be corrupt; all other blocks will be intact - o A corrupt bit in the cipher text will cause a block to be - corrupted, and the next block to be inverted, but all other - blocks will be intact. - - Security Notes: - o This method (and CTR) ARE recommended. - - Also see: - o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 - o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2''' - - - name = "Cipher-Block Chaining (CBC)" - - def __init__(self, key, iv = None): - if iv is None: - self._last_cipherblock = [ 0 ] * 16 - elif len(iv) != 16: - raise ValueError('initialization vector must be 16 bytes') - else: - self._last_cipherblock = _string_to_bytes(iv) - - AESBlockModeOfOperation.__init__(self, key) - - def encrypt(self, plaintext): - if len(plaintext) != 16: - raise ValueError('plaintext block must be 16 bytes') - - plaintext = _string_to_bytes(plaintext) - precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ] - self._last_cipherblock = self._aes.encrypt(precipherblock) - - return _bytes_to_string(self._last_cipherblock) - - def decrypt(self, ciphertext): - if len(ciphertext) != 16: - raise ValueError('ciphertext block must be 16 bytes') - - cipherblock = _string_to_bytes(ciphertext) - plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ] - self._last_cipherblock = cipherblock - - return _bytes_to_string(plaintext) - - - -class AESModeOfOperationCFB(AESSegmentModeOfOperation): - '''AES Cipher Feedback Mode of Operation. - - o A stream-cipher, so input does not need to be padded to blocks, - but does need to be padded to segment_size - - Also see: - o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29 - o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3''' - - - name = "Cipher Feedback (CFB)" - - def __init__(self, key, iv, segment_size = 1): - if segment_size == 0: segment_size = 1 - - if iv is None: - self._shift_register = [ 0 ] * 16 - elif len(iv) != 16: - raise ValueError('initialization vector must be 16 bytes') - else: - self._shift_register = _string_to_bytes(iv) - - self._segment_bytes = segment_size - - AESBlockModeOfOperation.__init__(self, key) - - segment_bytes = property(lambda s: s._segment_bytes) - - def encrypt(self, plaintext): - if len(plaintext) % self._segment_bytes != 0: - raise ValueError('plaintext block must be a multiple of segment_size') - - plaintext = _string_to_bytes(plaintext) - - # Break block into segments - encrypted = [ ] - for i in xrange(0, len(plaintext), self._segment_bytes): - plaintext_segment = plaintext[i: i + self._segment_bytes] - xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)] - cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ] - - # Shift the top bits out and the ciphertext in - self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) - - encrypted.extend(cipher_segment) - - return _bytes_to_string(encrypted) - - def decrypt(self, ciphertext): - if len(ciphertext) % self._segment_bytes != 0: - raise ValueError('ciphertext block must be a multiple of segment_size') - - ciphertext = _string_to_bytes(ciphertext) - - # Break block into segments - decrypted = [ ] - for i in xrange(0, len(ciphertext), self._segment_bytes): - cipher_segment = ciphertext[i: i + self._segment_bytes] - xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)] - plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ] - - # Shift the top bits out and the ciphertext in - self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) - - decrypted.extend(plaintext_segment) - - return _bytes_to_string(decrypted) - - - -class AESModeOfOperationOFB(AESStreamModeOfOperation): - '''AES Output Feedback Mode of Operation. - - o A stream-cipher, so input does not need to be padded to blocks, - allowing arbitrary length data. - o A bit twiddled in the cipher text, twiddles the same bit in the - same bit in the plain text, which can be useful for error - correction techniques. - - Also see: - o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29 - o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4''' - - - name = "Output Feedback (OFB)" - - def __init__(self, key, iv = None): - if iv is None: - self._last_precipherblock = [ 0 ] * 16 - elif len(iv) != 16: - raise ValueError('initialization vector must be 16 bytes') - else: - self._last_precipherblock = _string_to_bytes(iv) - - self._remaining_block = [ ] - - AESBlockModeOfOperation.__init__(self, key) - - def encrypt(self, plaintext): - encrypted = [ ] - for p in _string_to_bytes(plaintext): - if len(self._remaining_block) == 0: - self._remaining_block = self._aes.encrypt(self._last_precipherblock) - self._last_precipherblock = [ ] - precipherbyte = self._remaining_block.pop(0) - self._last_precipherblock.append(precipherbyte) - cipherbyte = p ^ precipherbyte - encrypted.append(cipherbyte) - - return _bytes_to_string(encrypted) - - def decrypt(self, ciphertext): - # AES-OFB is symetric - return self.encrypt(ciphertext) - - - -class AESModeOfOperationCTR(AESStreamModeOfOperation): - '''AES Counter Mode of Operation. - - o A stream-cipher, so input does not need to be padded to blocks, - allowing arbitrary length data. - o The counter must be the same size as the key size (ie. len(key)) - o Each block independant of the other, so a corrupt byte will not - damage future blocks. - o Each block has a uniue counter value associated with it, which - contributes to the encrypted value, so no data patterns are - leaked. - o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and - Segmented Integer Counter (SIC - - Security Notes: - o This method (and CBC) ARE recommended. - o Each message block is associated with a counter value which must be - unique for ALL messages with the same key. Otherwise security may be - compromised. - - Also see: - - o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29 - o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5 - and Appendix B for managing the initial counter''' - - - name = "Counter (CTR)" - - def __init__(self, key, counter = None): - AESBlockModeOfOperation.__init__(self, key) - - if counter is None: - counter = Counter() - - self._counter = counter - self._remaining_counter = [ ] - - def encrypt(self, plaintext): - while len(self._remaining_counter) < len(plaintext): - self._remaining_counter += self._aes.encrypt(self._counter.value) - self._counter.increment() - - plaintext = _string_to_bytes(plaintext) - - encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ] - self._remaining_counter = self._remaining_counter[len(encrypted):] - - return _bytes_to_string(encrypted) - - def decrypt(self, crypttext): - # AES-CTR is symetric - return self.encrypt(crypttext) - - -# Simple lookup table for each mode -AESModesOfOperation = dict( - ctr = AESModeOfOperationCTR, - cbc = AESModeOfOperationCBC, - cfb = AESModeOfOperationCFB, - ecb = AESModeOfOperationECB, - ofb = AESModeOfOperationOFB, -) diff --git a/src/lib/pyaes/blockfeeder.py b/src/lib/pyaes/blockfeeder.py deleted file mode 100644 index b9a904d2..00000000 --- a/src/lib/pyaes/blockfeeder.py +++ /dev/null @@ -1,227 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2014 Richard Moore -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - - -from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation -from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable - - -# First we inject three functions to each of the modes of operations -# -# _can_consume(size) -# - Given a size, determine how many bytes could be consumed in -# a single call to either the decrypt or encrypt method -# -# _final_encrypt(data, padding = PADDING_DEFAULT) -# - call and return encrypt on this (last) chunk of data, -# padding as necessary; this will always be at least 16 -# bytes unless the total incoming input was less than 16 -# bytes -# -# _final_decrypt(data, padding = PADDING_DEFAULT) -# - same as _final_encrypt except for decrypt, for -# stripping off padding -# - -PADDING_NONE = 'none' -PADDING_DEFAULT = 'default' - -# @TODO: Ciphertext stealing and explicit PKCS#7 -# PADDING_CIPHERTEXT_STEALING -# PADDING_PKCS7 - -# ECB and CBC are block-only ciphers - -def _block_can_consume(self, size): - if size >= 16: return 16 - return 0 - -# After padding, we may have more than one block -def _block_final_encrypt(self, data, padding = PADDING_DEFAULT): - if padding == PADDING_DEFAULT: - data = append_PKCS7_padding(data) - - elif padding == PADDING_NONE: - if len(data) != 16: - raise Exception('invalid data length for final block') - else: - raise Exception('invalid padding option') - - if len(data) == 32: - return self.encrypt(data[:16]) + self.encrypt(data[16:]) - - return self.encrypt(data) - - -def _block_final_decrypt(self, data, padding = PADDING_DEFAULT): - if padding == PADDING_DEFAULT: - return strip_PKCS7_padding(self.decrypt(data)) - - if padding == PADDING_NONE: - if len(data) != 16: - raise Exception('invalid data length for final block') - return self.decrypt(data) - - raise Exception('invalid padding option') - -AESBlockModeOfOperation._can_consume = _block_can_consume -AESBlockModeOfOperation._final_encrypt = _block_final_encrypt -AESBlockModeOfOperation._final_decrypt = _block_final_decrypt - - - -# CFB is a segment cipher - -def _segment_can_consume(self, size): - return self.segment_bytes * int(size // self.segment_bytes) - -# CFB can handle a non-segment-sized block at the end using the remaining cipherblock -def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT): - if padding != PADDING_DEFAULT: - raise Exception('invalid padding option') - - faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) - padded = data + to_bufferable(faux_padding) - return self.encrypt(padded)[:len(data)] - -# CFB can handle a non-segment-sized block at the end using the remaining cipherblock -def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT): - if padding != PADDING_DEFAULT: - raise Exception('invalid padding option') - - faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) - padded = data + to_bufferable(faux_padding) - return self.decrypt(padded)[:len(data)] - -AESSegmentModeOfOperation._can_consume = _segment_can_consume -AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt -AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt - - - -# OFB and CTR are stream ciphers - -def _stream_can_consume(self, size): - return size - -def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT): - if padding not in [PADDING_NONE, PADDING_DEFAULT]: - raise Exception('invalid padding option') - - return self.encrypt(data) - -def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT): - if padding not in [PADDING_NONE, PADDING_DEFAULT]: - raise Exception('invalid padding option') - - return self.decrypt(data) - -AESStreamModeOfOperation._can_consume = _stream_can_consume -AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt -AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt - - - -class BlockFeeder(object): - '''The super-class for objects to handle chunking a stream of bytes - into the appropriate block size for the underlying mode of operation - and applying (or stripping) padding, as necessary.''' - - def __init__(self, mode, feed, final, padding = PADDING_DEFAULT): - self._mode = mode - self._feed = feed - self._final = final - self._buffer = to_bufferable("") - self._padding = padding - - def feed(self, data = None): - '''Provide bytes to encrypt (or decrypt), returning any bytes - possible from this or any previous calls to feed. - - Call with None or an empty string to flush the mode of - operation and return any final bytes; no further calls to - feed may be made.''' - - if self._buffer is None: - raise ValueError('already finished feeder') - - # Finalize; process the spare bytes we were keeping - if data is None: - result = self._final(self._buffer, self._padding) - self._buffer = None - return result - - self._buffer += to_bufferable(data) - - # We keep 16 bytes around so we can determine padding - result = to_bufferable('') - while len(self._buffer) > 16: - can_consume = self._mode._can_consume(len(self._buffer) - 16) - if can_consume == 0: break - result += self._feed(self._buffer[:can_consume]) - self._buffer = self._buffer[can_consume:] - - return result - - -class Encrypter(BlockFeeder): - 'Accepts bytes of plaintext and returns encrypted ciphertext.' - - def __init__(self, mode, padding = PADDING_DEFAULT): - BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding) - - -class Decrypter(BlockFeeder): - 'Accepts bytes of ciphertext and returns decrypted plaintext.' - - def __init__(self, mode, padding = PADDING_DEFAULT): - BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding) - - -# 8kb blocks -BLOCK_SIZE = (1 << 13) - -def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE): - 'Uses feeder to read and convert from in_stream and write to out_stream.' - - while True: - chunk = in_stream.read(block_size) - if not chunk: - break - converted = feeder.feed(chunk) - out_stream.write(converted) - converted = feeder.feed() - out_stream.write(converted) - - -def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): - 'Encrypts a stream of bytes from in_stream to out_stream using mode.' - - encrypter = Encrypter(mode, padding = padding) - _feed_stream(encrypter, in_stream, out_stream, block_size) - - -def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): - 'Decrypts a stream of bytes from in_stream to out_stream using mode.' - - decrypter = Decrypter(mode, padding = padding) - _feed_stream(decrypter, in_stream, out_stream, block_size) diff --git a/src/lib/pyaes/util.py b/src/lib/pyaes/util.py deleted file mode 100644 index 081a3759..00000000 --- a/src/lib/pyaes/util.py +++ /dev/null @@ -1,60 +0,0 @@ -# The MIT License (MIT) -# -# Copyright (c) 2014 Richard Moore -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# Why to_bufferable? -# Python 3 is very different from Python 2.x when it comes to strings of text -# and strings of bytes; in Python 3, strings of bytes do not exist, instead to -# represent arbitrary binary data, we must use the "bytes" object. This method -# ensures the object behaves as we need it to. - -def to_bufferable(binary): - return binary - -def _get_byte(c): - return ord(c) - -try: - xrange -except: - - def to_bufferable(binary): - if isinstance(binary, bytes): - return binary - return bytes(ord(b) for b in binary) - - def _get_byte(c): - return c - -def append_PKCS7_padding(data): - pad = 16 - (len(data) % 16) - return data + to_bufferable(chr(pad) * pad) - -def strip_PKCS7_padding(data): - if len(data) % 16 != 0: - raise ValueError("invalid length") - - pad = _get_byte(data[-1]) - - if pad > 16: - raise ValueError("invalid padding byte") - - return data[:-pad] diff --git a/src/lib/pyaes/LICENSE.txt b/src/lib/pybitcointools/LICENSE similarity index 80% rename from src/lib/pyaes/LICENSE.txt rename to src/lib/pybitcointools/LICENSE index 0417a6c2..c47d4ad0 100644 --- a/src/lib/pyaes/LICENSE.txt +++ b/src/lib/pybitcointools/LICENSE @@ -1,6 +1,12 @@ +This code is public domain. Everyone has the right to do whatever they want +with it for any purpose. + +In case your jurisdiction does not consider the above disclaimer valid or +enforceable, here's an MIT license for you: + The MIT License (MIT) -Copyright (c) 2014 Richard Moore +Copyright (c) 2013 Vitalik Buterin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,4 +25,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/src/lib/pybitcointools/__init__.py b/src/lib/pybitcointools/__init__.py new file mode 100644 index 00000000..7d529abc --- /dev/null +++ b/src/lib/pybitcointools/__init__.py @@ -0,0 +1,10 @@ +from .py2specials import * +from .py3specials import * +from .main import * +from .transaction import * +from .deterministic import * +from .bci import * +from .composite import * +from .stealth import * +from .blocks import * +from .mnemonic import * diff --git a/src/lib/pybitcointools/bci.py b/src/lib/pybitcointools/bci.py new file mode 100644 index 00000000..79a2c401 --- /dev/null +++ b/src/lib/pybitcointools/bci.py @@ -0,0 +1,528 @@ +#!/usr/bin/python +import json, re +import random +import sys +try: + from urllib.request import build_opener +except: + from urllib2 import build_opener + + +# Makes a request to a given URL (first arg) and optional params (second arg) +def make_request(*args): + opener = build_opener() + opener.addheaders = [('User-agent', + 'Mozilla/5.0'+str(random.randrange(1000000)))] + try: + return opener.open(*args).read().strip() + except Exception as e: + try: + p = e.read().strip() + except: + p = e + raise Exception(p) + + +def is_testnet(inp): + '''Checks if inp is a testnet address or if UTXO is a known testnet TxID''' + if isinstance(inp, (list, tuple)) and len(inp) >= 1: + return any([is_testnet(x) for x in inp]) + elif not isinstance(inp, basestring): # sanity check + raise TypeError("Input must be str/unicode, not type %s" % str(type(inp))) + + if not inp or (inp.lower() in ("btc", "testnet")): + pass + + ## ADDRESSES + if inp[0] in "123mn": + if re.match("^[2mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp): + return True + elif re.match("^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp): + return False + else: + #sys.stderr.write("Bad address format %s") + return None + + ## TXID + elif re.match('^[0-9a-fA-F]{64}$', inp): + base_url = "http://api.blockcypher.com/v1/btc/{network}/txs/{txid}?includesHex=false" + try: + # try testnet fetchtx + make_request(base_url.format(network="test3", txid=inp.lower())) + return True + except: + # try mainnet fetchtx + make_request(base_url.format(network="main", txid=inp.lower())) + return False + sys.stderr.write("TxID %s has no match for testnet or mainnet (Bad TxID)") + return None + else: + raise TypeError("{0} is unknown input".format(inp)) + + +def set_network(*args): + '''Decides if args for unspent/fetchtx/pushtx are mainnet or testnet''' + r = [] + for arg in args: + if not arg: + pass + if isinstance(arg, basestring): + r.append(is_testnet(arg)) + elif isinstance(arg, (list, tuple)): + return set_network(*arg) + if any(r) and not all(r): + raise Exception("Mixed Testnet/Mainnet queries") + return "testnet" if any(r) else "btc" + + +def parse_addr_args(*args): + # Valid input formats: unspent([addr1, addr2, addr3]) + # unspent([addr1, addr2, addr3], network) + # unspent(addr1, addr2, addr3) + # unspent(addr1, addr2, addr3, network) + addr_args = args + network = "btc" + if len(args) == 0: + return [], 'btc' + if len(args) >= 1 and args[-1] in ('testnet', 'btc'): + network = args[-1] + addr_args = args[:-1] + if len(addr_args) == 1 and isinstance(addr_args, list): + network = set_network(*addr_args[0]) + addr_args = addr_args[0] + if addr_args and isinstance(addr_args, tuple) and isinstance(addr_args[0], list): + addr_args = addr_args[0] + network = set_network(addr_args) + return network, addr_args + + +# Gets the unspent outputs of one or more addresses +def bci_unspent(*args): + network, addrs = parse_addr_args(*args) + u = [] + for a in addrs: + try: + data = make_request('https://blockchain.info/unspent?active='+a) + except Exception as e: + if str(e) == 'No free outputs to spend': + continue + else: + raise Exception(e) + try: + jsonobj = json.loads(data.decode("utf-8")) + for o in jsonobj["unspent_outputs"]: + h = o['tx_hash'].decode('hex')[::-1].encode('hex') + u.append({ + "output": h+':'+str(o['tx_output_n']), + "value": o['value'] + }) + except: + raise Exception("Failed to decode data: "+data) + return u + + +def blockr_unspent(*args): + # Valid input formats: blockr_unspent([addr1, addr2,addr3]) + # blockr_unspent(addr1, addr2, addr3) + # blockr_unspent([addr1, addr2, addr3], network) + # blockr_unspent(addr1, addr2, addr3, network) + # Where network is 'btc' or 'testnet' + network, addr_args = parse_addr_args(*args) + + if network == 'testnet': + blockr_url = 'http://tbtc.blockr.io/api/v1/address/unspent/' + elif network == 'btc': + blockr_url = 'http://btc.blockr.io/api/v1/address/unspent/' + else: + raise Exception( + 'Unsupported network {0} for blockr_unspent'.format(network)) + + if len(addr_args) == 0: + return [] + elif isinstance(addr_args[0], list): + addrs = addr_args[0] + else: + addrs = addr_args + res = make_request(blockr_url+','.join(addrs)) + data = json.loads(res.decode("utf-8"))['data'] + o = [] + if 'unspent' in data: + data = [data] + for dat in data: + for u in dat['unspent']: + o.append({ + "output": u['tx']+':'+str(u['n']), + "value": int(u['amount'].replace('.', '')) + }) + return o + + +def helloblock_unspent(*args): + addrs, network = parse_addr_args(*args) + if network == 'testnet': + url = 'https://testnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s' + elif network == 'btc': + url = 'https://mainnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s' + o = [] + for addr in addrs: + for offset in xrange(0, 10**9, 500): + res = make_request(url % (addr, offset)) + data = json.loads(res.decode("utf-8"))["data"] + if not len(data["unspents"]): + break + elif offset: + sys.stderr.write("Getting more unspents: %d\n" % offset) + for dat in data["unspents"]: + o.append({ + "output": dat["txHash"]+':'+str(dat["index"]), + "value": dat["value"], + }) + return o + + +unspent_getters = { + 'bci': bci_unspent, + 'blockr': blockr_unspent, + 'helloblock': helloblock_unspent +} + + +def unspent(*args, **kwargs): + f = unspent_getters.get(kwargs.get('source', ''), bci_unspent) + return f(*args) + + +# Gets the transaction output history of a given set of addresses, +# including whether or not they have been spent +def history(*args): + # Valid input formats: history([addr1, addr2,addr3]) + # history(addr1, addr2, addr3) + if len(args) == 0: + return [] + elif isinstance(args[0], list): + addrs = args[0] + else: + addrs = args + + txs = [] + for addr in addrs: + offset = 0 + while 1: + gathered = False + while not gathered: + try: + data = make_request( + 'https://blockchain.info/address/%s?format=json&offset=%s' % + (addr, offset)) + gathered = True + except Exception as e: + try: + sys.stderr.write(e.read().strip()) + except: + sys.stderr.write(str(e)) + gathered = False + try: + jsonobj = json.loads(data.decode("utf-8")) + except: + raise Exception("Failed to decode data: "+data) + txs.extend(jsonobj["txs"]) + if len(jsonobj["txs"]) < 50: + break + offset += 50 + sys.stderr.write("Fetching more transactions... "+str(offset)+'\n') + outs = {} + for tx in txs: + for o in tx["out"]: + if o.get('addr', None) in addrs: + key = str(tx["tx_index"])+':'+str(o["n"]) + outs[key] = { + "address": o["addr"], + "value": o["value"], + "output": tx["hash"]+':'+str(o["n"]), + "block_height": tx.get("block_height", None) + } + for tx in txs: + for i, inp in enumerate(tx["inputs"]): + if "prev_out" in inp: + if inp["prev_out"].get("addr", None) in addrs: + key = str(inp["prev_out"]["tx_index"]) + \ + ':'+str(inp["prev_out"]["n"]) + if outs.get(key): + outs[key]["spend"] = tx["hash"]+':'+str(i) + return [outs[k] for k in outs] + + +# Pushes a transaction to the network using https://blockchain.info/pushtx +def bci_pushtx(tx): + if not re.match('^[0-9a-fA-F]*$', tx): + tx = tx.encode('hex') + return make_request('https://blockchain.info/pushtx', 'tx='+tx) + + +def eligius_pushtx(tx): + if not re.match('^[0-9a-fA-F]*$', tx): + tx = tx.encode('hex') + s = make_request( + 'http://eligius.st/~wizkid057/newstats/pushtxn.php', + 'transaction='+tx+'&send=Push') + strings = re.findall('string[^"]*"[^"]*"', s) + for string in strings: + quote = re.findall('"[^"]*"', string)[0] + if len(quote) >= 5: + return quote[1:-1] + + +def blockr_pushtx(tx, network='btc'): + if network == 'testnet': + blockr_url = 'http://tbtc.blockr.io/api/v1/tx/push' + elif network == 'btc': + blockr_url = 'http://btc.blockr.io/api/v1/tx/push' + else: + raise Exception( + 'Unsupported network {0} for blockr_pushtx'.format(network)) + + if not re.match('^[0-9a-fA-F]*$', tx): + tx = tx.encode('hex') + return make_request(blockr_url, '{"hex":"%s"}' % tx) + + +def helloblock_pushtx(tx): + if not re.match('^[0-9a-fA-F]*$', tx): + tx = tx.encode('hex') + return make_request('https://mainnet.helloblock.io/v1/transactions', + 'rawTxHex='+tx) + +pushtx_getters = { + 'bci': bci_pushtx, + 'blockr': blockr_pushtx, + 'helloblock': helloblock_pushtx +} + + +def pushtx(*args, **kwargs): + f = pushtx_getters.get(kwargs.get('source', ''), bci_pushtx) + return f(*args) + + +def last_block_height(network='btc'): + if network == 'testnet': + data = make_request('http://tbtc.blockr.io/api/v1/block/info/last') + jsonobj = json.loads(data.decode("utf-8")) + return jsonobj["data"]["nb"] + + data = make_request('https://blockchain.info/latestblock') + jsonobj = json.loads(data.decode("utf-8")) + return jsonobj["height"] + + +# Gets a specific transaction +def bci_fetchtx(txhash): + if isinstance(txhash, list): + return [bci_fetchtx(h) for h in txhash] + if not re.match('^[0-9a-fA-F]*$', txhash): + txhash = txhash.encode('hex') + data = make_request('https://blockchain.info/rawtx/'+txhash+'?format=hex') + return data + + +def blockr_fetchtx(txhash, network='btc'): + if network == 'testnet': + blockr_url = 'http://tbtc.blockr.io/api/v1/tx/raw/' + elif network == 'btc': + blockr_url = 'http://btc.blockr.io/api/v1/tx/raw/' + else: + raise Exception( + 'Unsupported network {0} for blockr_fetchtx'.format(network)) + if isinstance(txhash, list): + txhash = ','.join([x.encode('hex') if not re.match('^[0-9a-fA-F]*$', x) + else x for x in txhash]) + jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8")) + return [d['tx']['hex'] for d in jsondata['data']] + else: + if not re.match('^[0-9a-fA-F]*$', txhash): + txhash = txhash.encode('hex') + jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8")) + return jsondata['data']['tx']['hex'] + + +def helloblock_fetchtx(txhash, network='btc'): + if isinstance(txhash, list): + return [helloblock_fetchtx(h) for h in txhash] + if not re.match('^[0-9a-fA-F]*$', txhash): + txhash = txhash.encode('hex') + if network == 'testnet': + url = 'https://testnet.helloblock.io/v1/transactions/' + elif network == 'btc': + url = 'https://mainnet.helloblock.io/v1/transactions/' + else: + raise Exception( + 'Unsupported network {0} for helloblock_fetchtx'.format(network)) + data = json.loads(make_request(url + txhash).decode("utf-8"))["data"]["transaction"] + o = { + "locktime": data["locktime"], + "version": data["version"], + "ins": [], + "outs": [] + } + for inp in data["inputs"]: + o["ins"].append({ + "script": inp["scriptSig"], + "outpoint": { + "index": inp["prevTxoutIndex"], + "hash": inp["prevTxHash"], + }, + "sequence": 4294967295 + }) + for outp in data["outputs"]: + o["outs"].append({ + "value": outp["value"], + "script": outp["scriptPubKey"] + }) + from .transaction import serialize + from .transaction import txhash as TXHASH + tx = serialize(o) + assert TXHASH(tx) == txhash + return tx + + +fetchtx_getters = { + 'bci': bci_fetchtx, + 'blockr': blockr_fetchtx, + 'helloblock': helloblock_fetchtx +} + + +def fetchtx(*args, **kwargs): + f = fetchtx_getters.get(kwargs.get('source', ''), bci_fetchtx) + return f(*args) + + +def firstbits(address): + if len(address) >= 25: + return make_request('https://blockchain.info/q/getfirstbits/'+address) + else: + return make_request( + 'https://blockchain.info/q/resolvefirstbits/'+address) + + +def get_block_at_height(height): + j = json.loads(make_request("https://blockchain.info/block-height/" + + str(height)+"?format=json").decode("utf-8")) + for b in j['blocks']: + if b['main_chain'] is True: + return b + raise Exception("Block at this height not found") + + +def _get_block(inp): + if len(str(inp)) < 64: + return get_block_at_height(inp) + else: + return json.loads(make_request( + 'https://blockchain.info/rawblock/'+inp).decode("utf-8")) + + +def bci_get_block_header_data(inp): + j = _get_block(inp) + return { + 'version': j['ver'], + 'hash': j['hash'], + 'prevhash': j['prev_block'], + 'timestamp': j['time'], + 'merkle_root': j['mrkl_root'], + 'bits': j['bits'], + 'nonce': j['nonce'], + } + +def blockr_get_block_header_data(height, network='btc'): + if network == 'testnet': + blockr_url = "http://tbtc.blockr.io/api/v1/block/raw/" + elif network == 'btc': + blockr_url = "http://btc.blockr.io/api/v1/block/raw/" + else: + raise Exception( + 'Unsupported network {0} for blockr_get_block_header_data'.format(network)) + + k = json.loads(make_request(blockr_url + str(height)).decode("utf-8")) + j = k['data'] + return { + 'version': j['version'], + 'hash': j['hash'], + 'prevhash': j['previousblockhash'], + 'timestamp': j['time'], + 'merkle_root': j['merkleroot'], + 'bits': int(j['bits'], 16), + 'nonce': j['nonce'], + } + + +def get_block_timestamp(height, network='btc'): + if network == 'testnet': + blockr_url = "http://tbtc.blockr.io/api/v1/block/info/" + elif network == 'btc': + blockr_url = "http://btc.blockr.io/api/v1/block/info/" + else: + raise Exception( + 'Unsupported network {0} for get_block_timestamp'.format(network)) + + import time, calendar + if isinstance(height, list): + k = json.loads(make_request(blockr_url + ','.join([str(x) for x in height])).decode("utf-8")) + o = {x['nb']: calendar.timegm(time.strptime(x['time_utc'], + "%Y-%m-%dT%H:%M:%SZ")) for x in k['data']} + return [o[x] for x in height] + else: + k = json.loads(make_request(blockr_url + str(height)).decode("utf-8")) + j = k['data']['time_utc'] + return calendar.timegm(time.strptime(j, "%Y-%m-%dT%H:%M:%SZ")) + + +block_header_data_getters = { + 'bci': bci_get_block_header_data, + 'blockr': blockr_get_block_header_data +} + + +def get_block_header_data(inp, **kwargs): + f = block_header_data_getters.get(kwargs.get('source', ''), + bci_get_block_header_data) + return f(inp, **kwargs) + + +def get_txs_in_block(inp): + j = _get_block(inp) + hashes = [t['hash'] for t in j['tx']] + return hashes + + +def get_block_height(txhash): + j = json.loads(make_request('https://blockchain.info/rawtx/'+txhash).decode("utf-8")) + return j['block_height'] + +# fromAddr, toAddr, 12345, changeAddress +def get_tx_composite(inputs, outputs, output_value, change_address=None, network=None): + """mktx using blockcypher API""" + inputs = [inputs] if not isinstance(inputs, list) else inputs + outputs = [outputs] if not isinstance(outputs, list) else outputs + network = set_network(change_address or inputs) if not network else network.lower() + url = "http://api.blockcypher.com/v1/btc/{network}/txs/new?includeToSignTx=true".format( + network=('test3' if network=='testnet' else 'main')) + is_address = lambda a: bool(re.match("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", a)) + if any([is_address(x) for x in inputs]): + inputs_type = 'addresses' # also accepts UTXOs, only addresses supported presently + if any([is_address(x) for x in outputs]): + outputs_type = 'addresses' # TODO: add UTXO support + data = { + 'inputs': [{inputs_type: inputs}], + 'confirmations': 0, + 'preference': 'high', + 'outputs': [{outputs_type: outputs, "value": output_value}] + } + if change_address: + data["change_address"] = change_address # + jdata = json.loads(make_request(url, data)) + hash, txh = jdata.get("tosign")[0], jdata.get("tosign_tx")[0] + assert bin_dbl_sha256(txh.decode('hex')).encode('hex') == hash, "checksum mismatch %s" % hash + return txh.encode("utf-8") + +blockcypher_mktx = get_tx_composite diff --git a/src/lib/pybitcointools/blocks.py b/src/lib/pybitcointools/blocks.py new file mode 100644 index 00000000..9df6b35c --- /dev/null +++ b/src/lib/pybitcointools/blocks.py @@ -0,0 +1,50 @@ +from .main import * + + +def serialize_header(inp): + o = encode(inp['version'], 256, 4)[::-1] + \ + inp['prevhash'].decode('hex')[::-1] + \ + inp['merkle_root'].decode('hex')[::-1] + \ + encode(inp['timestamp'], 256, 4)[::-1] + \ + encode(inp['bits'], 256, 4)[::-1] + \ + encode(inp['nonce'], 256, 4)[::-1] + h = bin_sha256(bin_sha256(o))[::-1].encode('hex') + assert h == inp['hash'], (sha256(o), inp['hash']) + return o.encode('hex') + + +def deserialize_header(inp): + inp = inp.decode('hex') + return { + "version": decode(inp[:4][::-1], 256), + "prevhash": inp[4:36][::-1].encode('hex'), + "merkle_root": inp[36:68][::-1].encode('hex'), + "timestamp": decode(inp[68:72][::-1], 256), + "bits": decode(inp[72:76][::-1], 256), + "nonce": decode(inp[76:80][::-1], 256), + "hash": bin_sha256(bin_sha256(inp))[::-1].encode('hex') + } + + +def mk_merkle_proof(header, hashes, index): + nodes = [h.decode('hex')[::-1] for h in hashes] + if len(nodes) % 2 and len(nodes) > 2: + nodes.append(nodes[-1]) + layers = [nodes] + while len(nodes) > 1: + newnodes = [] + for i in range(0, len(nodes) - 1, 2): + newnodes.append(bin_sha256(bin_sha256(nodes[i] + nodes[i+1]))) + if len(newnodes) % 2 and len(newnodes) > 2: + newnodes.append(newnodes[-1]) + nodes = newnodes + layers.append(nodes) + # Sanity check, make sure merkle root is valid + assert nodes[0][::-1].encode('hex') == header['merkle_root'] + merkle_siblings = \ + [layers[i][(index >> i) ^ 1] for i in range(len(layers)-1)] + return { + "hash": hashes[index], + "siblings": [x[::-1].encode('hex') for x in merkle_siblings], + "header": header + } diff --git a/src/lib/pybitcointools/composite.py b/src/lib/pybitcointools/composite.py new file mode 100644 index 00000000..e5d50492 --- /dev/null +++ b/src/lib/pybitcointools/composite.py @@ -0,0 +1,128 @@ +from .main import * +from .transaction import * +from .bci import * +from .deterministic import * +from .blocks import * + + +# Takes privkey, address, value (satoshis), fee (satoshis) +def send(frm, to, value, fee=10000, **kwargs): + return sendmultitx(frm, to + ":" + str(value), fee, **kwargs) + + +# Takes privkey, "address1:value1,address2:value2" (satoshis), fee (satoshis) +def sendmultitx(frm, *args, **kwargs): + tv, fee = args[:-1], int(args[-1]) + outs = [] + outvalue = 0 + for a in tv: + outs.append(a) + outvalue += int(a.split(":")[1]) + + u = unspent(privtoaddr(frm), **kwargs) + u2 = select(u, int(outvalue)+int(fee)) + argz = u2 + outs + [privtoaddr(frm), fee] + tx = mksend(*argz) + tx2 = signall(tx, frm) + return pushtx(tx2, **kwargs) + + +# Takes address, address, value (satoshis), fee(satoshis) +def preparetx(frm, to, value, fee=10000, **kwargs): + tovalues = to + ":" + str(value) + return preparemultitx(frm, tovalues, fee, **kwargs) + + +# Takes address, address:value, address:value ... (satoshis), fee(satoshis) +def preparemultitx(frm, *args, **kwargs): + tv, fee = args[:-1], int(args[-1]) + outs = [] + outvalue = 0 + for a in tv: + outs.append(a) + outvalue += int(a.split(":")[1]) + + u = unspent(frm, **kwargs) + u2 = select(u, int(outvalue)+int(fee)) + argz = u2 + outs + [frm, fee] + return mksend(*argz) + + +# BIP32 hierarchical deterministic multisig script +def bip32_hdm_script(*args): + if len(args) == 3: + keys, req, path = args + else: + i, keys, path = 0, [], [] + while len(args[i]) > 40: + keys.append(args[i]) + i += 1 + req = int(args[i]) + path = map(int, args[i+1:]) + pubs = sorted(map(lambda x: bip32_descend(x, path), keys)) + return mk_multisig_script(pubs, req) + + +# BIP32 hierarchical deterministic multisig address +def bip32_hdm_addr(*args): + return scriptaddr(bip32_hdm_script(*args)) + + +# Setup a coinvault transaction +def setup_coinvault_tx(tx, script): + txobj = deserialize(tx) + N = deserialize_script(script)[-2] + for inp in txobj["ins"]: + inp["script"] = serialize_script([None] * (N+1) + [script]) + return serialize(txobj) + + +# Sign a coinvault transaction +def sign_coinvault_tx(tx, priv): + pub = privtopub(priv) + txobj = deserialize(tx) + subscript = deserialize_script(txobj['ins'][0]['script']) + oscript = deserialize_script(subscript[-1]) + k, pubs = oscript[0], oscript[1:-2] + for j in range(len(txobj['ins'])): + scr = deserialize_script(txobj['ins'][j]['script']) + for i, p in enumerate(pubs): + if p == pub: + scr[i+1] = multisign(tx, j, subscript[-1], priv) + if len(filter(lambda x: x, scr[1:-1])) >= k: + scr = [None] + filter(lambda x: x, scr[1:-1])[:k] + [scr[-1]] + txobj['ins'][j]['script'] = serialize_script(scr) + return serialize(txobj) + + +# Inspects a transaction +def inspect(tx, **kwargs): + d = deserialize(tx) + isum = 0 + ins = {} + for _in in d['ins']: + h = _in['outpoint']['hash'] + i = _in['outpoint']['index'] + prevout = deserialize(fetchtx(h, **kwargs))['outs'][i] + isum += prevout['value'] + a = script_to_address(prevout['script']) + ins[a] = ins.get(a, 0) + prevout['value'] + outs = [] + osum = 0 + for _out in d['outs']: + outs.append({'address': script_to_address(_out['script']), + 'value': _out['value']}) + osum += _out['value'] + return { + 'fee': isum - osum, + 'outs': outs, + 'ins': ins + } + + +def merkle_prove(txhash): + blocknum = str(get_block_height(txhash)) + header = get_block_header_data(blocknum) + hashes = get_txs_in_block(blocknum) + i = hashes.index(txhash) + return mk_merkle_proof(header, hashes, i) diff --git a/src/lib/pybitcointools/deterministic.py b/src/lib/pybitcointools/deterministic.py new file mode 100644 index 00000000..b2bdbbc6 --- /dev/null +++ b/src/lib/pybitcointools/deterministic.py @@ -0,0 +1,199 @@ +from .main import * +import hmac +import hashlib +from binascii import hexlify +# Electrum wallets + + +def electrum_stretch(seed): + return slowsha(seed) + +# Accepts seed or stretched seed, returns master public key + + +def electrum_mpk(seed): + if len(seed) == 32: + seed = electrum_stretch(seed) + return privkey_to_pubkey(seed)[2:] + +# Accepts (seed or stretched seed), index and secondary index +# (conventionally 0 for ordinary addresses, 1 for change) , returns privkey + + +def electrum_privkey(seed, n, for_change=0): + if len(seed) == 32: + seed = electrum_stretch(seed) + mpk = electrum_mpk(seed) + offset = dbl_sha256(from_int_representation_to_bytes(n)+b':'+from_int_representation_to_bytes(for_change)+b':'+binascii.unhexlify(mpk)) + return add_privkeys(seed, offset) + +# Accepts (seed or stretched seed or master pubkey), index and secondary index +# (conventionally 0 for ordinary addresses, 1 for change) , returns pubkey + + +def electrum_pubkey(masterkey, n, for_change=0): + if len(masterkey) == 32: + mpk = electrum_mpk(electrum_stretch(masterkey)) + elif len(masterkey) == 64: + mpk = electrum_mpk(masterkey) + else: + mpk = masterkey + bin_mpk = encode_pubkey(mpk, 'bin_electrum') + offset = bin_dbl_sha256(from_int_representation_to_bytes(n)+b':'+from_int_representation_to_bytes(for_change)+b':'+bin_mpk) + return add_pubkeys('04'+mpk, privtopub(offset)) + +# seed/stretched seed/pubkey -> address (convenience method) + + +def electrum_address(masterkey, n, for_change=0, version=0): + return pubkey_to_address(electrum_pubkey(masterkey, n, for_change), version) + +# Given a master public key, a private key from that wallet and its index, +# cracks the secret exponent which can be used to generate all other private +# keys in the wallet + + +def crack_electrum_wallet(mpk, pk, n, for_change=0): + bin_mpk = encode_pubkey(mpk, 'bin_electrum') + offset = dbl_sha256(str(n)+':'+str(for_change)+':'+bin_mpk) + return subtract_privkeys(pk, offset) + +# Below code ASSUMES binary inputs and compressed pubkeys +MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' +MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' +TESTNET_PRIVATE = b'\x04\x35\x83\x94' +TESTNET_PUBLIC = b'\x04\x35\x87\xCF' +PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE] +PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC] + +# BIP32 child key derivation + + +def raw_bip32_ckd(rawtuple, i): + vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple + i = int(i) + + if vbytes in PRIVATE: + priv = key + pub = privtopub(key) + else: + pub = key + + if i >= 2**31: + if vbytes in PUBLIC: + raise Exception("Can't do private derivation on public key!") + I = hmac.new(chaincode, b'\x00'+priv[:32]+encode(i, 256, 4), hashlib.sha512).digest() + else: + I = hmac.new(chaincode, pub+encode(i, 256, 4), hashlib.sha512).digest() + + if vbytes in PRIVATE: + newkey = add_privkeys(I[:32]+B'\x01', priv) + fingerprint = bin_hash160(privtopub(key))[:4] + if vbytes in PUBLIC: + newkey = add_pubkeys(compress(privtopub(I[:32])), key) + fingerprint = bin_hash160(key)[:4] + + return (vbytes, depth + 1, fingerprint, i, I[32:], newkey) + + +def bip32_serialize(rawtuple): + vbytes, depth, fingerprint, i, chaincode, key = rawtuple + i = encode(i, 256, 4) + chaincode = encode(hash_to_int(chaincode), 256, 32) + keydata = b'\x00'+key[:-1] if vbytes in PRIVATE else key + bindata = vbytes + from_int_to_byte(depth % 256) + fingerprint + i + chaincode + keydata + return changebase(bindata+bin_dbl_sha256(bindata)[:4], 256, 58) + + +def bip32_deserialize(data): + dbin = changebase(data, 58, 256) + if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]: + raise Exception("Invalid checksum") + vbytes = dbin[0:4] + depth = from_byte_to_int(dbin[4]) + fingerprint = dbin[5:9] + i = decode(dbin[9:13], 256) + chaincode = dbin[13:45] + key = dbin[46:78]+b'\x01' if vbytes in PRIVATE else dbin[45:78] + return (vbytes, depth, fingerprint, i, chaincode, key) + + +def raw_bip32_privtopub(rawtuple): + vbytes, depth, fingerprint, i, chaincode, key = rawtuple + newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC + return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key)) + + +def bip32_privtopub(data): + return bip32_serialize(raw_bip32_privtopub(bip32_deserialize(data))) + + +def bip32_ckd(data, i): + return bip32_serialize(raw_bip32_ckd(bip32_deserialize(data), i)) + + +def bip32_master_key(seed, vbytes=MAINNET_PRIVATE): + I = hmac.new(from_string_to_bytes("Bitcoin seed"), seed, hashlib.sha512).digest() + return bip32_serialize((vbytes, 0, b'\x00'*4, 0, I[32:], I[:32]+b'\x01')) + + +def bip32_bin_extract_key(data): + return bip32_deserialize(data)[-1] + + +def bip32_extract_key(data): + return safe_hexlify(bip32_deserialize(data)[-1]) + +# Exploits the same vulnerability as above in Electrum wallets +# Takes a BIP32 pubkey and one of the child privkeys of its corresponding +# privkey and returns the BIP32 privkey associated with that pubkey + + +def raw_crack_bip32_privkey(parent_pub, priv): + vbytes, depth, fingerprint, i, chaincode, key = priv + pvbytes, pdepth, pfingerprint, pi, pchaincode, pkey = parent_pub + i = int(i) + + if i >= 2**31: + raise Exception("Can't crack private derivation!") + + I = hmac.new(pchaincode, pkey+encode(i, 256, 4), hashlib.sha512).digest() + + pprivkey = subtract_privkeys(key, I[:32]+b'\x01') + + newvbytes = MAINNET_PRIVATE if vbytes == MAINNET_PUBLIC else TESTNET_PRIVATE + return (newvbytes, pdepth, pfingerprint, pi, pchaincode, pprivkey) + + +def crack_bip32_privkey(parent_pub, priv): + dsppub = bip32_deserialize(parent_pub) + dspriv = bip32_deserialize(priv) + return bip32_serialize(raw_crack_bip32_privkey(dsppub, dspriv)) + + +def coinvault_pub_to_bip32(*args): + if len(args) == 1: + args = args[0].split(' ') + vals = map(int, args[34:]) + I1 = ''.join(map(chr, vals[:33])) + I2 = ''.join(map(chr, vals[35:67])) + return bip32_serialize((MAINNET_PUBLIC, 0, b'\x00'*4, 0, I2, I1)) + + +def coinvault_priv_to_bip32(*args): + if len(args) == 1: + args = args[0].split(' ') + vals = map(int, args[34:]) + I2 = ''.join(map(chr, vals[35:67])) + I3 = ''.join(map(chr, vals[72:104])) + return bip32_serialize((MAINNET_PRIVATE, 0, b'\x00'*4, 0, I2, I3+b'\x01')) + + +def bip32_descend(*args): + if len(args) == 2 and isinstance(args[1], list): + key, path = args + else: + key, path = args[0], map(int, args[1:]) + for p in path: + key = bip32_ckd(key, p) + return bip32_extract_key(key) diff --git a/src/lib/pybitcointools/english.txt b/src/lib/pybitcointools/english.txt new file mode 100644 index 00000000..942040ed --- /dev/null +++ b/src/lib/pybitcointools/english.txt @@ -0,0 +1,2048 @@ +abandon +ability +able +about +above +absent +absorb +abstract +absurd +abuse +access +accident +account +accuse +achieve +acid +acoustic +acquire +across +act +action +actor +actress +actual +adapt +add +addict +address +adjust +admit +adult +advance +advice +aerobic +affair +afford +afraid +again +age +agent +agree +ahead +aim +air +airport +aisle +alarm +album +alcohol +alert +alien +all +alley +allow +almost +alone +alpha +already +also +alter +always +amateur +amazing +among +amount +amused +analyst +anchor +ancient +anger +angle +angry +animal +ankle +announce +annual +another +answer +antenna +antique +anxiety +any +apart +apology +appear +apple +approve +april +arch +arctic +area +arena +argue +arm +armed +armor +army +around +arrange +arrest +arrive +arrow +art +artefact +artist +artwork +ask +aspect +assault +asset +assist +assume +asthma +athlete +atom +attack +attend +attitude +attract +auction +audit +august +aunt +author +auto +autumn +average +avocado +avoid +awake +aware +away +awesome +awful +awkward +axis +baby +bachelor +bacon +badge +bag +balance +balcony +ball +bamboo +banana +banner +bar +barely +bargain +barrel +base +basic +basket +battle +beach +bean +beauty +because +become +beef +before +begin +behave +behind +believe +below +belt +bench +benefit +best +betray +better +between +beyond +bicycle +bid +bike +bind +biology +bird +birth +bitter +black +blade +blame +blanket +blast +bleak +bless +blind +blood +blossom +blouse +blue +blur +blush +board +boat +body +boil +bomb +bone +bonus +book +boost +border +boring +borrow +boss +bottom +bounce +box +boy +bracket +brain +brand +brass +brave +bread +breeze +brick +bridge +brief +bright +bring +brisk +broccoli +broken +bronze +broom +brother +brown +brush +bubble +buddy +budget +buffalo +build +bulb +bulk +bullet +bundle +bunker +burden +burger +burst +bus +business +busy +butter +buyer +buzz +cabbage +cabin +cable +cactus +cage +cake +call +calm +camera +camp +can +canal +cancel +candy +cannon +canoe +canvas +canyon +capable +capital +captain +car +carbon +card +cargo +carpet +carry +cart +case +cash +casino +castle +casual +cat +catalog +catch +category +cattle +caught +cause +caution +cave +ceiling +celery +cement +census +century +cereal +certain +chair +chalk +champion +change +chaos +chapter +charge +chase +chat +cheap +check +cheese +chef +cherry +chest +chicken +chief +child +chimney +choice +choose +chronic +chuckle +chunk +churn +cigar +cinnamon +circle +citizen +city +civil +claim +clap +clarify +claw +clay +clean +clerk +clever +click +client +cliff +climb +clinic +clip +clock +clog +close +cloth +cloud +clown +club +clump +cluster +clutch +coach +coast +coconut +code +coffee +coil +coin +collect +color +column +combine +come +comfort +comic +common +company +concert +conduct +confirm +congress +connect +consider +control +convince +cook +cool +copper +copy +coral +core +corn +correct +cost +cotton +couch +country +couple +course +cousin +cover +coyote +crack +cradle +craft +cram +crane +crash +crater +crawl +crazy +cream +credit +creek +crew +cricket +crime +crisp +critic +crop +cross +crouch +crowd +crucial +cruel +cruise +crumble +crunch +crush +cry +crystal +cube +culture +cup +cupboard +curious +current +curtain +curve +cushion +custom +cute +cycle +dad +damage +damp +dance +danger +daring +dash +daughter +dawn +day +deal +debate +debris +decade +december +decide +decline +decorate +decrease +deer +defense +define +defy +degree +delay +deliver +demand +demise +denial +dentist +deny +depart +depend +deposit +depth +deputy +derive +describe +desert +design +desk +despair +destroy +detail +detect +develop +device +devote +diagram +dial +diamond +diary +dice +diesel +diet +differ +digital +dignity +dilemma +dinner +dinosaur +direct +dirt +disagree +discover +disease +dish +dismiss +disorder +display +distance +divert +divide +divorce +dizzy +doctor +document +dog +doll +dolphin +domain +donate +donkey +donor +door +dose +double +dove +draft +dragon +drama +drastic +draw +dream +dress +drift +drill +drink +drip +drive +drop +drum +dry +duck +dumb +dune +during +dust +dutch +duty +dwarf +dynamic +eager +eagle +early +earn +earth +easily +east +easy +echo +ecology +economy +edge +edit +educate +effort +egg +eight +either +elbow +elder +electric +elegant +element +elephant +elevator +elite +else +embark +embody +embrace +emerge +emotion +employ +empower +empty +enable +enact +end +endless +endorse +enemy +energy +enforce +engage +engine +enhance +enjoy +enlist +enough +enrich +enroll +ensure +enter +entire +entry +envelope +episode +equal +equip +era +erase +erode +erosion +error +erupt +escape +essay +essence +estate +eternal +ethics +evidence +evil +evoke +evolve +exact +example +excess +exchange +excite +exclude +excuse +execute +exercise +exhaust +exhibit +exile +exist +exit +exotic +expand +expect +expire +explain +expose +express +extend +extra +eye +eyebrow +fabric +face +faculty +fade +faint +faith +fall +false +fame +family +famous +fan +fancy +fantasy +farm +fashion +fat +fatal +father +fatigue +fault +favorite +feature +february +federal +fee +feed +feel +female +fence +festival +fetch +fever +few +fiber +fiction +field +figure +file +film +filter +final +find +fine +finger +finish +fire +firm +first +fiscal +fish +fit +fitness +fix +flag +flame +flash +flat +flavor +flee +flight +flip +float +flock +floor +flower +fluid +flush +fly +foam +focus +fog +foil +fold +follow +food +foot +force +forest +forget +fork +fortune +forum +forward +fossil +foster +found +fox +fragile +frame +frequent +fresh +friend +fringe +frog +front +frost +frown +frozen +fruit +fuel +fun +funny +furnace +fury +future +gadget +gain +galaxy +gallery +game +gap +garage +garbage +garden +garlic +garment +gas +gasp +gate +gather +gauge +gaze +general +genius +genre +gentle +genuine +gesture +ghost +giant +gift +giggle +ginger +giraffe +girl +give +glad +glance +glare +glass +glide +glimpse +globe +gloom +glory +glove +glow +glue +goat +goddess +gold +good +goose +gorilla +gospel +gossip +govern +gown +grab +grace +grain +grant +grape +grass +gravity +great +green +grid +grief +grit +grocery +group +grow +grunt +guard +guess +guide +guilt +guitar +gun +gym +habit +hair +half +hammer +hamster +hand +happy +harbor +hard +harsh +harvest +hat +have +hawk +hazard +head +health +heart +heavy +hedgehog +height +hello +helmet +help +hen +hero +hidden +high +hill +hint +hip +hire +history +hobby +hockey +hold +hole +holiday +hollow +home +honey +hood +hope +horn +horror +horse +hospital +host +hotel +hour +hover +hub +huge +human +humble +humor +hundred +hungry +hunt +hurdle +hurry +hurt +husband +hybrid +ice +icon +idea +identify +idle +ignore +ill +illegal +illness +image +imitate +immense +immune +impact +impose +improve +impulse +inch +include +income +increase +index +indicate +indoor +industry +infant +inflict +inform +inhale +inherit +initial +inject +injury +inmate +inner +innocent +input +inquiry +insane +insect +inside +inspire +install +intact +interest +into +invest +invite +involve +iron +island +isolate +issue +item +ivory +jacket +jaguar +jar +jazz +jealous +jeans +jelly +jewel +job +join +joke +journey +joy +judge +juice +jump +jungle +junior +junk +just +kangaroo +keen +keep +ketchup +key +kick +kid +kidney +kind +kingdom +kiss +kit +kitchen +kite +kitten +kiwi +knee +knife +knock +know +lab +label +labor +ladder +lady +lake +lamp +language +laptop +large +later +latin +laugh +laundry +lava +law +lawn +lawsuit +layer +lazy +leader +leaf +learn +leave +lecture +left +leg +legal +legend +leisure +lemon +lend +length +lens +leopard +lesson +letter +level +liar +liberty +library +license +life +lift +light +like +limb +limit +link +lion +liquid +list +little +live +lizard +load +loan +lobster +local +lock +logic +lonely +long +loop +lottery +loud +lounge +love +loyal +lucky +luggage +lumber +lunar +lunch +luxury +lyrics +machine +mad +magic +magnet +maid +mail +main +major +make +mammal +man +manage +mandate +mango +mansion +manual +maple +marble +march +margin +marine +market +marriage +mask +mass +master +match +material +math +matrix +matter +maximum +maze +meadow +mean +measure +meat +mechanic +medal +media +melody +melt +member +memory +mention +menu +mercy +merge +merit +merry +mesh +message +metal +method +middle +midnight +milk +million +mimic +mind +minimum +minor +minute +miracle +mirror +misery +miss +mistake +mix +mixed +mixture +mobile +model +modify +mom +moment +monitor +monkey +monster +month +moon +moral +more +morning +mosquito +mother +motion +motor +mountain +mouse +move +movie +much +muffin +mule +multiply +muscle +museum +mushroom +music +must +mutual +myself +mystery +myth +naive +name +napkin +narrow +nasty +nation +nature +near +neck +need +negative +neglect +neither +nephew +nerve +nest +net +network +neutral +never +news +next +nice +night +noble +noise +nominee +noodle +normal +north +nose +notable +note +nothing +notice +novel +now +nuclear +number +nurse +nut +oak +obey +object +oblige +obscure +observe +obtain +obvious +occur +ocean +october +odor +off +offer +office +often +oil +okay +old +olive +olympic +omit +once +one +onion +online +only +open +opera +opinion +oppose +option +orange +orbit +orchard +order +ordinary +organ +orient +original +orphan +ostrich +other +outdoor +outer +output +outside +oval +oven +over +own +owner +oxygen +oyster +ozone +pact +paddle +page +pair +palace +palm +panda +panel +panic +panther +paper +parade +parent +park +parrot +party +pass +patch +path +patient +patrol +pattern +pause +pave +payment +peace +peanut +pear +peasant +pelican +pen +penalty +pencil +people +pepper +perfect +permit +person +pet +phone +photo +phrase +physical +piano +picnic +picture +piece +pig +pigeon +pill +pilot +pink +pioneer +pipe +pistol +pitch +pizza +place +planet +plastic +plate +play +please +pledge +pluck +plug +plunge +poem +poet +point +polar +pole +police +pond +pony +pool +popular +portion +position +possible +post +potato +pottery +poverty +powder +power +practice +praise +predict +prefer +prepare +present +pretty +prevent +price +pride +primary +print +priority +prison +private +prize +problem +process +produce +profit +program +project +promote +proof +property +prosper +protect +proud +provide +public +pudding +pull +pulp +pulse +pumpkin +punch +pupil +puppy +purchase +purity +purpose +purse +push +put +puzzle +pyramid +quality +quantum +quarter +question +quick +quit +quiz +quote +rabbit +raccoon +race +rack +radar +radio +rail +rain +raise +rally +ramp +ranch +random +range +rapid +rare +rate +rather +raven +raw +razor +ready +real +reason +rebel +rebuild +recall +receive +recipe +record +recycle +reduce +reflect +reform +refuse +region +regret +regular +reject +relax +release +relief +rely +remain +remember +remind +remove +render +renew +rent +reopen +repair +repeat +replace +report +require +rescue +resemble +resist +resource +response +result +retire +retreat +return +reunion +reveal +review +reward +rhythm +rib +ribbon +rice +rich +ride +ridge +rifle +right +rigid +ring +riot +ripple +risk +ritual +rival +river +road +roast +robot +robust +rocket +romance +roof +rookie +room +rose +rotate +rough +round +route +royal +rubber +rude +rug +rule +run +runway +rural +sad +saddle +sadness +safe +sail +salad +salmon +salon +salt +salute +same +sample +sand +satisfy +satoshi +sauce +sausage +save +say +scale +scan +scare +scatter +scene +scheme +school +science +scissors +scorpion +scout +scrap +screen +script +scrub +sea +search +season +seat +second +secret +section +security +seed +seek +segment +select +sell +seminar +senior +sense +sentence +series +service +session +settle +setup +seven +shadow +shaft +shallow +share +shed +shell +sheriff +shield +shift +shine +ship +shiver +shock +shoe +shoot +shop +short +shoulder +shove +shrimp +shrug +shuffle +shy +sibling +sick +side +siege +sight +sign +silent +silk +silly +silver +similar +simple +since +sing +siren +sister +situate +six +size +skate +sketch +ski +skill +skin +skirt +skull +slab +slam +sleep +slender +slice +slide +slight +slim +slogan +slot +slow +slush +small +smart +smile +smoke +smooth +snack +snake +snap +sniff +snow +soap +soccer +social +sock +soda +soft +solar +soldier +solid +solution +solve +someone +song +soon +sorry +sort +soul +sound +soup +source +south +space +spare +spatial +spawn +speak +special +speed +spell +spend +sphere +spice +spider +spike +spin +spirit +split +spoil +sponsor +spoon +sport +spot +spray +spread +spring +spy +square +squeeze +squirrel +stable +stadium +staff +stage +stairs +stamp +stand +start +state +stay +steak +steel +stem +step +stereo +stick +still +sting +stock +stomach +stone +stool +story +stove +strategy +street +strike +strong +struggle +student +stuff +stumble +style +subject +submit +subway +success +such +sudden +suffer +sugar +suggest +suit +summer +sun +sunny +sunset +super +supply +supreme +sure +surface +surge +surprise +surround +survey +suspect +sustain +swallow +swamp +swap +swarm +swear +sweet +swift +swim +swing +switch +sword +symbol +symptom +syrup +system +table +tackle +tag +tail +talent +talk +tank +tape +target +task +taste +tattoo +taxi +teach +team +tell +ten +tenant +tennis +tent +term +test +text +thank +that +theme +then +theory +there +they +thing +this +thought +three +thrive +throw +thumb +thunder +ticket +tide +tiger +tilt +timber +time +tiny +tip +tired +tissue +title +toast +tobacco +today +toddler +toe +together +toilet +token +tomato +tomorrow +tone +tongue +tonight +tool +tooth +top +topic +topple +torch +tornado +tortoise +toss +total +tourist +toward +tower +town +toy +track +trade +traffic +tragic +train +transfer +trap +trash +travel +tray +treat +tree +trend +trial +tribe +trick +trigger +trim +trip +trophy +trouble +truck +true +truly +trumpet +trust +truth +try +tube +tuition +tumble +tuna +tunnel +turkey +turn +turtle +twelve +twenty +twice +twin +twist +two +type +typical +ugly +umbrella +unable +unaware +uncle +uncover +under +undo +unfair +unfold +unhappy +uniform +unique +unit +universe +unknown +unlock +until +unusual +unveil +update +upgrade +uphold +upon +upper +upset +urban +urge +usage +use +used +useful +useless +usual +utility +vacant +vacuum +vague +valid +valley +valve +van +vanish +vapor +various +vast +vault +vehicle +velvet +vendor +venture +venue +verb +verify +version +very +vessel +veteran +viable +vibrant +vicious +victory +video +view +village +vintage +violin +virtual +virus +visa +visit +visual +vital +vivid +vocal +voice +void +volcano +volume +vote +voyage +wage +wagon +wait +walk +wall +walnut +want +warfare +warm +warrior +wash +wasp +waste +water +wave +way +wealth +weapon +wear +weasel +weather +web +wedding +weekend +weird +welcome +west +wet +whale +what +wheat +wheel +when +where +whip +whisper +wide +width +wife +wild +will +win +window +wine +wing +wink +winner +winter +wire +wisdom +wise +wish +witness +wolf +woman +wonder +wood +wool +word +work +world +worry +worth +wrap +wreck +wrestle +wrist +write +wrong +yard +year +yellow +you +young +youth +zebra +zero +zone +zoo diff --git a/src/lib/pybitcointools/main.py b/src/lib/pybitcointools/main.py new file mode 100644 index 00000000..8cf3a9f7 --- /dev/null +++ b/src/lib/pybitcointools/main.py @@ -0,0 +1,581 @@ +#!/usr/bin/python +from .py2specials import * +from .py3specials import * +import binascii +import hashlib +import re +import sys +import os +import base64 +import time +import random +import hmac +from .ripemd import * + +# Elliptic curve parameters (secp256k1) + +P = 2**256 - 2**32 - 977 +N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 +A = 0 +B = 7 +Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 +Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 +G = (Gx, Gy) + + +def change_curve(p, n, a, b, gx, gy): + global P, N, A, B, Gx, Gy, G + P, N, A, B, Gx, Gy = p, n, a, b, gx, gy + G = (Gx, Gy) + + +def getG(): + return G + +# Extended Euclidean Algorithm + + +def inv(a, n): + if a == 0: + return 0 + lm, hm = 1, 0 + low, high = a % n, n + while low > 1: + r = high//low + nm, new = hm-lm*r, high-low*r + lm, low, hm, high = nm, new, lm, low + return lm % n + + + +# JSON access (for pybtctool convenience) + + +def access(obj, prop): + if isinstance(obj, dict): + if prop in obj: + return obj[prop] + elif '.' in prop: + return obj[float(prop)] + else: + return obj[int(prop)] + else: + return obj[int(prop)] + + +def multiaccess(obj, prop): + return [access(o, prop) for o in obj] + + +def slice(obj, start=0, end=2**200): + return obj[int(start):int(end)] + + +def count(obj): + return len(obj) + +_sum = sum + + +def sum(obj): + return _sum(obj) + + +def isinf(p): + return p[0] == 0 and p[1] == 0 + + +def to_jacobian(p): + o = (p[0], p[1], 1) + return o + + +def jacobian_double(p): + if not p[1]: + return (0, 0, 0) + ysq = (p[1] ** 2) % P + S = (4 * p[0] * ysq) % P + M = (3 * p[0] ** 2 + A * p[2] ** 4) % P + nx = (M**2 - 2 * S) % P + ny = (M * (S - nx) - 8 * ysq ** 2) % P + nz = (2 * p[1] * p[2]) % P + return (nx, ny, nz) + + +def jacobian_add(p, q): + if not p[1]: + return q + if not q[1]: + return p + U1 = (p[0] * q[2] ** 2) % P + U2 = (q[0] * p[2] ** 2) % P + S1 = (p[1] * q[2] ** 3) % P + S2 = (q[1] * p[2] ** 3) % P + if U1 == U2: + if S1 != S2: + return (0, 0, 1) + return jacobian_double(p) + H = U2 - U1 + R = S2 - S1 + H2 = (H * H) % P + H3 = (H * H2) % P + U1H2 = (U1 * H2) % P + nx = (R ** 2 - H3 - 2 * U1H2) % P + ny = (R * (U1H2 - nx) - S1 * H3) % P + nz = (H * p[2] * q[2]) % P + return (nx, ny, nz) + + +def from_jacobian(p): + z = inv(p[2], P) + return ((p[0] * z**2) % P, (p[1] * z**3) % P) + + +def jacobian_multiply(a, n): + if a[1] == 0 or n == 0: + return (0, 0, 1) + if n == 1: + return a + if n < 0 or n >= N: + return jacobian_multiply(a, n % N) + if (n % 2) == 0: + return jacobian_double(jacobian_multiply(a, n//2)) + if (n % 2) == 1: + return jacobian_add(jacobian_double(jacobian_multiply(a, n//2)), a) + + +def fast_multiply(a, n): + return from_jacobian(jacobian_multiply(to_jacobian(a), n)) + + +def fast_add(a, b): + return from_jacobian(jacobian_add(to_jacobian(a), to_jacobian(b))) + +# Functions for handling pubkey and privkey formats + + +def get_pubkey_format(pub): + if is_python2: + two = '\x02' + three = '\x03' + four = '\x04' + else: + two = 2 + three = 3 + four = 4 + + if isinstance(pub, (tuple, list)): return 'decimal' + elif len(pub) == 65 and pub[0] == four: return 'bin' + elif len(pub) == 130 and pub[0:2] == '04': return 'hex' + elif len(pub) == 33 and pub[0] in [two, three]: return 'bin_compressed' + elif len(pub) == 66 and pub[0:2] in ['02', '03']: return 'hex_compressed' + elif len(pub) == 64: return 'bin_electrum' + elif len(pub) == 128: return 'hex_electrum' + else: raise Exception("Pubkey not in recognized format") + + +def encode_pubkey(pub, formt): + if not isinstance(pub, (tuple, list)): + pub = decode_pubkey(pub) + if formt == 'decimal': return pub + elif formt == 'bin': return b'\x04' + encode(pub[0], 256, 32) + encode(pub[1], 256, 32) + elif formt == 'bin_compressed': + return from_int_to_byte(2+(pub[1] % 2)) + encode(pub[0], 256, 32) + elif formt == 'hex': return '04' + encode(pub[0], 16, 64) + encode(pub[1], 16, 64) + elif formt == 'hex_compressed': + return '0'+str(2+(pub[1] % 2)) + encode(pub[0], 16, 64) + elif formt == 'bin_electrum': return encode(pub[0], 256, 32) + encode(pub[1], 256, 32) + elif formt == 'hex_electrum': return encode(pub[0], 16, 64) + encode(pub[1], 16, 64) + else: raise Exception("Invalid format!") + + +def decode_pubkey(pub, formt=None): + if not formt: formt = get_pubkey_format(pub) + if formt == 'decimal': return pub + elif formt == 'bin': return (decode(pub[1:33], 256), decode(pub[33:65], 256)) + elif formt == 'bin_compressed': + x = decode(pub[1:33], 256) + beta = pow(int(x*x*x+A*x+B), int((P+1)//4), int(P)) + y = (P-beta) if ((beta + from_byte_to_int(pub[0])) % 2) else beta + return (x, y) + elif formt == 'hex': return (decode(pub[2:66], 16), decode(pub[66:130], 16)) + elif formt == 'hex_compressed': + return decode_pubkey(safe_from_hex(pub), 'bin_compressed') + elif formt == 'bin_electrum': + return (decode(pub[:32], 256), decode(pub[32:64], 256)) + elif formt == 'hex_electrum': + return (decode(pub[:64], 16), decode(pub[64:128], 16)) + else: raise Exception("Invalid format!") + +def get_privkey_format(priv): + if isinstance(priv, int_types): return 'decimal' + elif len(priv) == 32: return 'bin' + elif len(priv) == 33: return 'bin_compressed' + elif len(priv) == 64: return 'hex' + elif len(priv) == 66: return 'hex_compressed' + else: + bin_p = b58check_to_bin(priv) + if len(bin_p) == 32: return 'wif' + elif len(bin_p) == 33: return 'wif_compressed' + else: raise Exception("WIF does not represent privkey") + +def encode_privkey(priv, formt, vbyte=0): + if not isinstance(priv, int_types): + return encode_privkey(decode_privkey(priv), formt, vbyte) + if formt == 'decimal': return priv + elif formt == 'bin': return encode(priv, 256, 32) + elif formt == 'bin_compressed': return encode(priv, 256, 32)+b'\x01' + elif formt == 'hex': return encode(priv, 16, 64) + elif formt == 'hex_compressed': return encode(priv, 16, 64)+'01' + elif formt == 'wif': + return bin_to_b58check(encode(priv, 256, 32), 128+int(vbyte)) + elif formt == 'wif_compressed': + return bin_to_b58check(encode(priv, 256, 32)+b'\x01', 128+int(vbyte)) + else: raise Exception("Invalid format!") + +def decode_privkey(priv,formt=None): + if not formt: formt = get_privkey_format(priv) + if formt == 'decimal': return priv + elif formt == 'bin': return decode(priv, 256) + elif formt == 'bin_compressed': return decode(priv[:32], 256) + elif formt == 'hex': return decode(priv, 16) + elif formt == 'hex_compressed': return decode(priv[:64], 16) + elif formt == 'wif': return decode(b58check_to_bin(priv),256) + elif formt == 'wif_compressed': + return decode(b58check_to_bin(priv)[:32],256) + else: raise Exception("WIF does not represent privkey") + +def add_pubkeys(p1, p2): + f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2) + return encode_pubkey(fast_add(decode_pubkey(p1, f1), decode_pubkey(p2, f2)), f1) + +def add_privkeys(p1, p2): + f1, f2 = get_privkey_format(p1), get_privkey_format(p2) + return encode_privkey((decode_privkey(p1, f1) + decode_privkey(p2, f2)) % N, f1) + +def mul_privkeys(p1, p2): + f1, f2 = get_privkey_format(p1), get_privkey_format(p2) + return encode_privkey((decode_privkey(p1, f1) * decode_privkey(p2, f2)) % N, f1) + +def multiply(pubkey, privkey): + f1, f2 = get_pubkey_format(pubkey), get_privkey_format(privkey) + pubkey, privkey = decode_pubkey(pubkey, f1), decode_privkey(privkey, f2) + # http://safecurves.cr.yp.to/twist.html + if not isinf(pubkey) and (pubkey[0]**3+B-pubkey[1]*pubkey[1]) % P != 0: + raise Exception("Point not on curve") + return encode_pubkey(fast_multiply(pubkey, privkey), f1) + + +def divide(pubkey, privkey): + factor = inv(decode_privkey(privkey), N) + return multiply(pubkey, factor) + + +def compress(pubkey): + f = get_pubkey_format(pubkey) + if 'compressed' in f: return pubkey + elif f == 'bin': return encode_pubkey(decode_pubkey(pubkey, f), 'bin_compressed') + elif f == 'hex' or f == 'decimal': + return encode_pubkey(decode_pubkey(pubkey, f), 'hex_compressed') + + +def decompress(pubkey): + f = get_pubkey_format(pubkey) + if 'compressed' not in f: return pubkey + elif f == 'bin_compressed': return encode_pubkey(decode_pubkey(pubkey, f), 'bin') + elif f == 'hex_compressed' or f == 'decimal': + return encode_pubkey(decode_pubkey(pubkey, f), 'hex') + + +def privkey_to_pubkey(privkey): + f = get_privkey_format(privkey) + privkey = decode_privkey(privkey, f) + if privkey >= N: + raise Exception("Invalid privkey") + if f in ['bin', 'bin_compressed', 'hex', 'hex_compressed', 'decimal']: + return encode_pubkey(fast_multiply(G, privkey), f) + else: + return encode_pubkey(fast_multiply(G, privkey), f.replace('wif', 'hex')) + +privtopub = privkey_to_pubkey + + +def privkey_to_address(priv, magicbyte=0): + return pubkey_to_address(privkey_to_pubkey(priv), magicbyte) +privtoaddr = privkey_to_address + + +def neg_pubkey(pubkey): + f = get_pubkey_format(pubkey) + pubkey = decode_pubkey(pubkey, f) + return encode_pubkey((pubkey[0], (P-pubkey[1]) % P), f) + + +def neg_privkey(privkey): + f = get_privkey_format(privkey) + privkey = decode_privkey(privkey, f) + return encode_privkey((N - privkey) % N, f) + +def subtract_pubkeys(p1, p2): + f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2) + k2 = decode_pubkey(p2, f2) + return encode_pubkey(fast_add(decode_pubkey(p1, f1), (k2[0], (P - k2[1]) % P)), f1) + + +def subtract_privkeys(p1, p2): + f1, f2 = get_privkey_format(p1), get_privkey_format(p2) + k2 = decode_privkey(p2, f2) + return encode_privkey((decode_privkey(p1, f1) - k2) % N, f1) + +# Hashes + + +def bin_hash160(string): + intermed = hashlib.sha256(string).digest() + digest = '' + try: + digest = hashlib.new('ripemd160', intermed).digest() + except: + digest = RIPEMD160(intermed).digest() + return digest + + +def hash160(string): + return safe_hexlify(bin_hash160(string)) + + +def bin_sha256(string): + binary_data = string if isinstance(string, bytes) else bytes(string, 'utf-8') + return hashlib.sha256(binary_data).digest() + +def sha256(string): + return bytes_to_hex_string(bin_sha256(string)) + + +def bin_ripemd160(string): + try: + digest = hashlib.new('ripemd160', string).digest() + except: + digest = RIPEMD160(string).digest() + return digest + + +def ripemd160(string): + return safe_hexlify(bin_ripemd160(string)) + + +def bin_dbl_sha256(s): + bytes_to_hash = from_string_to_bytes(s) + return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() + + +def dbl_sha256(string): + return safe_hexlify(bin_dbl_sha256(string)) + + +def bin_slowsha(string): + string = from_string_to_bytes(string) + orig_input = string + for i in range(100000): + string = hashlib.sha256(string + orig_input).digest() + return string + + +def slowsha(string): + return safe_hexlify(bin_slowsha(string)) + + +def hash_to_int(x): + if len(x) in [40, 64]: + return decode(x, 16) + return decode(x, 256) + + +def num_to_var_int(x): + x = int(x) + if x < 253: return from_int_to_byte(x) + elif x < 65536: return from_int_to_byte(253)+encode(x, 256, 2)[::-1] + elif x < 4294967296: return from_int_to_byte(254) + encode(x, 256, 4)[::-1] + else: return from_int_to_byte(255) + encode(x, 256, 8)[::-1] + + +# WTF, Electrum? +def electrum_sig_hash(message): + padded = b"\x18Bitcoin Signed Message:\n" + num_to_var_int(len(message)) + from_string_to_bytes(message) + return bin_dbl_sha256(padded) + + +def random_key(): + # Gotta be secure after that java.SecureRandom fiasco... + entropy = random_string(32) \ + + str(random.randrange(2**256)) \ + + str(int(time.time() * 1000000)) + return sha256(entropy) + + +def random_electrum_seed(): + entropy = os.urandom(32) \ + + str(random.randrange(2**256)) \ + + str(int(time.time() * 1000000)) + return sha256(entropy)[:32] + +# Encodings + +def b58check_to_bin(inp): + leadingzbytes = len(re.match('^1*', inp).group(0)) + data = b'\x00' * leadingzbytes + changebase(inp, 58, 256) + assert bin_dbl_sha256(data[:-4])[:4] == data[-4:] + return data[1:-4] + + +def get_version_byte(inp): + leadingzbytes = len(re.match('^1*', inp).group(0)) + data = b'\x00' * leadingzbytes + changebase(inp, 58, 256) + assert bin_dbl_sha256(data[:-4])[:4] == data[-4:] + return ord(data[0]) + + +def hex_to_b58check(inp, magicbyte=0): + return bin_to_b58check(binascii.unhexlify(inp), magicbyte) + + +def b58check_to_hex(inp): + return safe_hexlify(b58check_to_bin(inp)) + + +def pubkey_to_address(pubkey, magicbyte=0): + if isinstance(pubkey, (list, tuple)): + pubkey = encode_pubkey(pubkey, 'bin') + if len(pubkey) in [66, 130]: + return bin_to_b58check( + bin_hash160(binascii.unhexlify(pubkey)), magicbyte) + return bin_to_b58check(bin_hash160(pubkey), magicbyte) + +pubtoaddr = pubkey_to_address + + +def is_privkey(priv): + try: + get_privkey_format(priv) + return True + except: + return False + +def is_pubkey(pubkey): + try: + get_pubkey_format(pubkey) + return True + except: + return False + +def is_address(addr): + ADDR_RE = re.compile("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$") + return bool(ADDR_RE.match(addr)) + + +# EDCSA + + +def encode_sig(v, r, s): + vb, rb, sb = from_int_to_byte(v), encode(r, 256), encode(s, 256) + + result = base64.b64encode(vb+b'\x00'*(32-len(rb))+rb+b'\x00'*(32-len(sb))+sb) + return result if is_python2 else str(result, 'utf-8') + + +def decode_sig(sig): + bytez = base64.b64decode(sig) + return from_byte_to_int(bytez[0]), decode(bytez[1:33], 256), decode(bytez[33:], 256) + +# https://tools.ietf.org/html/rfc6979#section-3.2 + + +def deterministic_generate_k(msghash, priv): + v = b'\x01' * 32 + k = b'\x00' * 32 + priv = encode_privkey(priv, 'bin') + msghash = encode(hash_to_int(msghash), 256, 32) + k = hmac.new(k, v+b'\x00'+priv+msghash, hashlib.sha256).digest() + v = hmac.new(k, v, hashlib.sha256).digest() + k = hmac.new(k, v+b'\x01'+priv+msghash, hashlib.sha256).digest() + v = hmac.new(k, v, hashlib.sha256).digest() + return decode(hmac.new(k, v, hashlib.sha256).digest(), 256) + + +def ecdsa_raw_sign(msghash, priv): + + z = hash_to_int(msghash) + k = deterministic_generate_k(msghash, priv) + + r, y = fast_multiply(G, k) + s = inv(k, N) * (z + r*decode_privkey(priv)) % N + + v, r, s = 27+((y % 2) ^ (0 if s * 2 < N else 1)), r, s if s * 2 < N else N - s + if 'compressed' in get_privkey_format(priv): + v += 4 + return v, r, s + + +def ecdsa_sign(msg, priv): + v, r, s = ecdsa_raw_sign(electrum_sig_hash(msg), priv) + sig = encode_sig(v, r, s) + assert ecdsa_verify(msg, sig, + privtopub(priv)), "Bad Sig!\t %s\nv = %d\n,r = %d\ns = %d" % (sig, v, r, s) + return sig + + +def ecdsa_raw_verify(msghash, vrs, pub): + v, r, s = vrs + if not (27 <= v <= 34): + return False + + w = inv(s, N) + z = hash_to_int(msghash) + + u1, u2 = z*w % N, r*w % N + x, y = fast_add(fast_multiply(G, u1), fast_multiply(decode_pubkey(pub), u2)) + return bool(r == x and (r % N) and (s % N)) + + +# For BitcoinCore, (msg = addr or msg = "") be default +def ecdsa_verify_addr(msg, sig, addr): + assert is_address(addr) + Q = ecdsa_recover(msg, sig) + magic = get_version_byte(addr) + return (addr == pubtoaddr(Q, int(magic))) or (addr == pubtoaddr(compress(Q), int(magic))) + + +def ecdsa_verify(msg, sig, pub): + if is_address(pub): + return ecdsa_verify_addr(msg, sig, pub) + return ecdsa_raw_verify(electrum_sig_hash(msg), decode_sig(sig), pub) + + +def ecdsa_raw_recover(msghash, vrs): + v, r, s = vrs + if not (27 <= v <= 34): + raise ValueError("%d must in range 27-31" % v) + x = r + xcubedaxb = (x*x*x+A*x+B) % P + beta = pow(xcubedaxb, (P+1)//4, P) + y = beta if v % 2 ^ beta % 2 else (P - beta) + # If xcubedaxb is not a quadratic residue, then r cannot be the x coord + # for a point on the curve, and so the sig is invalid + if (xcubedaxb - y*y) % P != 0 or not (r % N) or not (s % N): + return False + z = hash_to_int(msghash) + Gz = jacobian_multiply((Gx, Gy, 1), (N - z) % N) + XY = jacobian_multiply((x, y, 1), s) + Qr = jacobian_add(Gz, XY) + Q = jacobian_multiply(Qr, inv(r, N)) + Q = from_jacobian(Q) + + # if ecdsa_raw_verify(msghash, vrs, Q): + return Q + # return False + + +def ecdsa_recover(msg, sig): + v,r,s = decode_sig(sig) + Q = ecdsa_raw_recover(electrum_sig_hash(msg), (v,r,s)) + return encode_pubkey(Q, 'hex_compressed') if v >= 31 else encode_pubkey(Q, 'hex') diff --git a/src/lib/pybitcointools/mnemonic.py b/src/lib/pybitcointools/mnemonic.py new file mode 100644 index 00000000..a9df3617 --- /dev/null +++ b/src/lib/pybitcointools/mnemonic.py @@ -0,0 +1,127 @@ +import hashlib +import os.path +import binascii +import random +from bisect import bisect_left + +wordlist_english=list(open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'english.txt'),'r')) + +def eint_to_bytes(entint,entbits): + a=hex(entint)[2:].rstrip('L').zfill(32) + print(a) + return binascii.unhexlify(a) + +def mnemonic_int_to_words(mint,mint_num_words,wordlist=wordlist_english): + backwords=[wordlist[(mint >> (11*x)) & 0x7FF].strip() for x in range(mint_num_words)] + return backwords[::-1] + +def entropy_cs(entbytes): + entropy_size=8*len(entbytes) + checksum_size=entropy_size//32 + hd=hashlib.sha256(entbytes).hexdigest() + csint=int(hd,16) >> (256-checksum_size) + return csint,checksum_size + +def entropy_to_words(entbytes,wordlist=wordlist_english): + if(len(entbytes) < 4 or len(entbytes) % 4 != 0): + raise ValueError("The size of the entropy must be a multiple of 4 bytes (multiple of 32 bits)") + entropy_size=8*len(entbytes) + csint,checksum_size = entropy_cs(entbytes) + entint=int(binascii.hexlify(entbytes),16) + mint=(entint << checksum_size) | csint + mint_num_words=(entropy_size+checksum_size)//11 + + return mnemonic_int_to_words(mint,mint_num_words,wordlist) + +def words_bisect(word,wordlist=wordlist_english): + lo=bisect_left(wordlist,word) + hi=len(wordlist)-bisect_left(wordlist[:lo:-1],word) + + return lo,hi + +def words_split(wordstr,wordlist=wordlist_english): + def popword(wordstr,wordlist): + for fwl in range(1,9): + w=wordstr[:fwl].strip() + lo,hi=words_bisect(w,wordlist) + if(hi-lo == 1): + return w,wordstr[fwl:].lstrip() + wordlist=wordlist[lo:hi] + raise Exception("Wordstr %s not found in list" %(w)) + + words=[] + tail=wordstr + while(len(tail)): + head,tail=popword(tail,wordlist) + words.append(head) + return words + +def words_to_mnemonic_int(words,wordlist=wordlist_english): + if(isinstance(words,str)): + words=words_split(words,wordlist) + return sum([wordlist.index(w) << (11*x) for x,w in enumerate(words[::-1])]) + +def words_verify(words,wordlist=wordlist_english): + if(isinstance(words,str)): + words=words_split(words,wordlist) + + mint = words_to_mnemonic_int(words,wordlist) + mint_bits=len(words)*11 + cs_bits=mint_bits//32 + entropy_bits=mint_bits-cs_bits + eint=mint >> cs_bits + csint=mint & ((1 << cs_bits)-1) + ebytes=_eint_to_bytes(eint,entropy_bits) + return csint == entropy_cs(ebytes) + +def mnemonic_to_seed(mnemonic_phrase,passphrase=b''): + try: + from hashlib import pbkdf2_hmac + def pbkdf2_hmac_sha256(password,salt,iters=2048): + return pbkdf2_hmac(hash_name='sha512',password=password,salt=salt,iterations=iters) + except: + try: + from Crypto.Protocol.KDF import PBKDF2 + from Crypto.Hash import SHA512,HMAC + + def pbkdf2_hmac_sha256(password,salt,iters=2048): + return PBKDF2(password=password,salt=salt,dkLen=64,count=iters,prf=lambda p,s: HMAC.new(p,s,SHA512).digest()) + except: + try: + + from pbkdf2 import PBKDF2 + import hmac + def pbkdf2_hmac_sha256(password,salt,iters=2048): + return PBKDF2(password,salt, iterations=iters, macmodule=hmac, digestmodule=hashlib.sha512).read(64) + except: + raise RuntimeError("No implementation of pbkdf2 was found!") + + return pbkdf2_hmac_sha256(password=mnemonic_phrase,salt=b'mnemonic'+passphrase) + +def words_mine(prefix,entbits,satisfunction,wordlist=wordlist_english,randombits=random.getrandbits): + prefix_bits=len(prefix)*11 + mine_bits=entbits-prefix_bits + pint=words_to_mnemonic_int(prefix,wordlist) + pint<<=mine_bits + dint=randombits(mine_bits) + count=0 + while(not satisfunction(entropy_to_words(eint_to_bytes(pint+dint,entbits)))): + dint=randombits(mine_bits) + if((count & 0xFFFF) == 0): + print("Searched %f percent of the space" % (float(count)/float(1 << mine_bits))) + + return entropy_to_words(eint_to_bytes(pint+dint,entbits)) + +if __name__=="__main__": + import json + testvectors=json.load(open('vectors.json','r')) + passed=True + for v in testvectors['english']: + ebytes=binascii.unhexlify(v[0]) + w=' '.join(entropy_to_words(ebytes)) + seed=mnemonic_to_seed(w,passphrase='TREZOR') + passed = passed and w==v[1] + passed = passed and binascii.hexlify(seed)==v[2] + print("Tests %s." % ("Passed" if passed else "Failed")) + + diff --git a/src/lib/pybitcointools/py2specials.py b/src/lib/pybitcointools/py2specials.py new file mode 100644 index 00000000..337154f3 --- /dev/null +++ b/src/lib/pybitcointools/py2specials.py @@ -0,0 +1,98 @@ +import sys, re +import binascii +import os +import hashlib + + +if sys.version_info.major == 2: + string_types = (str, unicode) + string_or_bytes_types = string_types + int_types = (int, float, long) + + # Base switching + code_strings = { + 2: '01', + 10: '0123456789', + 16: '0123456789abcdef', + 32: 'abcdefghijklmnopqrstuvwxyz234567', + 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', + 256: ''.join([chr(x) for x in range(256)]) + } + + def bin_dbl_sha256(s): + bytes_to_hash = from_string_to_bytes(s) + return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() + + def lpad(msg, symbol, length): + if len(msg) >= length: + return msg + return symbol * (length - len(msg)) + msg + + def get_code_string(base): + if base in code_strings: + return code_strings[base] + else: + raise ValueError("Invalid base!") + + def changebase(string, frm, to, minlen=0): + if frm == to: + return lpad(string, get_code_string(frm)[0], minlen) + return encode(decode(string, frm), to, minlen) + + def bin_to_b58check(inp, magicbyte=0): + if magicbyte == 0: + inp = '\x00' + inp + while magicbyte > 0: + inp = chr(int(magicbyte % 256)) + inp + magicbyte //= 256 + leadingzbytes = len(re.match('^\x00*', inp).group(0)) + checksum = bin_dbl_sha256(inp)[:4] + return '1' * leadingzbytes + changebase(inp+checksum, 256, 58) + + def bytes_to_hex_string(b): + return b.encode('hex') + + def safe_from_hex(s): + return s.decode('hex') + + def from_int_representation_to_bytes(a): + return str(a) + + def from_int_to_byte(a): + return chr(a) + + def from_byte_to_int(a): + return ord(a) + + def from_bytes_to_string(s): + return s + + def from_string_to_bytes(a): + return a + + def safe_hexlify(a): + return binascii.hexlify(a) + + def encode(val, base, minlen=0): + base, minlen = int(base), int(minlen) + code_string = get_code_string(base) + result = "" + while val > 0: + result = code_string[val % base] + result + val //= base + return code_string[0] * max(minlen - len(result), 0) + result + + def decode(string, base): + base = int(base) + code_string = get_code_string(base) + result = 0 + if base == 16: + string = string.lower() + while len(string) > 0: + result *= base + result += code_string.find(string[0]) + string = string[1:] + return result + + def random_string(x): + return os.urandom(x) diff --git a/src/lib/pybitcointools/py3specials.py b/src/lib/pybitcointools/py3specials.py new file mode 100644 index 00000000..7593b9a6 --- /dev/null +++ b/src/lib/pybitcointools/py3specials.py @@ -0,0 +1,123 @@ +import sys, os +import binascii +import hashlib + + +if sys.version_info.major == 3: + string_types = (str) + string_or_bytes_types = (str, bytes) + int_types = (int, float) + # Base switching + code_strings = { + 2: '01', + 10: '0123456789', + 16: '0123456789abcdef', + 32: 'abcdefghijklmnopqrstuvwxyz234567', + 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', + 256: ''.join([chr(x) for x in range(256)]) + } + + def bin_dbl_sha256(s): + bytes_to_hash = from_string_to_bytes(s) + return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() + + def lpad(msg, symbol, length): + if len(msg) >= length: + return msg + return symbol * (length - len(msg)) + msg + + def get_code_string(base): + if base in code_strings: + return code_strings[base] + else: + raise ValueError("Invalid base!") + + def changebase(string, frm, to, minlen=0): + if frm == to: + return lpad(string, get_code_string(frm)[0], minlen) + return encode(decode(string, frm), to, minlen) + + def bin_to_b58check(inp, magicbyte=0): + if magicbyte == 0: + inp = from_int_to_byte(0) + inp + while magicbyte > 0: + inp = from_int_to_byte(magicbyte % 256) + inp + magicbyte //= 256 + + leadingzbytes = 0 + for x in inp: + if x != 0: + break + leadingzbytes += 1 + + checksum = bin_dbl_sha256(inp)[:4] + return '1' * leadingzbytes + changebase(inp+checksum, 256, 58) + + def bytes_to_hex_string(b): + if isinstance(b, str): + return b + + return ''.join('{:02x}'.format(y) for y in b) + + def safe_from_hex(s): + return bytes.fromhex(s) + + def from_int_representation_to_bytes(a): + return bytes(str(a), 'utf-8') + + def from_int_to_byte(a): + return bytes([a]) + + def from_byte_to_int(a): + return a + + def from_string_to_bytes(a): + return a if isinstance(a, bytes) else bytes(a, 'utf-8') + + def safe_hexlify(a): + return str(binascii.hexlify(a), 'utf-8') + + def encode(val, base, minlen=0): + base, minlen = int(base), int(minlen) + code_string = get_code_string(base) + result_bytes = bytes() + while val > 0: + curcode = code_string[val % base] + result_bytes = bytes([ord(curcode)]) + result_bytes + val //= base + + pad_size = minlen - len(result_bytes) + + padding_element = b'\x00' if base == 256 else b'1' \ + if base == 58 else b'0' + if (pad_size > 0): + result_bytes = padding_element*pad_size + result_bytes + + result_string = ''.join([chr(y) for y in result_bytes]) + result = result_bytes if base == 256 else result_string + + return result + + def decode(string, base): + if base == 256 and isinstance(string, str): + string = bytes(bytearray.fromhex(string)) + base = int(base) + code_string = get_code_string(base) + result = 0 + if base == 256: + def extract(d, cs): + return d + else: + def extract(d, cs): + return cs.find(d if isinstance(d, str) else chr(d)) + + if base == 16: + string = string.lower() + while len(string) > 0: + result *= base + result += extract(string[0], code_string) + string = string[1:] + return result + + def random_string(x): + return str(os.urandom(x)) diff --git a/src/lib/pybitcointools/ripemd.py b/src/lib/pybitcointools/ripemd.py new file mode 100644 index 00000000..4b0c6045 --- /dev/null +++ b/src/lib/pybitcointools/ripemd.py @@ -0,0 +1,414 @@ +## ripemd.py - pure Python implementation of the RIPEMD-160 algorithm. +## Bjorn Edstrom 16 december 2007. +## +## Copyrights +## ========== +## +## This code is a derived from an implementation by Markus Friedl which is +## subject to the following license. This Python implementation is not +## subject to any other license. +## +##/* +## * Copyright (c) 2001 Markus Friedl. All rights reserved. +## * +## * Redistribution and use in source and binary forms, with or without +## * modification, are permitted provided that the following conditions +## * are met: +## * 1. Redistributions of source code must retain the above copyright +## * notice, this list of conditions and the following disclaimer. +## * 2. Redistributions in binary form must reproduce the above copyright +## * notice, this list of conditions and the following disclaimer in the +## * documentation and/or other materials provided with the distribution. +## * +## * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +## * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +## * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +## * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +## * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +## * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +## * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +## */ +##/* +## * Preneel, Bosselaers, Dobbertin, "The Cryptographic Hash Function RIPEMD-160", +## * RSA Laboratories, CryptoBytes, Volume 3, Number 2, Autumn 1997, +## * ftp://ftp.rsasecurity.com/pub/cryptobytes/crypto3n2.pdf +## */ + +try: + import psyco + psyco.full() +except ImportError: + pass + +import sys + +is_python2 = sys.version_info.major == 2 +#block_size = 1 +digest_size = 20 +digestsize = 20 + +try: + range = xrange +except: + pass + +class RIPEMD160: + """Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed.""" + + def __init__(self, arg=None): + self.ctx = RMDContext() + if arg: + self.update(arg) + self.dig = None + + def update(self, arg): + """update(arg)""" + RMD160Update(self.ctx, arg, len(arg)) + self.dig = None + + def digest(self): + """digest()""" + if self.dig: + return self.dig + ctx = self.ctx.copy() + self.dig = RMD160Final(self.ctx) + self.ctx = ctx + return self.dig + + def hexdigest(self): + """hexdigest()""" + dig = self.digest() + hex_digest = '' + for d in dig: + if (is_python2): + hex_digest += '%02x' % ord(d) + else: + hex_digest += '%02x' % d + return hex_digest + + def copy(self): + """copy()""" + import copy + return copy.deepcopy(self) + + + +def new(arg=None): + """Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed.""" + return RIPEMD160(arg) + + + +# +# Private. +# + +class RMDContext: + def __init__(self): + self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, + 0x10325476, 0xC3D2E1F0] # uint32 + self.count = 0 # uint64 + self.buffer = [0]*64 # uchar + def copy(self): + ctx = RMDContext() + ctx.state = self.state[:] + ctx.count = self.count + ctx.buffer = self.buffer[:] + return ctx + +K0 = 0x00000000 +K1 = 0x5A827999 +K2 = 0x6ED9EBA1 +K3 = 0x8F1BBCDC +K4 = 0xA953FD4E + +KK0 = 0x50A28BE6 +KK1 = 0x5C4DD124 +KK2 = 0x6D703EF3 +KK3 = 0x7A6D76E9 +KK4 = 0x00000000 + +def ROL(n, x): + return ((x << n) & 0xffffffff) | (x >> (32 - n)) + +def F0(x, y, z): + return x ^ y ^ z + +def F1(x, y, z): + return (x & y) | (((~x) % 0x100000000) & z) + +def F2(x, y, z): + return (x | ((~y) % 0x100000000)) ^ z + +def F3(x, y, z): + return (x & z) | (((~z) % 0x100000000) & y) + +def F4(x, y, z): + return x ^ (y | ((~z) % 0x100000000)) + +def R(a, b, c, d, e, Fj, Kj, sj, rj, X): + a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e + c = ROL(10, c) + return a % 0x100000000, c + +PADDING = [0x80] + [0]*63 + +import sys +import struct + +def RMD160Transform(state, block): #uint32 state[5], uchar block[64] + x = [0]*16 + if sys.byteorder == 'little': + if is_python2: + x = struct.unpack('<16L', ''.join([chr(x) for x in block[0:64]])) + else: + x = struct.unpack('<16L', bytes(block[0:64])) + else: + raise "Error!!" + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + #/* Round 1 */ + a, c = R(a, b, c, d, e, F0, K0, 11, 0, x); + e, b = R(e, a, b, c, d, F0, K0, 14, 1, x); + d, a = R(d, e, a, b, c, F0, K0, 15, 2, x); + c, e = R(c, d, e, a, b, F0, K0, 12, 3, x); + b, d = R(b, c, d, e, a, F0, K0, 5, 4, x); + a, c = R(a, b, c, d, e, F0, K0, 8, 5, x); + e, b = R(e, a, b, c, d, F0, K0, 7, 6, x); + d, a = R(d, e, a, b, c, F0, K0, 9, 7, x); + c, e = R(c, d, e, a, b, F0, K0, 11, 8, x); + b, d = R(b, c, d, e, a, F0, K0, 13, 9, x); + a, c = R(a, b, c, d, e, F0, K0, 14, 10, x); + e, b = R(e, a, b, c, d, F0, K0, 15, 11, x); + d, a = R(d, e, a, b, c, F0, K0, 6, 12, x); + c, e = R(c, d, e, a, b, F0, K0, 7, 13, x); + b, d = R(b, c, d, e, a, F0, K0, 9, 14, x); + a, c = R(a, b, c, d, e, F0, K0, 8, 15, x); #/* #15 */ + #/* Round 2 */ + e, b = R(e, a, b, c, d, F1, K1, 7, 7, x); + d, a = R(d, e, a, b, c, F1, K1, 6, 4, x); + c, e = R(c, d, e, a, b, F1, K1, 8, 13, x); + b, d = R(b, c, d, e, a, F1, K1, 13, 1, x); + a, c = R(a, b, c, d, e, F1, K1, 11, 10, x); + e, b = R(e, a, b, c, d, F1, K1, 9, 6, x); + d, a = R(d, e, a, b, c, F1, K1, 7, 15, x); + c, e = R(c, d, e, a, b, F1, K1, 15, 3, x); + b, d = R(b, c, d, e, a, F1, K1, 7, 12, x); + a, c = R(a, b, c, d, e, F1, K1, 12, 0, x); + e, b = R(e, a, b, c, d, F1, K1, 15, 9, x); + d, a = R(d, e, a, b, c, F1, K1, 9, 5, x); + c, e = R(c, d, e, a, b, F1, K1, 11, 2, x); + b, d = R(b, c, d, e, a, F1, K1, 7, 14, x); + a, c = R(a, b, c, d, e, F1, K1, 13, 11, x); + e, b = R(e, a, b, c, d, F1, K1, 12, 8, x); #/* #31 */ + #/* Round 3 */ + d, a = R(d, e, a, b, c, F2, K2, 11, 3, x); + c, e = R(c, d, e, a, b, F2, K2, 13, 10, x); + b, d = R(b, c, d, e, a, F2, K2, 6, 14, x); + a, c = R(a, b, c, d, e, F2, K2, 7, 4, x); + e, b = R(e, a, b, c, d, F2, K2, 14, 9, x); + d, a = R(d, e, a, b, c, F2, K2, 9, 15, x); + c, e = R(c, d, e, a, b, F2, K2, 13, 8, x); + b, d = R(b, c, d, e, a, F2, K2, 15, 1, x); + a, c = R(a, b, c, d, e, F2, K2, 14, 2, x); + e, b = R(e, a, b, c, d, F2, K2, 8, 7, x); + d, a = R(d, e, a, b, c, F2, K2, 13, 0, x); + c, e = R(c, d, e, a, b, F2, K2, 6, 6, x); + b, d = R(b, c, d, e, a, F2, K2, 5, 13, x); + a, c = R(a, b, c, d, e, F2, K2, 12, 11, x); + e, b = R(e, a, b, c, d, F2, K2, 7, 5, x); + d, a = R(d, e, a, b, c, F2, K2, 5, 12, x); #/* #47 */ + #/* Round 4 */ + c, e = R(c, d, e, a, b, F3, K3, 11, 1, x); + b, d = R(b, c, d, e, a, F3, K3, 12, 9, x); + a, c = R(a, b, c, d, e, F3, K3, 14, 11, x); + e, b = R(e, a, b, c, d, F3, K3, 15, 10, x); + d, a = R(d, e, a, b, c, F3, K3, 14, 0, x); + c, e = R(c, d, e, a, b, F3, K3, 15, 8, x); + b, d = R(b, c, d, e, a, F3, K3, 9, 12, x); + a, c = R(a, b, c, d, e, F3, K3, 8, 4, x); + e, b = R(e, a, b, c, d, F3, K3, 9, 13, x); + d, a = R(d, e, a, b, c, F3, K3, 14, 3, x); + c, e = R(c, d, e, a, b, F3, K3, 5, 7, x); + b, d = R(b, c, d, e, a, F3, K3, 6, 15, x); + a, c = R(a, b, c, d, e, F3, K3, 8, 14, x); + e, b = R(e, a, b, c, d, F3, K3, 6, 5, x); + d, a = R(d, e, a, b, c, F3, K3, 5, 6, x); + c, e = R(c, d, e, a, b, F3, K3, 12, 2, x); #/* #63 */ + #/* Round 5 */ + b, d = R(b, c, d, e, a, F4, K4, 9, 4, x); + a, c = R(a, b, c, d, e, F4, K4, 15, 0, x); + e, b = R(e, a, b, c, d, F4, K4, 5, 5, x); + d, a = R(d, e, a, b, c, F4, K4, 11, 9, x); + c, e = R(c, d, e, a, b, F4, K4, 6, 7, x); + b, d = R(b, c, d, e, a, F4, K4, 8, 12, x); + a, c = R(a, b, c, d, e, F4, K4, 13, 2, x); + e, b = R(e, a, b, c, d, F4, K4, 12, 10, x); + d, a = R(d, e, a, b, c, F4, K4, 5, 14, x); + c, e = R(c, d, e, a, b, F4, K4, 12, 1, x); + b, d = R(b, c, d, e, a, F4, K4, 13, 3, x); + a, c = R(a, b, c, d, e, F4, K4, 14, 8, x); + e, b = R(e, a, b, c, d, F4, K4, 11, 11, x); + d, a = R(d, e, a, b, c, F4, K4, 8, 6, x); + c, e = R(c, d, e, a, b, F4, K4, 5, 15, x); + b, d = R(b, c, d, e, a, F4, K4, 6, 13, x); #/* #79 */ + + aa = a; + bb = b; + cc = c; + dd = d; + ee = e; + + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + #/* Parallel round 1 */ + a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) + e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) + d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) + c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) + b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) + a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) + e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) + d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) + c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) + b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) + a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) + e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) + d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) + c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) + b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) + a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) #/* #15 */ + #/* Parallel round 2 */ + e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) + d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) + c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) + a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) + e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) + d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) + c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) + a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) + e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) + d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) + c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) + b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) + a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) + e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) #/* #31 */ + #/* Parallel round 3 */ + d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) + b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) + e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) + c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) + a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) + c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) + b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) + a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) + e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) #/* #47 */ + #/* Parallel round 4 */ + c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) + b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) + a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) + e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) + d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) + c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) + b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) + a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) + e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) + d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) + c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) + b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) + a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) + e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) + d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) + c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) #/* #63 */ + #/* Parallel round 5 */ + b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) + e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) + d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) + c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) + b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) + a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) + e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) + d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) + c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) + b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) + e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) + d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) + c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) + b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) #/* #79 */ + + t = (state[1] + cc + d) % 0x100000000; + state[1] = (state[2] + dd + e) % 0x100000000; + state[2] = (state[3] + ee + a) % 0x100000000; + state[3] = (state[4] + aa + b) % 0x100000000; + state[4] = (state[0] + bb + c) % 0x100000000; + state[0] = t % 0x100000000; + + pass + + +def RMD160Update(ctx, inp, inplen): + if type(inp) == str: + inp = [ord(i)&0xff for i in inp] + + have = int((ctx.count // 8) % 64) + inplen = int(inplen) + need = 64 - have + ctx.count += 8 * inplen + off = 0 + if inplen >= need: + if have: + for i in range(need): + ctx.buffer[have+i] = inp[i] + RMD160Transform(ctx.state, ctx.buffer) + off = need + have = 0 + while off + 64 <= inplen: + RMD160Transform(ctx.state, inp[off:]) #<--- + off += 64 + if off < inplen: + # memcpy(ctx->buffer + have, input+off, len-off); + for i in range(inplen - off): + ctx.buffer[have+i] = inp[off+i] + +def RMD160Final(ctx): + size = struct.pack(" 73: return False + if (sig[0] != 0x30): return False + if (sig[1] != len(sig)-3): return False + rlen = sig[3] + if (5+rlen >= len(sig)): return False + slen = sig[5+rlen] + if (rlen + slen + 7 != len(sig)): return False + if (sig[2] != 0x02): return False + if (rlen == 0): return False + if (sig[4] & 0x80): return False + if (rlen > 1 and (sig[4] == 0x00) and not (sig[5] & 0x80)): return False + if (sig[4+rlen] != 0x02): return False + if (slen == 0): return False + if (sig[rlen+6] & 0x80): return False + if (slen > 1 and (sig[6+rlen] == 0x00) and not (sig[7+rlen] & 0x80)): + return False + return True + +def txhash(tx, hashcode=None): + if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx): + tx = changebase(tx, 16, 256) + if hashcode: + return dbl_sha256(from_string_to_bytes(tx) + encode(int(hashcode), 256, 4)[::-1]) + else: + return safe_hexlify(bin_dbl_sha256(tx)[::-1]) + + +def bin_txhash(tx, hashcode=None): + return binascii.unhexlify(txhash(tx, hashcode)) + + +def ecdsa_tx_sign(tx, priv, hashcode=SIGHASH_ALL): + rawsig = ecdsa_raw_sign(bin_txhash(tx, hashcode), priv) + return der_encode_sig(*rawsig)+encode(hashcode, 16, 2) + + +def ecdsa_tx_verify(tx, sig, pub, hashcode=SIGHASH_ALL): + return ecdsa_raw_verify(bin_txhash(tx, hashcode), der_decode_sig(sig), pub) + + +def ecdsa_tx_recover(tx, sig, hashcode=SIGHASH_ALL): + z = bin_txhash(tx, hashcode) + _, r, s = der_decode_sig(sig) + left = ecdsa_raw_recover(z, (0, r, s)) + right = ecdsa_raw_recover(z, (1, r, s)) + return (encode_pubkey(left, 'hex'), encode_pubkey(right, 'hex')) + +# Scripts + + +def mk_pubkey_script(addr): + # Keep the auxiliary functions around for altcoins' sake + return '76a914' + b58check_to_hex(addr) + '88ac' + + +def mk_scripthash_script(addr): + return 'a914' + b58check_to_hex(addr) + '87' + +# Address representation to output script + + +def address_to_script(addr): + if addr[0] == '3' or addr[0] == '2': + return mk_scripthash_script(addr) + else: + return mk_pubkey_script(addr) + +# Output script to address representation + + +def script_to_address(script, vbyte=0): + if re.match('^[0-9a-fA-F]*$', script): + script = binascii.unhexlify(script) + if script[:3] == b'\x76\xa9\x14' and script[-2:] == b'\x88\xac' and len(script) == 25: + return bin_to_b58check(script[3:-2], vbyte) # pubkey hash addresses + else: + if vbyte in [111, 196]: + # Testnet + scripthash_byte = 196 + elif vbyte == 0: + # Mainnet + scripthash_byte = 5 + else: + scripthash_byte = vbyte + # BIP0016 scripthash addresses + return bin_to_b58check(script[2:-1], scripthash_byte) + + +def p2sh_scriptaddr(script, magicbyte=5): + if re.match('^[0-9a-fA-F]*$', script): + script = binascii.unhexlify(script) + return hex_to_b58check(hash160(script), magicbyte) +scriptaddr = p2sh_scriptaddr + + +def deserialize_script(script): + if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script): + return json_changebase(deserialize_script(binascii.unhexlify(script)), + lambda x: safe_hexlify(x)) + out, pos = [], 0 + while pos < len(script): + code = from_byte_to_int(script[pos]) + if code == 0: + out.append(None) + pos += 1 + elif code <= 75: + out.append(script[pos+1:pos+1+code]) + pos += 1 + code + elif code <= 78: + szsz = pow(2, code - 76) + sz = decode(script[pos+szsz: pos:-1], 256) + out.append(script[pos + 1 + szsz:pos + 1 + szsz + sz]) + pos += 1 + szsz + sz + elif code <= 96: + out.append(code - 80) + pos += 1 + else: + out.append(code) + pos += 1 + return out + + +def serialize_script_unit(unit): + if isinstance(unit, int): + if unit < 16: + return from_int_to_byte(unit + 80) + else: + return from_int_to_byte(unit) + elif unit is None: + return b'\x00' + else: + if len(unit) <= 75: + return from_int_to_byte(len(unit))+unit + elif len(unit) < 256: + return from_int_to_byte(76)+from_int_to_byte(len(unit))+unit + elif len(unit) < 65536: + return from_int_to_byte(77)+encode(len(unit), 256, 2)[::-1]+unit + else: + return from_int_to_byte(78)+encode(len(unit), 256, 4)[::-1]+unit + + +if is_python2: + def serialize_script(script): + if json_is_base(script, 16): + return binascii.hexlify(serialize_script(json_changebase(script, + lambda x: binascii.unhexlify(x)))) + return ''.join(map(serialize_script_unit, script)) +else: + def serialize_script(script): + if json_is_base(script, 16): + return safe_hexlify(serialize_script(json_changebase(script, + lambda x: binascii.unhexlify(x)))) + + result = bytes() + for b in map(serialize_script_unit, script): + result += b if isinstance(b, bytes) else bytes(b, 'utf-8') + return result + + +def mk_multisig_script(*args): # [pubs],k or pub1,pub2...pub[n],k + if isinstance(args[0], list): + pubs, k = args[0], int(args[1]) + else: + pubs = list(filter(lambda x: len(str(x)) >= 32, args)) + k = int(args[len(pubs)]) + return serialize_script([k]+pubs+[len(pubs)]+[0xae]) + +# Signing and verifying + + +def verify_tx_input(tx, i, script, sig, pub): + if re.match('^[0-9a-fA-F]*$', tx): + tx = binascii.unhexlify(tx) + if re.match('^[0-9a-fA-F]*$', script): + script = binascii.unhexlify(script) + if not re.match('^[0-9a-fA-F]*$', sig): + sig = safe_hexlify(sig) + hashcode = decode(sig[-2:], 16) + modtx = signature_form(tx, int(i), script, hashcode) + return ecdsa_tx_verify(modtx, sig, pub, hashcode) + + +def sign(tx, i, priv, hashcode=SIGHASH_ALL): + i = int(i) + if (not is_python2 and isinstance(re, bytes)) or not re.match('^[0-9a-fA-F]*$', tx): + return binascii.unhexlify(sign(safe_hexlify(tx), i, priv)) + if len(priv) <= 33: + priv = safe_hexlify(priv) + pub = privkey_to_pubkey(priv) + address = pubkey_to_address(pub) + signing_tx = signature_form(tx, i, mk_pubkey_script(address), hashcode) + sig = ecdsa_tx_sign(signing_tx, priv, hashcode) + txobj = deserialize(tx) + txobj["ins"][i]["script"] = serialize_script([sig, pub]) + return serialize(txobj) + + +def signall(tx, priv): + # if priv is a dictionary, assume format is + # { 'txinhash:txinidx' : privkey } + if isinstance(priv, dict): + for e, i in enumerate(deserialize(tx)["ins"]): + k = priv["%s:%d" % (i["outpoint"]["hash"], i["outpoint"]["index"])] + tx = sign(tx, e, k) + else: + for i in range(len(deserialize(tx)["ins"])): + tx = sign(tx, i, priv) + return tx + + +def multisign(tx, i, script, pk, hashcode=SIGHASH_ALL): + if re.match('^[0-9a-fA-F]*$', tx): + tx = binascii.unhexlify(tx) + if re.match('^[0-9a-fA-F]*$', script): + script = binascii.unhexlify(script) + modtx = signature_form(tx, i, script, hashcode) + return ecdsa_tx_sign(modtx, pk, hashcode) + + +def apply_multisignatures(*args): + # tx,i,script,sigs OR tx,i,script,sig1,sig2...,sig[n] + tx, i, script = args[0], int(args[1]), args[2] + sigs = args[3] if isinstance(args[3], list) else list(args[3:]) + + if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script): + script = binascii.unhexlify(script) + sigs = [binascii.unhexlify(x) if x[:2] == '30' else x for x in sigs] + if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx): + return safe_hexlify(apply_multisignatures(binascii.unhexlify(tx), i, script, sigs)) + + # Not pushing empty elements on the top of the stack if passing no + # script (in case of bare multisig inputs there is no script) + script_blob = [] if script.__len__() == 0 else [script] + + txobj = deserialize(tx) + txobj["ins"][i]["script"] = serialize_script([None]+sigs+script_blob) + return serialize(txobj) + + +def is_inp(arg): + return len(arg) > 64 or "output" in arg or "outpoint" in arg + + +def mktx(*args): + # [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ... + ins, outs = [], [] + for arg in args: + if isinstance(arg, list): + for a in arg: (ins if is_inp(a) else outs).append(a) + else: + (ins if is_inp(arg) else outs).append(arg) + + txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []} + for i in ins: + if isinstance(i, dict) and "outpoint" in i: + txobj["ins"].append(i) + else: + if isinstance(i, dict) and "output" in i: + i = i["output"] + txobj["ins"].append({ + "outpoint": {"hash": i[:64], "index": int(i[65:])}, + "script": "", + "sequence": 4294967295 + }) + for o in outs: + if isinstance(o, string_or_bytes_types): + addr = o[:o.find(':')] + val = int(o[o.find(':')+1:]) + o = {} + if re.match('^[0-9a-fA-F]*$', addr): + o["script"] = addr + else: + o["address"] = addr + o["value"] = val + + outobj = {} + if "address" in o: + outobj["script"] = address_to_script(o["address"]) + elif "script" in o: + outobj["script"] = o["script"] + else: + raise Exception("Could not find 'address' or 'script' in output.") + outobj["value"] = o["value"] + txobj["outs"].append(outobj) + + return serialize(txobj) + + +def select(unspent, value): + value = int(value) + high = [u for u in unspent if u["value"] >= value] + high.sort(key=lambda u: u["value"]) + low = [u for u in unspent if u["value"] < value] + low.sort(key=lambda u: -u["value"]) + if len(high): + return [high[0]] + i, tv = 0, 0 + while tv < value and i < len(low): + tv += low[i]["value"] + i += 1 + if tv < value: + raise Exception("Not enough funds") + return low[:i] + +# Only takes inputs of the form { "output": blah, "value": foo } + + +def mksend(*args): + argz, change, fee = args[:-2], args[-2], int(args[-1]) + ins, outs = [], [] + for arg in argz: + if isinstance(arg, list): + for a in arg: + (ins if is_inp(a) else outs).append(a) + else: + (ins if is_inp(arg) else outs).append(arg) + + isum = sum([i["value"] for i in ins]) + osum, outputs2 = 0, [] + for o in outs: + if isinstance(o, string_types): + o2 = { + "address": o[:o.find(':')], + "value": int(o[o.find(':')+1:]) + } + else: + o2 = o + outputs2.append(o2) + osum += o2["value"] + + if isum < osum+fee: + raise Exception("Not enough money") + elif isum > osum+fee+5430: + outputs2 += [{"address": change, "value": isum-osum-fee}] + + return mktx(ins, outputs2) diff --git a/src/lib/pyelliptic/LICENSE b/src/lib/pyelliptic/LICENSE new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/src/lib/pyelliptic/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/src/lib/pyelliptic/README.md b/src/lib/pyelliptic/README.md new file mode 100644 index 00000000..3acf819c --- /dev/null +++ b/src/lib/pyelliptic/README.md @@ -0,0 +1,96 @@ +# PyElliptic + +PyElliptic is a high level wrapper for the cryptographic library : OpenSSL. +Under the GNU General Public License + +Python3 compatible. For GNU/Linux and Windows. +Require OpenSSL + +## Version + +The [upstream pyelliptic](https://github.com/yann2192/pyelliptic) has been +deprecated by the author at 1.5.8 and ECC API has been removed. + +This version is a fork of the pyelliptic extracted from the [BitMessage source +tree](https://github.com/Bitmessage/PyBitmessage), and does contain the ECC +API. To minimize confusion but to avoid renaming the module, major version has +been bumped. + +BitMessage is actively maintained, and this fork of pyelliptic will track and +incorporate any changes to pyelliptic from BitMessage. Ideally, in the future, +BitMessage would import this module as a dependency instead of maintaining a +copy of the source in its repository. + +The BitMessage fork forked from v1.3 of upstream pyelliptic. The commits in +this repository are the commits extracted from the BitMessage repository and +applied to pyelliptic v1.3 upstream repository (i.e. to the base of the fork), +so history with athorship is preserved. + +Some of the changes in upstream pyelliptic between 1.3 and 1.5.8 came from +BitMessage, those changes are present in this fork. Other changes do not exist +in this fork (they may be added in the future). + +Also, a few minor changes exist in this fork but is not (yet) present in +BitMessage source. See: + + git log 1.3-PyBitmessage-37489cf7feff8d5047f24baa8f6d27f353a6d6ac..HEAD + +## Features + +### Asymmetric cryptography using Elliptic Curve Cryptography (ECC) + +* Key agreement : ECDH +* Digital signatures : ECDSA +* Hybrid encryption : ECIES (like RSA) + +### Symmetric cryptography + +* AES-128 (CBC, OFB, CFB, CTR) +* AES-256 (CBC, OFB, CFB, CTR) +* Blowfish (CFB and CBC) +* RC4 + +### Other + +* CSPRNG +* HMAC (using SHA512) +* PBKDF2 (SHA256 and SHA512) + +## Example + +```python +#!/usr/bin/python + +import pyelliptic + +# Symmetric encryption +iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') +ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') + +ciphertext = ctx.update('test1') +ciphertext += ctx.update('test2') +ciphertext += ctx.final() + +ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') +print ctx2.ciphering(ciphertext) + +# Asymmetric encryption +alice = pyelliptic.ECC() # default curve: sect283r1 +bob = pyelliptic.ECC(curve='sect571r1') + +ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) +print bob.decrypt(ciphertext) + +signature = bob.sign("Hello Alice") +# alice's job : +print pyelliptic.ECC(pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") + +# ERROR !!! +try: + key = alice.get_ecdh_key(bob.get_pubkey()) +except: print("For ECDH key agreement, the keys must be defined on the same curve !") + +alice = pyelliptic.ECC(curve='sect571r1') +print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') +print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') +``` diff --git a/src/lib/pyelliptic/__init__.py b/src/lib/pyelliptic/__init__.py new file mode 100644 index 00000000..761d08af --- /dev/null +++ b/src/lib/pyelliptic/__init__.py @@ -0,0 +1,19 @@ +# Copyright (C) 2010 +# Author: Yann GUIBET +# Contact: + +__version__ = '1.3' + +__all__ = [ + 'OpenSSL', + 'ECC', + 'Cipher', + 'hmac_sha256', + 'hmac_sha512', + 'pbkdf2' +] + +from .openssl import OpenSSL +from .ecc import ECC +from .cipher import Cipher +from .hash import hmac_sha256, hmac_sha512, pbkdf2 diff --git a/src/lib/pyelliptic/arithmetic.py b/src/lib/pyelliptic/arithmetic.py new file mode 100644 index 00000000..95c85b93 --- /dev/null +++ b/src/lib/pyelliptic/arithmetic.py @@ -0,0 +1,144 @@ +# pylint: disable=missing-docstring,too-many-function-args + +import hashlib +import re + +P = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 +A = 0 +Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 +Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 +G = (Gx, Gy) + + +def inv(a, n): + lm, hm = 1, 0 + low, high = a % n, n + while low > 1: + r = high / low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return lm % n + + +def get_code_string(base): + if base == 2: + return '01' + elif base == 10: + return '0123456789' + elif base == 16: + return "0123456789abcdef" + elif base == 58: + return "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + elif base == 256: + return ''.join([chr(x) for x in range(256)]) + else: + raise ValueError("Invalid base!") + + +def encode(val, base, minlen=0): + code_string = get_code_string(base) + result = "" + while val > 0: + result = code_string[val % base] + result + val /= base + if len(result) < minlen: + result = code_string[0] * (minlen - len(result)) + result + return result + + +def decode(string, base): + code_string = get_code_string(base) + result = 0 + if base == 16: + string = string.lower() + while string: + result *= base + result += code_string.find(string[0]) + string = string[1:] + return result + + +def changebase(string, frm, to, minlen=0): + return encode(decode(string, frm), to, minlen) + + +def base10_add(a, b): + if a is None: + return b[0], b[1] + if b is None: + return a[0], a[1] + if a[0] == b[0]: + if a[1] == b[1]: + return base10_double(a[0], a[1]) + return None + m = ((b[1] - a[1]) * inv(b[0] - a[0], P)) % P + x = (m * m - a[0] - b[0]) % P + y = (m * (a[0] - x) - a[1]) % P + return (x, y) + + +def base10_double(a): + if a is None: + return None + m = ((3 * a[0] * a[0] + A) * inv(2 * a[1], P)) % P + x = (m * m - 2 * a[0]) % P + y = (m * (a[0] - x) - a[1]) % P + return (x, y) + + +def base10_multiply(a, n): + if n == 0: + return G + if n == 1: + return a + if (n % 2) == 0: + return base10_double(base10_multiply(a, n / 2)) + if (n % 2) == 1: + return base10_add(base10_double(base10_multiply(a, n / 2)), a) + return None + + +def hex_to_point(h): + return (decode(h[2:66], 16), decode(h[66:], 16)) + + +def point_to_hex(p): + return '04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) + + +def multiply(privkey, pubkey): + return point_to_hex(base10_multiply(hex_to_point(pubkey), decode(privkey, 16))) + + +def privtopub(privkey): + return point_to_hex(base10_multiply(G, decode(privkey, 16))) + + +def add(p1, p2): + if len(p1) == 32: + return encode(decode(p1, 16) + decode(p2, 16) % P, 16, 32) + return point_to_hex(base10_add(hex_to_point(p1), hex_to_point(p2))) + + +def hash_160(string): + intermed = hashlib.sha256(string).digest() + ripemd160 = hashlib.new('ripemd160') + ripemd160.update(intermed) + return ripemd160.digest() + + +def dbl_sha256(string): + return hashlib.sha256(hashlib.sha256(string).digest()).digest() + + +def bin_to_b58check(inp): + inp_fmtd = '\x00' + inp + leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0)) + checksum = dbl_sha256(inp_fmtd)[:4] + return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58) + +# Convert a public key (in hex) to a Bitcoin address + + +def pubkey_to_address(pubkey): + return bin_to_b58check(hash_160(changebase(pubkey, 16, 256))) diff --git a/src/lib/pyelliptic/cipher.py b/src/lib/pyelliptic/cipher.py new file mode 100644 index 00000000..54ae7a09 --- /dev/null +++ b/src/lib/pyelliptic/cipher.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2011 Yann GUIBET +# See LICENSE for details. + +from .openssl import OpenSSL + + +class Cipher: + """ + Symmetric encryption + + import pyelliptic + iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') + ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') + ciphertext = ctx.update('test1') + ciphertext += ctx.update('test2') + ciphertext += ctx.final() + + ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') + print ctx2.ciphering(ciphertext) + """ + def __init__(self, key, iv, do, ciphername='aes-256-cbc'): + """ + do == 1 => Encrypt; do == 0 => Decrypt + """ + self.cipher = OpenSSL.get_cipher(ciphername) + self.ctx = OpenSSL.EVP_CIPHER_CTX_new() + if do == 1 or do == 0: + k = OpenSSL.malloc(key, len(key)) + IV = OpenSSL.malloc(iv, len(iv)) + OpenSSL.EVP_CipherInit_ex( + self.ctx, self.cipher.get_pointer(), 0, k, IV, do) + else: + raise Exception("RTFM ...") + + @staticmethod + def get_all_cipher(): + """ + static method, returns all ciphers available + """ + return OpenSSL.cipher_algo.keys() + + @staticmethod + def get_blocksize(ciphername): + cipher = OpenSSL.get_cipher(ciphername) + return cipher.get_blocksize() + + @staticmethod + def gen_IV(ciphername): + cipher = OpenSSL.get_cipher(ciphername) + return OpenSSL.rand(cipher.get_blocksize()) + + def update(self, input): + i = OpenSSL.c_int(0) + buffer = OpenSSL.malloc(b"", len(input) + self.cipher.get_blocksize()) + inp = OpenSSL.malloc(input, len(input)) + if OpenSSL.EVP_CipherUpdate(self.ctx, OpenSSL.byref(buffer), + OpenSSL.byref(i), inp, len(input)) == 0: + raise Exception("[OpenSSL] EVP_CipherUpdate FAIL ...") + return buffer.raw[0:i.value] + + def final(self): + i = OpenSSL.c_int(0) + buffer = OpenSSL.malloc(b"", self.cipher.get_blocksize()) + if (OpenSSL.EVP_CipherFinal_ex(self.ctx, OpenSSL.byref(buffer), + OpenSSL.byref(i))) == 0: + raise Exception("[OpenSSL] EVP_CipherFinal_ex FAIL ...") + return buffer.raw[0:i.value] + + def ciphering(self, input): + """ + Do update and final in one method + """ + buff = self.update(input) + return buff + self.final() + + def __del__(self): + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_CIPHER_CTX_reset(self.ctx) + else: + OpenSSL.EVP_CIPHER_CTX_cleanup(self.ctx) + OpenSSL.EVP_CIPHER_CTX_free(self.ctx) diff --git a/src/lib/pyelliptic/ecc.py b/src/lib/pyelliptic/ecc.py new file mode 100644 index 00000000..a45f8d78 --- /dev/null +++ b/src/lib/pyelliptic/ecc.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +pyelliptic/ecc.py +===================== +""" +# pylint: disable=protected-access + +# Copyright (C) 2011 Yann GUIBET +# See LICENSE for details. + +from hashlib import sha512 +from struct import pack, unpack + +from .cipher import Cipher +from .hash import equals, hmac_sha256 +from .openssl import OpenSSL + + +class ECC(object): + """ + Asymmetric encryption with Elliptic Curve Cryptography (ECC) + ECDH, ECDSA and ECIES + + >>> import pyelliptic + + >>> alice = pyelliptic.ECC() # default curve: sect283r1 + >>> bob = pyelliptic.ECC(curve='sect571r1') + + >>> ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) + >>> print bob.decrypt(ciphertext) + + >>> signature = bob.sign("Hello Alice") + >>> # alice's job : + >>> print pyelliptic.ECC( + >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") + + >>> # ERROR !!! + >>> try: + >>> key = alice.get_ecdh_key(bob.get_pubkey()) + >>> except: + >>> print("For ECDH key agreement, the keys must be defined on the same curve !") + + >>> alice = pyelliptic.ECC(curve='sect571r1') + >>> print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') + >>> print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') + + """ + + def __init__( + self, + pubkey=None, + privkey=None, + pubkey_x=None, + pubkey_y=None, + raw_privkey=None, + curve='sect283r1', + ): # pylint: disable=too-many-arguments + """ + For a normal and High level use, specifie pubkey, + privkey (if you need) and the curve + """ + if isinstance(curve, str): + self.curve = OpenSSL.get_curve(curve) + else: + self.curve = curve + + if pubkey_x is not None and pubkey_y is not None: + self._set_keys(pubkey_x, pubkey_y, raw_privkey) + elif pubkey is not None: + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) + if privkey is not None: + curve2, raw_privkey, _ = ECC._decode_privkey(privkey) + if curve != curve2: + raise Exception("Bad ECC keys ...") + self.curve = curve + self._set_keys(pubkey_x, pubkey_y, raw_privkey) + else: + self.privkey, self.pubkey_x, self.pubkey_y = self._generate() + + def _set_keys(self, pubkey_x, pubkey_y, privkey): + if self.raw_check_key(privkey, pubkey_x, pubkey_y) < 0: + self.pubkey_x = None + self.pubkey_y = None + self.privkey = None + raise Exception("Bad ECC keys ...") + else: + self.pubkey_x = pubkey_x + self.pubkey_y = pubkey_y + self.privkey = privkey + + @staticmethod + def get_curves(): + """ + static method, returns the list of all the curves available + """ + return OpenSSL.curves.keys() + + def get_curve(self): + """Encryption object from curve name""" + return OpenSSL.get_curve_by_id(self.curve) + + def get_curve_id(self): + """Currently used curve""" + return self.curve + + def get_pubkey(self): + """ + High level function which returns : + curve(2) + len_of_pubkeyX(2) + pubkeyX + len_of_pubkeyY + pubkeyY + """ + return b''.join(( + pack('!H', self.curve), + pack('!H', len(self.pubkey_x)), + self.pubkey_x, + pack('!H', len(self.pubkey_y)), + self.pubkey_y, + )) + + def get_privkey(self): + """ + High level function which returns + curve(2) + len_of_privkey(2) + privkey + """ + return b''.join(( + pack('!H', self.curve), + pack('!H', len(self.privkey)), + self.privkey, + )) + + @staticmethod + def _decode_pubkey(pubkey): + i = 0 + curve = unpack('!H', pubkey[i:i + 2])[0] + i += 2 + tmplen = unpack('!H', pubkey[i:i + 2])[0] + i += 2 + pubkey_x = pubkey[i:i + tmplen] + i += tmplen + tmplen = unpack('!H', pubkey[i:i + 2])[0] + i += 2 + pubkey_y = pubkey[i:i + tmplen] + i += tmplen + return curve, pubkey_x, pubkey_y, i + + @staticmethod + def _decode_privkey(privkey): + i = 0 + curve = unpack('!H', privkey[i:i + 2])[0] + i += 2 + tmplen = unpack('!H', privkey[i:i + 2])[0] + i += 2 + privkey = privkey[i:i + tmplen] + i += tmplen + return curve, privkey, i + + def _generate(self): + try: + pub_key_x = OpenSSL.BN_new() + pub_key_y = OpenSSL.BN_new() + + key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + if key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + if (OpenSSL.EC_KEY_generate_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_generate_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + priv_key = OpenSSL.EC_KEY_get0_private_key(key) + + group = OpenSSL.EC_KEY_get0_group(key) + pub_key = OpenSSL.EC_KEY_get0_public_key(key) + + if OpenSSL.EC_POINT_get_affine_coordinates_GFp( + group, pub_key, pub_key_x, pub_key_y, 0) == 0: + raise Exception("[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ...") + + privkey = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(priv_key)) + pubkeyx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_x)) + pubkeyy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_y)) + OpenSSL.BN_bn2bin(priv_key, privkey) + privkey = privkey.raw + OpenSSL.BN_bn2bin(pub_key_x, pubkeyx) + pubkeyx = pubkeyx.raw + OpenSSL.BN_bn2bin(pub_key_y, pubkeyy) + pubkeyy = pubkeyy.raw + self.raw_check_key(privkey, pubkeyx, pubkeyy) + + return privkey, pubkeyx, pubkeyy + + finally: + OpenSSL.EC_KEY_free(key) + OpenSSL.BN_free(pub_key_x) + OpenSSL.BN_free(pub_key_y) + + def get_ecdh_key(self, pubkey): + """ + High level function. Compute public key with the local private key + and returns a 512bits shared key + """ + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) + if curve != self.curve: + raise Exception("ECC keys must be from the same curve !") + return sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() + + def raw_get_ecdh_key(self, pubkey_x, pubkey_y): + """ECDH key as binary data""" + try: + ecdh_keybuffer = OpenSSL.malloc(0, 32) + + other_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + if other_key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + + other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) + other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) + + other_group = OpenSSL.EC_KEY_get0_group(other_key) + other_pub_key = OpenSSL.EC_POINT_new(other_group) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group, + other_pub_key, + other_pub_key_x, + other_pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") + if (OpenSSL.EC_KEY_set_public_key(other_key, other_pub_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(other_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + + own_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + if own_key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + own_priv_key = OpenSSL.BN_bin2bn( + self.privkey, len(self.privkey), 0) + + if (OpenSSL.EC_KEY_set_private_key(own_key, own_priv_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") + + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EC_KEY_set_method(own_key, OpenSSL.EC_KEY_OpenSSL()) + else: + OpenSSL.ECDH_set_method(own_key, OpenSSL.ECDH_OpenSSL()) + ecdh_keylen = OpenSSL.ECDH_compute_key( + ecdh_keybuffer, 32, other_pub_key, own_key, 0) + + if ecdh_keylen != 32: + raise Exception("[OpenSSL] ECDH keylen FAIL ...") + + return ecdh_keybuffer.raw + + finally: + OpenSSL.EC_KEY_free(other_key) + OpenSSL.BN_free(other_pub_key_x) + OpenSSL.BN_free(other_pub_key_y) + OpenSSL.EC_POINT_free(other_pub_key) + OpenSSL.EC_KEY_free(own_key) + OpenSSL.BN_free(own_priv_key) + + def check_key(self, privkey, pubkey): + """ + Check the public key and the private key. + The private key is optional (replace by None) + """ + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) + if privkey is None: + raw_privkey = None + curve2 = curve + else: + curve2, raw_privkey, _ = ECC._decode_privkey(privkey) + if curve != curve2: + raise Exception("Bad public and private key") + return self.raw_check_key(raw_privkey, pubkey_x, pubkey_y, curve) + + def raw_check_key(self, privkey, pubkey_x, pubkey_y, curve=None): + """Check key validity, key is supplied as binary data""" + # pylint: disable=too-many-branches + if curve is None: + curve = self.curve + elif isinstance(curve, str): + curve = OpenSSL.get_curve(curve) + else: + curve = curve + try: + key = OpenSSL.EC_KEY_new_by_curve_name(curve) + if key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + if privkey is not None: + priv_key = OpenSSL.BN_bin2bn(privkey, len(privkey), 0) + pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) + pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) + + if privkey is not None: + if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: + raise Exception( + "[OpenSSL] EC_KEY_set_private_key FAIL ...") + + group = OpenSSL.EC_KEY_get0_group(key) + pub_key = OpenSSL.EC_POINT_new(group) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") + if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + return 0 + + finally: + OpenSSL.EC_KEY_free(key) + OpenSSL.BN_free(pub_key_x) + OpenSSL.BN_free(pub_key_y) + OpenSSL.EC_POINT_free(pub_key) + if privkey is not None: + OpenSSL.BN_free(priv_key) + + def sign(self, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): + """ + Sign the input with ECDSA method and returns the signature + """ + # pylint: disable=too-many-branches,too-many-locals + try: + size = len(inputb) + buff = OpenSSL.malloc(inputb, size) + digest = OpenSSL.malloc(0, 64) + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + md_ctx = OpenSSL.EVP_MD_CTX_new() + else: + md_ctx = OpenSSL.EVP_MD_CTX_create() + dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) + siglen = OpenSSL.pointer(OpenSSL.c_int(0)) + sig = OpenSSL.malloc(0, 151) + + key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + if key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + + priv_key = OpenSSL.BN_bin2bn(self.privkey, len(self.privkey), 0) + pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) + pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) + + if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") + + group = OpenSSL.EC_KEY_get0_group(key) + pub_key = OpenSSL.EC_POINT_new(group) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") + if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_MD_CTX_new(md_ctx) + else: + OpenSSL.EVP_MD_CTX_init(md_ctx) + OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) + + if (OpenSSL.EVP_DigestUpdate(md_ctx, buff, size)) == 0: + raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") + OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) + OpenSSL.ECDSA_sign(0, digest, dgst_len.contents, sig, siglen, key) + if (OpenSSL.ECDSA_verify(0, digest, dgst_len.contents, sig, + siglen.contents, key)) != 1: + raise Exception("[OpenSSL] ECDSA_verify FAIL ...") + + return sig.raw[:siglen.contents.value] + + finally: + OpenSSL.EC_KEY_free(key) + OpenSSL.BN_free(pub_key_x) + OpenSSL.BN_free(pub_key_y) + OpenSSL.BN_free(priv_key) + OpenSSL.EC_POINT_free(pub_key) + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_MD_CTX_free(md_ctx) + else: + OpenSSL.EVP_MD_CTX_destroy(md_ctx) + + def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): + """ + Verify the signature with the input and the local public key. + Returns a boolean + """ + # pylint: disable=too-many-branches + try: + bsig = OpenSSL.malloc(sig, len(sig)) + binputb = OpenSSL.malloc(inputb, len(inputb)) + digest = OpenSSL.malloc(0, 64) + dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + md_ctx = OpenSSL.EVP_MD_CTX_new() + else: + md_ctx = OpenSSL.EVP_MD_CTX_create() + key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) + + if key == 0: + raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") + + pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) + pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) + group = OpenSSL.EC_KEY_get0_group(key) + pub_key = OpenSSL.EC_POINT_new(group) + + if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, + pub_key_x, + pub_key_y, + 0)) == 0: + raise Exception( + "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") + if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: + raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") + if (OpenSSL.EC_KEY_check_key(key)) == 0: + raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_MD_CTX_new(md_ctx) + else: + OpenSSL.EVP_MD_CTX_init(md_ctx) + OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) + if (OpenSSL.EVP_DigestUpdate(md_ctx, binputb, len(inputb))) == 0: + raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") + + OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) + ret = OpenSSL.ECDSA_verify( + 0, digest, dgst_len.contents, bsig, len(sig), key) + + if ret == -1: + return False # Fail to Check + if ret == 0: + return False # Bad signature ! + return True # Good + + finally: + OpenSSL.EC_KEY_free(key) + OpenSSL.BN_free(pub_key_x) + OpenSSL.BN_free(pub_key_y) + OpenSSL.EC_POINT_free(pub_key) + if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: + OpenSSL.EVP_MD_CTX_free(md_ctx) + else: + OpenSSL.EVP_MD_CTX_destroy(md_ctx) + + @staticmethod + def encrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): + """ + Encrypt data with ECIES method using the public key of the recipient. + """ + curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) + return ECC.raw_encrypt(data, pubkey_x, pubkey_y, curve=curve, + ephemcurve=ephemcurve, ciphername=ciphername) + + @staticmethod + def raw_encrypt( + data, + pubkey_x, + pubkey_y, + curve='sect283r1', + ephemcurve=None, + ciphername='aes-256-cbc', + ): # pylint: disable=too-many-arguments + """ECHD encryption, keys supplied in binary data format""" + + if ephemcurve is None: + ephemcurve = curve + ephem = ECC(curve=ephemcurve) + key = sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() + key_e, key_m = key[:32], key[32:] + pubkey = ephem.get_pubkey() + iv = OpenSSL.rand(OpenSSL.get_cipher(ciphername).get_blocksize()) + ctx = Cipher(key_e, iv, 1, ciphername) + ciphertext = iv + pubkey + ctx.ciphering(data) + mac = hmac_sha256(key_m, ciphertext) + return ciphertext + mac + + def decrypt(self, data, ciphername='aes-256-cbc'): + """ + Decrypt data with ECIES method using the local private key + """ + # pylint: disable=too-many-locals + blocksize = OpenSSL.get_cipher(ciphername).get_blocksize() + iv = data[:blocksize] + i = blocksize + _, pubkey_x, pubkey_y, i2 = ECC._decode_pubkey(data[i:]) + i += i2 + ciphertext = data[i:len(data) - 32] + i += len(ciphertext) + mac = data[i:] + key = sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() + key_e, key_m = key[:32], key[32:] + if not equals(hmac_sha256(key_m, data[:len(data) - 32]), mac): + raise RuntimeError("Fail to verify data") + ctx = Cipher(key_e, iv, 0, ciphername) + return ctx.ciphering(ciphertext) diff --git a/src/lib/pyelliptic/hash.py b/src/lib/pyelliptic/hash.py new file mode 100644 index 00000000..d6a15811 --- /dev/null +++ b/src/lib/pyelliptic/hash.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2011 Yann GUIBET +# See LICENSE for details. + +from .openssl import OpenSSL + + +# For python3 +def _equals_bytes(a, b): + if len(a) != len(b): + return False + result = 0 + for x, y in zip(a, b): + result |= x ^ y + return result == 0 + + +def _equals_str(a, b): + if len(a) != len(b): + return False + result = 0 + for x, y in zip(a, b): + result |= ord(x) ^ ord(y) + return result == 0 + + +def equals(a, b): + if isinstance(a, str): + return _equals_str(a, b) + else: + return _equals_bytes(a, b) + + +def hmac_sha256(k, m): + """ + Compute the key and the message with HMAC SHA5256 + """ + key = OpenSSL.malloc(k, len(k)) + d = OpenSSL.malloc(m, len(m)) + md = OpenSSL.malloc(0, 32) + i = OpenSSL.pointer(OpenSSL.c_int(0)) + OpenSSL.HMAC(OpenSSL.EVP_sha256(), key, len(k), d, len(m), md, i) + return md.raw + + +def hmac_sha512(k, m): + """ + Compute the key and the message with HMAC SHA512 + """ + key = OpenSSL.malloc(k, len(k)) + d = OpenSSL.malloc(m, len(m)) + md = OpenSSL.malloc(0, 64) + i = OpenSSL.pointer(OpenSSL.c_int(0)) + OpenSSL.HMAC(OpenSSL.EVP_sha512(), key, len(k), d, len(m), md, i) + return md.raw + + +def pbkdf2(password, salt=None, i=10000, keylen=64): + if salt is None: + salt = OpenSSL.rand(8) + p_password = OpenSSL.malloc(password, len(password)) + p_salt = OpenSSL.malloc(salt, len(salt)) + output = OpenSSL.malloc(0, keylen) + OpenSSL.PKCS5_PBKDF2_HMAC(p_password, len(password), p_salt, + len(p_salt), i, OpenSSL.EVP_sha256(), + keylen, output) + return salt, output.raw diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py new file mode 100644 index 00000000..bc4fe6a6 --- /dev/null +++ b/src/lib/pyelliptic/openssl.py @@ -0,0 +1,553 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (C) 2011 Yann GUIBET +# See LICENSE for details. +# +# Software slightly changed by Jonathan Warren + +import sys +import ctypes + +OpenSSL = None + + +class CipherName: + def __init__(self, name, pointer, blocksize): + self._name = name + self._pointer = pointer + self._blocksize = blocksize + + def __str__(self): + return "Cipher : " + self._name + " | Blocksize : " + str(self._blocksize) + " | Fonction pointer : " + str(self._pointer) + + def get_pointer(self): + return self._pointer() + + def get_name(self): + return self._name + + def get_blocksize(self): + return self._blocksize + + +def get_version(library): + version = None + hexversion = None + cflags = None + try: + #OpenSSL 1.1 + OPENSSL_VERSION = 0 + OPENSSL_CFLAGS = 1 + library.OpenSSL_version.argtypes = [ctypes.c_int] + library.OpenSSL_version.restype = ctypes.c_char_p + version = library.OpenSSL_version(OPENSSL_VERSION) + cflags = library.OpenSSL_version(OPENSSL_CFLAGS) + library.OpenSSL_version_num.restype = ctypes.c_long + hexversion = library.OpenSSL_version_num() + except AttributeError: + try: + #OpenSSL 1.0 + SSLEAY_VERSION = 0 + SSLEAY_CFLAGS = 2 + library.SSLeay.restype = ctypes.c_long + library.SSLeay_version.restype = ctypes.c_char_p + library.SSLeay_version.argtypes = [ctypes.c_int] + version = library.SSLeay_version(SSLEAY_VERSION) + cflags = library.SSLeay_version(SSLEAY_CFLAGS) + hexversion = library.SSLeay() + except AttributeError: + #raise NotImplementedError('Cannot determine version of this OpenSSL library.') + pass + return (version, hexversion, cflags) + + +class _OpenSSL: + """ + Wrapper for OpenSSL using ctypes + """ + def __init__(self, library): + """ + Build the wrapper + """ + self._lib = ctypes.CDLL(library) + self._version, self._hexversion, self._cflags = get_version(self._lib) + self._libreSSL = self._version.startswith(b"LibreSSL") + + self.pointer = ctypes.pointer + self.c_int = ctypes.c_int + self.byref = ctypes.byref + self.create_string_buffer = ctypes.create_string_buffer + + self.BN_new = self._lib.BN_new + self.BN_new.restype = ctypes.c_void_p + self.BN_new.argtypes = [] + + self.BN_free = self._lib.BN_free + self.BN_free.restype = None + self.BN_free.argtypes = [ctypes.c_void_p] + + self.BN_num_bits = self._lib.BN_num_bits + self.BN_num_bits.restype = ctypes.c_int + self.BN_num_bits.argtypes = [ctypes.c_void_p] + + self.BN_bn2bin = self._lib.BN_bn2bin + self.BN_bn2bin.restype = ctypes.c_int + self.BN_bn2bin.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.BN_bin2bn = self._lib.BN_bin2bn + self.BN_bin2bn.restype = ctypes.c_void_p + self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p] + + self.EC_KEY_free = self._lib.EC_KEY_free + self.EC_KEY_free.restype = None + self.EC_KEY_free.argtypes = [ctypes.c_void_p] + + self.EC_KEY_new_by_curve_name = self._lib.EC_KEY_new_by_curve_name + self.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p + self.EC_KEY_new_by_curve_name.argtypes = [ctypes.c_int] + + self.EC_KEY_generate_key = self._lib.EC_KEY_generate_key + self.EC_KEY_generate_key.restype = ctypes.c_int + self.EC_KEY_generate_key.argtypes = [ctypes.c_void_p] + + self.EC_KEY_check_key = self._lib.EC_KEY_check_key + self.EC_KEY_check_key.restype = ctypes.c_int + self.EC_KEY_check_key.argtypes = [ctypes.c_void_p] + + self.EC_KEY_get0_private_key = self._lib.EC_KEY_get0_private_key + self.EC_KEY_get0_private_key.restype = ctypes.c_void_p + self.EC_KEY_get0_private_key.argtypes = [ctypes.c_void_p] + + self.EC_KEY_get0_public_key = self._lib.EC_KEY_get0_public_key + self.EC_KEY_get0_public_key.restype = ctypes.c_void_p + self.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p] + + self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group + self.EC_KEY_get0_group.restype = ctypes.c_void_p + self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] + + self.EC_POINT_get_affine_coordinates_GFp = self._lib.EC_POINT_get_affine_coordinates_GFp + self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int + self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key + self.EC_KEY_set_private_key.restype = ctypes.c_int + self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + self.EC_KEY_set_public_key = self._lib.EC_KEY_set_public_key + self.EC_KEY_set_public_key.restype = ctypes.c_int + self.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + self.EC_KEY_set_group = self._lib.EC_KEY_set_group + self.EC_KEY_set_group.restype = ctypes.c_int + self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.EC_POINT_set_affine_coordinates_GFp = self._lib.EC_POINT_set_affine_coordinates_GFp + self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int + self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.EC_POINT_new = self._lib.EC_POINT_new + self.EC_POINT_new.restype = ctypes.c_void_p + self.EC_POINT_new.argtypes = [ctypes.c_void_p] + + self.EC_POINT_free = self._lib.EC_POINT_free + self.EC_POINT_free.restype = None + self.EC_POINT_free.argtypes = [ctypes.c_void_p] + + self.BN_CTX_free = self._lib.BN_CTX_free + self.BN_CTX_free.restype = None + self.BN_CTX_free.argtypes = [ctypes.c_void_p] + + self.EC_POINT_mul = self._lib.EC_POINT_mul + self.EC_POINT_mul.restype = ctypes.c_int + self.EC_POINT_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key + self.EC_KEY_set_private_key.restype = ctypes.c_int + self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, + ctypes.c_void_p] + + if self._hexversion >= 0x10100000 and not self._libreSSL: + self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL + self._lib.EC_KEY_OpenSSL.restype = ctypes.c_void_p + self._lib.EC_KEY_OpenSSL.argtypes = [] + + self.EC_KEY_set_method = self._lib.EC_KEY_set_method + self._lib.EC_KEY_set_method.restype = ctypes.c_int + self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + else: + self.ECDH_OpenSSL = self._lib.ECDH_OpenSSL + self._lib.ECDH_OpenSSL.restype = ctypes.c_void_p + self._lib.ECDH_OpenSSL.argtypes = [] + + self.ECDH_set_method = self._lib.ECDH_set_method + self._lib.ECDH_set_method.restype = ctypes.c_int + self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.BN_CTX_new = self._lib.BN_CTX_new + self._lib.BN_CTX_new.restype = ctypes.c_void_p + self._lib.BN_CTX_new.argtypes = [] + + self.ECDH_compute_key = self._lib.ECDH_compute_key + self.ECDH_compute_key.restype = ctypes.c_int + self.ECDH_compute_key.argtypes = [ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_CipherInit_ex = self._lib.EVP_CipherInit_ex + self.EVP_CipherInit_ex.restype = ctypes.c_int + self.EVP_CipherInit_ex.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_CIPHER_CTX_new = self._lib.EVP_CIPHER_CTX_new + self.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p + self.EVP_CIPHER_CTX_new.argtypes = [] + + # Cipher + self.EVP_aes_128_cfb128 = self._lib.EVP_aes_128_cfb128 + self.EVP_aes_128_cfb128.restype = ctypes.c_void_p + self.EVP_aes_128_cfb128.argtypes = [] + + self.EVP_aes_256_cfb128 = self._lib.EVP_aes_256_cfb128 + self.EVP_aes_256_cfb128.restype = ctypes.c_void_p + self.EVP_aes_256_cfb128.argtypes = [] + + self.EVP_aes_128_cbc = self._lib.EVP_aes_128_cbc + self.EVP_aes_128_cbc.restype = ctypes.c_void_p + self.EVP_aes_128_cbc.argtypes = [] + + self.EVP_aes_256_cbc = self._lib.EVP_aes_256_cbc + self.EVP_aes_256_cbc.restype = ctypes.c_void_p + self.EVP_aes_256_cbc.argtypes = [] + + #self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr + #self.EVP_aes_128_ctr.restype = ctypes.c_void_p + #self.EVP_aes_128_ctr.argtypes = [] + + #self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr + #self.EVP_aes_256_ctr.restype = ctypes.c_void_p + #self.EVP_aes_256_ctr.argtypes = [] + + self.EVP_aes_128_ofb = self._lib.EVP_aes_128_ofb + self.EVP_aes_128_ofb.restype = ctypes.c_void_p + self.EVP_aes_128_ofb.argtypes = [] + + self.EVP_aes_256_ofb = self._lib.EVP_aes_256_ofb + self.EVP_aes_256_ofb.restype = ctypes.c_void_p + self.EVP_aes_256_ofb.argtypes = [] + + self.EVP_bf_cbc = self._lib.EVP_bf_cbc + self.EVP_bf_cbc.restype = ctypes.c_void_p + self.EVP_bf_cbc.argtypes = [] + + self.EVP_bf_cfb64 = self._lib.EVP_bf_cfb64 + self.EVP_bf_cfb64.restype = ctypes.c_void_p + self.EVP_bf_cfb64.argtypes = [] + + self.EVP_rc4 = self._lib.EVP_rc4 + self.EVP_rc4.restype = ctypes.c_void_p + self.EVP_rc4.argtypes = [] + + if self._hexversion >= 0x10100000 and not self._libreSSL: + self.EVP_CIPHER_CTX_reset = self._lib.EVP_CIPHER_CTX_reset + self.EVP_CIPHER_CTX_reset.restype = ctypes.c_int + self.EVP_CIPHER_CTX_reset.argtypes = [ctypes.c_void_p] + else: + self.EVP_CIPHER_CTX_cleanup = self._lib.EVP_CIPHER_CTX_cleanup + self.EVP_CIPHER_CTX_cleanup.restype = ctypes.c_int + self.EVP_CIPHER_CTX_cleanup.argtypes = [ctypes.c_void_p] + + self.EVP_CIPHER_CTX_free = self._lib.EVP_CIPHER_CTX_free + self.EVP_CIPHER_CTX_free.restype = None + self.EVP_CIPHER_CTX_free.argtypes = [ctypes.c_void_p] + + self.EVP_CipherUpdate = self._lib.EVP_CipherUpdate + self.EVP_CipherUpdate.restype = ctypes.c_int + self.EVP_CipherUpdate.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] + + self.EVP_CipherFinal_ex = self._lib.EVP_CipherFinal_ex + self.EVP_CipherFinal_ex.restype = ctypes.c_int + self.EVP_CipherFinal_ex.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_DigestInit = self._lib.EVP_DigestInit + self.EVP_DigestInit.restype = ctypes.c_int + self._lib.EVP_DigestInit.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex + self.EVP_DigestInit_ex.restype = ctypes.c_int + self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p] + + self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate + self.EVP_DigestUpdate.restype = ctypes.c_int + self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_int] + + self.EVP_DigestFinal = self._lib.EVP_DigestFinal + self.EVP_DigestFinal.restype = ctypes.c_int + self.EVP_DigestFinal.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_DigestFinal_ex = self._lib.EVP_DigestFinal_ex + self.EVP_DigestFinal_ex.restype = ctypes.c_int + self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p] + + self.ECDSA_sign = self._lib.ECDSA_sign + self.ECDSA_sign.restype = ctypes.c_int + self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] + + self.ECDSA_verify = self._lib.ECDSA_verify + self.ECDSA_verify.restype = ctypes.c_int + self.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] + + if self._hexversion >= 0x10100000 and not self._libreSSL: + self.EVP_MD_CTX_new = self._lib.EVP_MD_CTX_new + self.EVP_MD_CTX_new.restype = ctypes.c_void_p + self.EVP_MD_CTX_new.argtypes = [] + + self.EVP_MD_CTX_reset = self._lib.EVP_MD_CTX_reset + self.EVP_MD_CTX_reset.restype = None + self.EVP_MD_CTX_reset.argtypes = [ctypes.c_void_p] + + self.EVP_MD_CTX_free = self._lib.EVP_MD_CTX_free + self.EVP_MD_CTX_free.restype = None + self.EVP_MD_CTX_free.argtypes = [ctypes.c_void_p] + + self.EVP_sha1 = self._lib.EVP_sha1 + self.EVP_sha1.restype = ctypes.c_void_p + self.EVP_sha1.argtypes = [] + + self.digest_ecdsa_sha1 = self.EVP_sha1 + else: + self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create + self.EVP_MD_CTX_create.restype = ctypes.c_void_p + self.EVP_MD_CTX_create.argtypes = [] + + self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init + self.EVP_MD_CTX_init.restype = None + self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p] + + self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy + self.EVP_MD_CTX_destroy.restype = None + self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p] + + self.EVP_ecdsa = self._lib.EVP_ecdsa + self._lib.EVP_ecdsa.restype = ctypes.c_void_p + self._lib.EVP_ecdsa.argtypes = [] + + self.digest_ecdsa_sha1 = self.EVP_ecdsa + + self.RAND_bytes = self._lib.RAND_bytes + self.RAND_bytes.restype = ctypes.c_int + self.RAND_bytes.argtypes = [ctypes.c_void_p, ctypes.c_int] + + self.EVP_sha256 = self._lib.EVP_sha256 + self.EVP_sha256.restype = ctypes.c_void_p + self.EVP_sha256.argtypes = [] + + self.i2o_ECPublicKey = self._lib.i2o_ECPublicKey + self.i2o_ECPublicKey.restype = ctypes.c_int + self.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p] + + self.EVP_sha512 = self._lib.EVP_sha512 + self.EVP_sha512.restype = ctypes.c_void_p + self.EVP_sha512.argtypes = [] + + self.HMAC = self._lib.HMAC + self.HMAC.restype = ctypes.c_void_p + self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] + + try: + self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC + except: + # The above is not compatible with all versions of OSX. + self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 + + self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int + self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int, + ctypes.c_void_p, ctypes.c_int, + ctypes.c_int, ctypes.c_void_p, + ctypes.c_int, ctypes.c_void_p] + + self._set_ciphers() + self._set_curves() + + def _set_ciphers(self): + self.cipher_algo = { + 'aes-128-cbc': CipherName('aes-128-cbc', self.EVP_aes_128_cbc, 16), + 'aes-256-cbc': CipherName('aes-256-cbc', self.EVP_aes_256_cbc, 16), + 'aes-128-cfb': CipherName('aes-128-cfb', self.EVP_aes_128_cfb128, 16), + 'aes-256-cfb': CipherName('aes-256-cfb', self.EVP_aes_256_cfb128, 16), + 'aes-128-ofb': CipherName('aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), + 'aes-256-ofb': CipherName('aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), + #'aes-128-ctr': CipherName('aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), + #'aes-256-ctr': CipherName('aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), + 'bf-cfb': CipherName('bf-cfb', self.EVP_bf_cfb64, 8), + 'bf-cbc': CipherName('bf-cbc', self.EVP_bf_cbc, 8), + 'rc4': CipherName('rc4', self.EVP_rc4, 128), # 128 is the initialisation size not block size + } + + def _set_curves(self): + self.curves = { + 'secp112r1': 704, + 'secp112r2': 705, + 'secp128r1': 706, + 'secp128r2': 707, + 'secp160k1': 708, + 'secp160r1': 709, + 'secp160r2': 710, + 'secp192k1': 711, + 'secp224k1': 712, + 'secp224r1': 713, + 'secp256k1': 714, + 'secp384r1': 715, + 'secp521r1': 716, + 'sect113r1': 717, + 'sect113r2': 718, + 'sect131r1': 719, + 'sect131r2': 720, + 'sect163k1': 721, + 'sect163r1': 722, + 'sect163r2': 723, + 'sect193r1': 724, + 'sect193r2': 725, + 'sect233k1': 726, + 'sect233r1': 727, + 'sect239k1': 728, + 'sect283k1': 729, + 'sect283r1': 730, + 'sect409k1': 731, + 'sect409r1': 732, + 'sect571k1': 733, + 'sect571r1': 734, + } + + def BN_num_bytes(self, x): + """ + returns the length of a BN (OpenSSl API) + """ + return int((self.BN_num_bits(x) + 7) / 8) + + def get_cipher(self, name): + """ + returns the OpenSSL cipher instance + """ + if name not in self.cipher_algo: + raise Exception("Unknown cipher") + return self.cipher_algo[name] + + def get_curve(self, name): + """ + returns the id of a elliptic curve + """ + if name not in self.curves: + raise Exception("Unknown curve") + return self.curves[name] + + def get_curve_by_id(self, id): + """ + returns the name of a elliptic curve with his id + """ + res = None + for i in self.curves: + if self.curves[i] == id: + res = i + break + if res is None: + raise Exception("Unknown curve") + return res + + def rand(self, size): + """ + OpenSSL random function + """ + buffer = self.malloc(0, size) + # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is + # evidently possible that it returned an error and not-actually-random data. However, in + # tests on various operating systems, while generating hundreds of gigabytes of random + # strings of various sizes I could not get an error to occur. Also Bitcoin doesn't check + # the return value of RAND_bytes either. + # Fixed in Bitmessage version 0.4.2 (in source code on 2013-10-13) + while self.RAND_bytes(buffer, size) != 1: + import time + time.sleep(1) + return buffer.raw + + def malloc(self, data, size): + """ + returns a create_string_buffer (ctypes) + """ + buffer = None + if data != 0: + if sys.version_info.major == 3 and isinstance(data, type('')): + data = data.encode() + buffer = self.create_string_buffer(data, size) + else: + buffer = self.create_string_buffer(size) + return buffer + +def loadOpenSSL(): + global OpenSSL + from os import path, environ + from ctypes.util import find_library + + libdir = [] + + if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: + libdir.append(find_library('ssl')) + elif 'win32' in sys.platform or 'win64' in sys.platform: + libdir.append(find_library('libeay32')) + + if getattr(sys,'frozen', None): + if 'darwin' in sys.platform: + libdir.extend([ + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.1.0.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.2.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.1.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.0.dylib'), + path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.0.9.8.dylib'), + ]) + elif 'win32' in sys.platform or 'win64' in sys.platform: + libdir.append(path.join(sys._MEIPASS, 'libeay32.dll')) + else: + libdir.extend([ + path.join(sys._MEIPASS, 'libcrypto.so'), + path.join(sys._MEIPASS, 'libssl.so'), + path.join(sys._MEIPASS, 'libcrypto.so.1.1.0'), + path.join(sys._MEIPASS, 'libssl.so.1.1.0'), + path.join(sys._MEIPASS, 'libcrypto.so.1.0.2'), + path.join(sys._MEIPASS, 'libssl.so.1.0.2'), + path.join(sys._MEIPASS, 'libcrypto.so.1.0.1'), + path.join(sys._MEIPASS, 'libssl.so.1.0.1'), + path.join(sys._MEIPASS, 'libcrypto.so.1.0.0'), + path.join(sys._MEIPASS, 'libssl.so.1.0.0'), + path.join(sys._MEIPASS, 'libcrypto.so.0.9.8'), + path.join(sys._MEIPASS, 'libssl.so.0.9.8'), + ]) + if 'darwin' in sys.platform: + libdir.extend(['libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) + elif 'win32' in sys.platform or 'win64' in sys.platform: + libdir.append('libeay32.dll') + else: + libdir.append('libcrypto.so') + libdir.append('libssl.so') + libdir.append('libcrypto.so.1.0.0') + libdir.append('libssl.so.1.0.0') + for library in libdir: + try: + OpenSSL = _OpenSSL(library) + return + except: + pass + raise Exception("Failed to load OpenSSL library, searched for: " + " ".join(libdir)) + +loadOpenSSL() diff --git a/src/lib/pyelliptic/setup.py b/src/lib/pyelliptic/setup.py new file mode 100644 index 00000000..cc9c0a21 --- /dev/null +++ b/src/lib/pyelliptic/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +setup( + name="pyelliptic", + version='2.0.1', + url='https://github.com/radfish/pyelliptic', + license='GPL', + description="Python OpenSSL wrapper for ECC (ECDSA, ECIES), AES, HMAC, Blowfish, ...", + author='Yann GUIBET', + author_email='yannguibet@gmail.com', + maintainer="redfish", + maintainer_email='redfish@galactica.pw', + packages=find_packages(), + classifiers=[ + 'Operating System :: Unix', + 'Operating System :: Microsoft :: Windows', + 'Environment :: MacOS X', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.7', + 'Topic :: Security :: Cryptography', + ], +) diff --git a/src/lib/sslcrypto/LICENSE b/src/lib/sslcrypto/LICENSE deleted file mode 100644 index 2feefc45..00000000 --- a/src/lib/sslcrypto/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -MIT License - -Copyright (c) 2019 Ivan Machugovskiy - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -Additionally, the following licenses must be preserved: - -- ripemd implementation is licensed under BSD-3 by Markus Friedl, see `_ripemd.py`; -- jacobian curve implementation is dual-licensed under MIT or public domain license, see `_jacobian.py`. diff --git a/src/lib/sslcrypto/__init__.py b/src/lib/sslcrypto/__init__.py deleted file mode 100644 index 77f9b3f3..00000000 --- a/src/lib/sslcrypto/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -__all__ = ["aes", "ecc", "rsa"] - -try: - from .openssl import aes, ecc, rsa -except OSError: - from .fallback import aes, ecc, rsa diff --git a/src/lib/sslcrypto/_aes.py b/src/lib/sslcrypto/_aes.py deleted file mode 100644 index 4f8d4ec2..00000000 --- a/src/lib/sslcrypto/_aes.py +++ /dev/null @@ -1,53 +0,0 @@ -# pylint: disable=import-outside-toplevel - -class AES: - def __init__(self, backend, fallback=None): - self._backend = backend - self._fallback = fallback - - - def get_algo_key_length(self, algo): - if algo.count("-") != 2: - raise ValueError("Invalid algorithm name") - try: - return int(algo.split("-")[1]) // 8 - except ValueError: - raise ValueError("Invalid algorithm name") from None - - - def new_key(self, algo="aes-256-cbc"): - if not self._backend.is_algo_supported(algo): - if self._fallback is None: - raise ValueError("This algorithm is not supported") - return self._fallback.new_key(algo) - return self._backend.random(self.get_algo_key_length(algo)) - - - def encrypt(self, data, key, algo="aes-256-cbc"): - if not self._backend.is_algo_supported(algo): - if self._fallback is None: - raise ValueError("This algorithm is not supported") - return self._fallback.encrypt(data, key, algo) - - key_length = self.get_algo_key_length(algo) - if len(key) != key_length: - raise ValueError("Expected key to be {} bytes, got {} bytes".format(key_length, len(key))) - - return self._backend.encrypt(data, key, algo) - - - def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): - if not self._backend.is_algo_supported(algo): - if self._fallback is None: - raise ValueError("This algorithm is not supported") - return self._fallback.decrypt(ciphertext, iv, key, algo) - - key_length = self.get_algo_key_length(algo) - if len(key) != key_length: - raise ValueError("Expected key to be {} bytes, got {} bytes".format(key_length, len(key))) - - return self._backend.decrypt(ciphertext, iv, key, algo) - - - def get_backend(self): - return self._backend.get_backend() diff --git a/src/lib/sslcrypto/_ecc.py b/src/lib/sslcrypto/_ecc.py deleted file mode 100644 index 9831d688..00000000 --- a/src/lib/sslcrypto/_ecc.py +++ /dev/null @@ -1,334 +0,0 @@ -import hashlib -import struct -import hmac -import base58 - - -try: - hashlib.new("ripemd160") -except ValueError: - # No native implementation - from . import _ripemd - def ripemd160(*args): - return _ripemd.new(*args) -else: - # Use OpenSSL - def ripemd160(*args): - return hashlib.new("ripemd160", *args) - - -class ECC: - CURVES = { - "secp112r1": 704, - "secp112r2": 705, - "secp128r1": 706, - "secp128r2": 707, - "secp160k1": 708, - "secp160r1": 709, - "secp160r2": 710, - "secp192k1": 711, - "prime192v1": 409, - "secp224k1": 712, - "secp224r1": 713, - "secp256k1": 714, - "prime256v1": 415, - "secp384r1": 715, - "secp521r1": 716 - } - - def __init__(self, backend, aes): - self._backend = backend - self._aes = aes - - - def get_curve(self, name): - if name not in self.CURVES: - raise ValueError("Unknown curve {}".format(name)) - nid = self.CURVES[name] - return EllipticCurve(self._backend(nid), self._aes, nid) - - - def get_backend(self): - return self._backend.get_backend() - - -class EllipticCurve: - def __init__(self, backend, aes, nid): - self._backend = backend - self._aes = aes - self.nid = nid - - - def _encode_public_key(self, x, y, is_compressed=True, raw=True): - if raw: - if is_compressed: - return bytes([0x02 + (y[-1] % 2)]) + x - else: - return bytes([0x04]) + x + y - else: - return struct.pack("!HH", self.nid, len(x)) + x + struct.pack("!H", len(y)) + y - - - def _decode_public_key(self, public_key, partial=False): - if not public_key: - raise ValueError("No public key") - - if public_key[0] == 0x04: - # Uncompressed - expected_length = 1 + 2 * self._backend.public_key_length - if partial: - if len(public_key) < expected_length: - raise ValueError("Invalid uncompressed public key length") - else: - if len(public_key) != expected_length: - raise ValueError("Invalid uncompressed public key length") - x = public_key[1:1 + self._backend.public_key_length] - y = public_key[1 + self._backend.public_key_length:expected_length] - if partial: - return (x, y), expected_length - else: - return x, y - elif public_key[0] in (0x02, 0x03): - # Compressed - expected_length = 1 + self._backend.public_key_length - if partial: - if len(public_key) < expected_length: - raise ValueError("Invalid compressed public key length") - else: - if len(public_key) != expected_length: - raise ValueError("Invalid compressed public key length") - - x, y = self._backend.decompress_point(public_key[:expected_length]) - # Sanity check - if x != public_key[1:expected_length]: - raise ValueError("Incorrect compressed public key") - if partial: - return (x, y), expected_length - else: - return x, y - else: - raise ValueError("Invalid public key prefix") - - - def _decode_public_key_openssl(self, public_key, partial=False): - if not public_key: - raise ValueError("No public key") - - i = 0 - - nid, = struct.unpack("!H", public_key[i:i + 2]) - i += 2 - if nid != self.nid: - raise ValueError("Wrong curve") - - xlen, = struct.unpack("!H", public_key[i:i + 2]) - i += 2 - if len(public_key) - i < xlen: - raise ValueError("Too short public key") - x = public_key[i:i + xlen] - i += xlen - - ylen, = struct.unpack("!H", public_key[i:i + 2]) - i += 2 - if len(public_key) - i < ylen: - raise ValueError("Too short public key") - y = public_key[i:i + ylen] - i += ylen - - if partial: - return (x, y), i - else: - if i < len(public_key): - raise ValueError("Too long public key") - return x, y - - - def new_private_key(self): - return self._backend.new_private_key() - - - def private_to_public(self, private_key, is_compressed=True): - x, y = self._backend.private_to_public(private_key) - return self._encode_public_key(x, y, is_compressed=is_compressed) - - - def private_to_wif(self, private_key): - return base58.b58encode_check(b"\x80" + private_key) - - - def wif_to_private(self, wif): - dec = base58.b58decode_check(wif) - if dec[0] != 0x80: - raise ValueError("Invalid network (expected mainnet)") - return dec[1:] - - - def public_to_address(self, public_key): - h = hashlib.sha256(public_key).digest() - hash160 = ripemd160(h).digest() - return base58.b58encode_check(b"\x00" + hash160) - - - def private_to_address(self, private_key, is_compressed=True): - # Kinda useless but left for quick migration from pybitcointools - return self.public_to_address(self.private_to_public(private_key, is_compressed=is_compressed)) - - - def derive(self, private_key, public_key): - if not isinstance(public_key, tuple): - public_key = self._decode_public_key(public_key) - return self._backend.ecdh(private_key, public_key) - - - def _digest(self, data, hash): - if hash is None: - return data - elif callable(hash): - return hash(data) - elif hash == "sha1": - return hashlib.sha1(data).digest() - elif hash == "sha256": - return hashlib.sha256(data).digest() - elif hash == "sha512": - return hashlib.sha512(data).digest() - else: - raise ValueError("Unknown hash/derivation method") - - - # High-level functions - def encrypt(self, data, public_key, algo="aes-256-cbc", derivation="sha256", mac="hmac-sha256", return_aes_key=False): - # Generate ephemeral private key - private_key = self.new_private_key() - - # Derive key - ecdh = self.derive(private_key, public_key) - key = self._digest(ecdh, derivation) - k_enc_len = self._aes.get_algo_key_length(algo) - if len(key) < k_enc_len: - raise ValueError("Too short digest") - k_enc, k_mac = key[:k_enc_len], key[k_enc_len:] - - # Encrypt - ciphertext, iv = self._aes.encrypt(data, k_enc, algo=algo) - ephem_public_key = self.private_to_public(private_key) - ephem_public_key = self._decode_public_key(ephem_public_key) - ephem_public_key = self._encode_public_key(*ephem_public_key, raw=False) - ciphertext = iv + ephem_public_key + ciphertext - - # Add MAC tag - if callable(mac): - tag = mac(k_mac, ciphertext) - elif mac == "hmac-sha256": - h = hmac.new(k_mac, digestmod="sha256") - h.update(ciphertext) - tag = h.digest() - elif mac == "hmac-sha512": - h = hmac.new(k_mac, digestmod="sha512") - h.update(ciphertext) - tag = h.digest() - elif mac is None: - tag = b"" - else: - raise ValueError("Unsupported MAC") - - if return_aes_key: - return ciphertext + tag, k_enc - else: - return ciphertext + tag - - - def decrypt(self, ciphertext, private_key, algo="aes-256-cbc", derivation="sha256", mac="hmac-sha256"): - # Get MAC tag - if callable(mac): - tag_length = mac.digest_size - elif mac == "hmac-sha256": - tag_length = hmac.new(b"", digestmod="sha256").digest_size - elif mac == "hmac-sha512": - tag_length = hmac.new(b"", digestmod="sha512").digest_size - elif mac is None: - tag_length = 0 - else: - raise ValueError("Unsupported MAC") - - if len(ciphertext) < tag_length: - raise ValueError("Ciphertext is too small to contain MAC tag") - if tag_length == 0: - tag = b"" - else: - ciphertext, tag = ciphertext[:-tag_length], ciphertext[-tag_length:] - - orig_ciphertext = ciphertext - - if len(ciphertext) < 16: - raise ValueError("Ciphertext is too small to contain IV") - iv, ciphertext = ciphertext[:16], ciphertext[16:] - - public_key, pos = self._decode_public_key_openssl(ciphertext, partial=True) - ciphertext = ciphertext[pos:] - - # Derive key - ecdh = self.derive(private_key, public_key) - key = self._digest(ecdh, derivation) - k_enc_len = self._aes.get_algo_key_length(algo) - if len(key) < k_enc_len: - raise ValueError("Too short digest") - k_enc, k_mac = key[:k_enc_len], key[k_enc_len:] - - # Verify MAC tag - if callable(mac): - expected_tag = mac(k_mac, orig_ciphertext) - elif mac == "hmac-sha256": - h = hmac.new(k_mac, digestmod="sha256") - h.update(orig_ciphertext) - expected_tag = h.digest() - elif mac == "hmac-sha512": - h = hmac.new(k_mac, digestmod="sha512") - h.update(orig_ciphertext) - expected_tag = h.digest() - elif mac is None: - expected_tag = b"" - - if not hmac.compare_digest(tag, expected_tag): - raise ValueError("Invalid MAC tag") - - return self._aes.decrypt(ciphertext, iv, k_enc, algo=algo) - - - def sign(self, data, private_key, hash="sha256", recoverable=False, is_compressed=True, entropy=None): - data = self._digest(data, hash) - if not entropy: - v = b"\x01" * len(data) - k = b"\x00" * len(data) - k = hmac.new(k, v + b"\x00" + private_key + data, "sha256").digest() - v = hmac.new(k, v, "sha256").digest() - k = hmac.new(k, v + b"\x01" + private_key + data, "sha256").digest() - v = hmac.new(k, v, "sha256").digest() - entropy = hmac.new(k, v, "sha256").digest() - return self._backend.sign(data, private_key, recoverable, is_compressed, entropy=entropy) - - - def recover(self, signature, data, hash="sha256"): - # Sanity check: is this signature recoverable? - if len(signature) != 1 + 2 * self._backend.public_key_length: - raise ValueError("Cannot recover an unrecoverable signature") - x, y = self._backend.recover(signature, self._digest(data, hash)) - is_compressed = signature[0] >= 31 - return self._encode_public_key(x, y, is_compressed=is_compressed) - - - def verify(self, signature, data, public_key, hash="sha256"): - if len(signature) == 1 + 2 * self._backend.public_key_length: - # Recoverable signature - signature = signature[1:] - if len(signature) != 2 * self._backend.public_key_length: - raise ValueError("Invalid signature format") - if not isinstance(public_key, tuple): - public_key = self._decode_public_key(public_key) - return self._backend.verify(signature, self._digest(data, hash), public_key) - - - def derive_child(self, seed, child): - # Based on BIP32 - if not 0 <= child < 2 ** 31: - raise ValueError("Invalid child index") - return self._backend.derive_child(seed, child) diff --git a/src/lib/sslcrypto/_ripemd.py b/src/lib/sslcrypto/_ripemd.py deleted file mode 100644 index 89377cc2..00000000 --- a/src/lib/sslcrypto/_ripemd.py +++ /dev/null @@ -1,375 +0,0 @@ -# Copyright (c) 2001 Markus Friedl. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -# pylint: skip-file - -import sys - -digest_size = 20 -digestsize = 20 - -class RIPEMD160: - """ - Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed. - """ - - def __init__(self, arg=None): - self.ctx = RMDContext() - if arg: - self.update(arg) - self.dig = None - - def update(self, arg): - RMD160Update(self.ctx, arg, len(arg)) - self.dig = None - - def digest(self): - if self.dig: - return self.dig - ctx = self.ctx.copy() - self.dig = RMD160Final(self.ctx) - self.ctx = ctx - return self.dig - - def hexdigest(self): - dig = self.digest() - hex_digest = "" - for d in dig: - hex_digest += "%02x" % d - return hex_digest - - def copy(self): - import copy - return copy.deepcopy(self) - - - -def new(arg=None): - """ - Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed. - """ - return RIPEMD160(arg) - - - -# -# Private. -# - -class RMDContext: - def __init__(self): - self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, - 0x10325476, 0xC3D2E1F0] # uint32 - self.count = 0 # uint64 - self.buffer = [0] * 64 # uchar - def copy(self): - ctx = RMDContext() - ctx.state = self.state[:] - ctx.count = self.count - ctx.buffer = self.buffer[:] - return ctx - -K0 = 0x00000000 -K1 = 0x5A827999 -K2 = 0x6ED9EBA1 -K3 = 0x8F1BBCDC -K4 = 0xA953FD4E - -KK0 = 0x50A28BE6 -KK1 = 0x5C4DD124 -KK2 = 0x6D703EF3 -KK3 = 0x7A6D76E9 -KK4 = 0x00000000 - -def ROL(n, x): - return ((x << n) & 0xffffffff) | (x >> (32 - n)) - -def F0(x, y, z): - return x ^ y ^ z - -def F1(x, y, z): - return (x & y) | (((~x) % 0x100000000) & z) - -def F2(x, y, z): - return (x | ((~y) % 0x100000000)) ^ z - -def F3(x, y, z): - return (x & z) | (((~z) % 0x100000000) & y) - -def F4(x, y, z): - return x ^ (y | ((~z) % 0x100000000)) - -def R(a, b, c, d, e, Fj, Kj, sj, rj, X): - a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e - c = ROL(10, c) - return a % 0x100000000, c - -PADDING = [0x80] + [0] * 63 - -import sys -import struct - -def RMD160Transform(state, block): # uint32 state[5], uchar block[64] - x = [0] * 16 - if sys.byteorder == "little": - x = struct.unpack("<16L", bytes(block[0:64])) - else: - raise ValueError("Big-endian platforms are not supported") - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - # Round 1 - a, c = R(a, b, c, d, e, F0, K0, 11, 0, x) - e, b = R(e, a, b, c, d, F0, K0, 14, 1, x) - d, a = R(d, e, a, b, c, F0, K0, 15, 2, x) - c, e = R(c, d, e, a, b, F0, K0, 12, 3, x) - b, d = R(b, c, d, e, a, F0, K0, 5, 4, x) - a, c = R(a, b, c, d, e, F0, K0, 8, 5, x) - e, b = R(e, a, b, c, d, F0, K0, 7, 6, x) - d, a = R(d, e, a, b, c, F0, K0, 9, 7, x) - c, e = R(c, d, e, a, b, F0, K0, 11, 8, x) - b, d = R(b, c, d, e, a, F0, K0, 13, 9, x) - a, c = R(a, b, c, d, e, F0, K0, 14, 10, x) - e, b = R(e, a, b, c, d, F0, K0, 15, 11, x) - d, a = R(d, e, a, b, c, F0, K0, 6, 12, x) - c, e = R(c, d, e, a, b, F0, K0, 7, 13, x) - b, d = R(b, c, d, e, a, F0, K0, 9, 14, x) - a, c = R(a, b, c, d, e, F0, K0, 8, 15, x) # #15 - # Round 2 - e, b = R(e, a, b, c, d, F1, K1, 7, 7, x) - d, a = R(d, e, a, b, c, F1, K1, 6, 4, x) - c, e = R(c, d, e, a, b, F1, K1, 8, 13, x) - b, d = R(b, c, d, e, a, F1, K1, 13, 1, x) - a, c = R(a, b, c, d, e, F1, K1, 11, 10, x) - e, b = R(e, a, b, c, d, F1, K1, 9, 6, x) - d, a = R(d, e, a, b, c, F1, K1, 7, 15, x) - c, e = R(c, d, e, a, b, F1, K1, 15, 3, x) - b, d = R(b, c, d, e, a, F1, K1, 7, 12, x) - a, c = R(a, b, c, d, e, F1, K1, 12, 0, x) - e, b = R(e, a, b, c, d, F1, K1, 15, 9, x) - d, a = R(d, e, a, b, c, F1, K1, 9, 5, x) - c, e = R(c, d, e, a, b, F1, K1, 11, 2, x) - b, d = R(b, c, d, e, a, F1, K1, 7, 14, x) - a, c = R(a, b, c, d, e, F1, K1, 13, 11, x) - e, b = R(e, a, b, c, d, F1, K1, 12, 8, x) # #31 - # Round 3 - d, a = R(d, e, a, b, c, F2, K2, 11, 3, x) - c, e = R(c, d, e, a, b, F2, K2, 13, 10, x) - b, d = R(b, c, d, e, a, F2, K2, 6, 14, x) - a, c = R(a, b, c, d, e, F2, K2, 7, 4, x) - e, b = R(e, a, b, c, d, F2, K2, 14, 9, x) - d, a = R(d, e, a, b, c, F2, K2, 9, 15, x) - c, e = R(c, d, e, a, b, F2, K2, 13, 8, x) - b, d = R(b, c, d, e, a, F2, K2, 15, 1, x) - a, c = R(a, b, c, d, e, F2, K2, 14, 2, x) - e, b = R(e, a, b, c, d, F2, K2, 8, 7, x) - d, a = R(d, e, a, b, c, F2, K2, 13, 0, x) - c, e = R(c, d, e, a, b, F2, K2, 6, 6, x) - b, d = R(b, c, d, e, a, F2, K2, 5, 13, x) - a, c = R(a, b, c, d, e, F2, K2, 12, 11, x) - e, b = R(e, a, b, c, d, F2, K2, 7, 5, x) - d, a = R(d, e, a, b, c, F2, K2, 5, 12, x) # #47 - # Round 4 - c, e = R(c, d, e, a, b, F3, K3, 11, 1, x) - b, d = R(b, c, d, e, a, F3, K3, 12, 9, x) - a, c = R(a, b, c, d, e, F3, K3, 14, 11, x) - e, b = R(e, a, b, c, d, F3, K3, 15, 10, x) - d, a = R(d, e, a, b, c, F3, K3, 14, 0, x) - c, e = R(c, d, e, a, b, F3, K3, 15, 8, x) - b, d = R(b, c, d, e, a, F3, K3, 9, 12, x) - a, c = R(a, b, c, d, e, F3, K3, 8, 4, x) - e, b = R(e, a, b, c, d, F3, K3, 9, 13, x) - d, a = R(d, e, a, b, c, F3, K3, 14, 3, x) - c, e = R(c, d, e, a, b, F3, K3, 5, 7, x) - b, d = R(b, c, d, e, a, F3, K3, 6, 15, x) - a, c = R(a, b, c, d, e, F3, K3, 8, 14, x) - e, b = R(e, a, b, c, d, F3, K3, 6, 5, x) - d, a = R(d, e, a, b, c, F3, K3, 5, 6, x) - c, e = R(c, d, e, a, b, F3, K3, 12, 2, x) # #63 - # Round 5 - b, d = R(b, c, d, e, a, F4, K4, 9, 4, x) - a, c = R(a, b, c, d, e, F4, K4, 15, 0, x) - e, b = R(e, a, b, c, d, F4, K4, 5, 5, x) - d, a = R(d, e, a, b, c, F4, K4, 11, 9, x) - c, e = R(c, d, e, a, b, F4, K4, 6, 7, x) - b, d = R(b, c, d, e, a, F4, K4, 8, 12, x) - a, c = R(a, b, c, d, e, F4, K4, 13, 2, x) - e, b = R(e, a, b, c, d, F4, K4, 12, 10, x) - d, a = R(d, e, a, b, c, F4, K4, 5, 14, x) - c, e = R(c, d, e, a, b, F4, K4, 12, 1, x) - b, d = R(b, c, d, e, a, F4, K4, 13, 3, x) - a, c = R(a, b, c, d, e, F4, K4, 14, 8, x) - e, b = R(e, a, b, c, d, F4, K4, 11, 11, x) - d, a = R(d, e, a, b, c, F4, K4, 8, 6, x) - c, e = R(c, d, e, a, b, F4, K4, 5, 15, x) - b, d = R(b, c, d, e, a, F4, K4, 6, 13, x) # #79 - - aa = a - bb = b - cc = c - dd = d - ee = e - - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - # Parallel round 1 - a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) - e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) - d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) - c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) - b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) - a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) - e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) - d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) - c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) - b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) - a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) - e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) - d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) - c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) - b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) - a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) # #15 - # Parallel round 2 - e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) - d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) - c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) - a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) - e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) - d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) - c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) - a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) - e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) - d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) - c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) - b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) - a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) - e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) # #31 - # Parallel round 3 - d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) - c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) - b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) - a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) - e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) - d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) - c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) - b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) - a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) - e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) - c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) - b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) - a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) - e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) # #47 - # Parallel round 4 - c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) - b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) - a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) - e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) - d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) - c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) - b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) - a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) - e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) - d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) - c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) - b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) - a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) - e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) - d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) - c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) # #63 - # Parallel round 5 - b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) - e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) - d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) - c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) - b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) - a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) - e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) - d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) - c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) - b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) - e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) - d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) - c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) - b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) # #79 - - t = (state[1] + cc + d) % 0x100000000 - state[1] = (state[2] + dd + e) % 0x100000000 - state[2] = (state[3] + ee + a) % 0x100000000 - state[3] = (state[4] + aa + b) % 0x100000000 - state[4] = (state[0] + bb + c) % 0x100000000 - state[0] = t % 0x100000000 - - -def RMD160Update(ctx, inp, inplen): - if type(inp) == str: - inp = [ord(i)&0xff for i in inp] - - have = int((ctx.count // 8) % 64) - inplen = int(inplen) - need = 64 - have - ctx.count += 8 * inplen - off = 0 - if inplen >= need: - if have: - for i in range(need): - ctx.buffer[have + i] = inp[i] - RMD160Transform(ctx.state, ctx.buffer) - off = need - have = 0 - while off + 64 <= inplen: - RMD160Transform(ctx.state, inp[off:]) #<--- - off += 64 - if off < inplen: - # memcpy(ctx->buffer + have, input+off, len-off) - for i in range(inplen - off): - ctx.buffer[have + i] = inp[off + i] - -def RMD160Final(ctx): - size = struct.pack("= self.n: - return self.jacobian_multiply(a, n % self.n, secret) - half = self.jacobian_multiply(a, n // 2, secret) - half_sq = self.jacobian_double(half) - if secret: - # A constant-time implementation - half_sq_a = self.jacobian_add(half_sq, a) - if n % 2 == 0: - result = half_sq - if n % 2 == 1: - result = half_sq_a - return result - else: - if n % 2 == 0: - return half_sq - return self.jacobian_add(half_sq, a) - - - def jacobian_shamir(self, a, n, b, m): - ab = self.jacobian_add(a, b) - if n < 0 or n >= self.n: - n %= self.n - if m < 0 or m >= self.n: - m %= self.n - res = 0, 0, 1 # point on infinity - for i in range(self.n_length - 1, -1, -1): - res = self.jacobian_double(res) - has_n = n & (1 << i) - has_m = m & (1 << i) - if has_n: - if has_m == 0: - res = self.jacobian_add(res, a) - if has_m != 0: - res = self.jacobian_add(res, ab) - else: - if has_m == 0: - res = self.jacobian_add(res, (0, 0, 1)) # Try not to leak - if has_m != 0: - res = self.jacobian_add(res, b) - return res - - - def fast_multiply(self, a, n, secret=False): - return self.from_jacobian(self.jacobian_multiply(self.to_jacobian(a), n, secret)) - - - def fast_add(self, a, b): - return self.from_jacobian(self.jacobian_add(self.to_jacobian(a), self.to_jacobian(b))) - - - def fast_shamir(self, a, n, b, m): - return self.from_jacobian(self.jacobian_shamir(self.to_jacobian(a), n, self.to_jacobian(b), m)) - - - def is_on_curve(self, a): - x, y = a - # Simple arithmetic check - if (pow(x, 3, self.p) + self.a * x + self.b) % self.p != y * y % self.p: - return False - # nP = point-at-infinity - return self.isinf(self.jacobian_multiply(self.to_jacobian(a), self.n)) diff --git a/src/lib/sslcrypto/fallback/_util.py b/src/lib/sslcrypto/fallback/_util.py deleted file mode 100644 index 2236ebee..00000000 --- a/src/lib/sslcrypto/fallback/_util.py +++ /dev/null @@ -1,79 +0,0 @@ -def int_to_bytes(raw, length): - data = [] - for _ in range(length): - data.append(raw % 256) - raw //= 256 - return bytes(data[::-1]) - - -def bytes_to_int(data): - raw = 0 - for byte in data: - raw = raw * 256 + byte - return raw - - -def legendre(a, p): - res = pow(a, (p - 1) // 2, p) - if res == p - 1: - return -1 - else: - return res - - -def inverse(a, n): - if a == 0: - return 0 - lm, hm = 1, 0 - low, high = a % n, n - while low > 1: - r = high // low - nm, new = hm - lm * r, high - low * r - lm, low, hm, high = nm, new, lm, low - return lm % n - - -def square_root_mod_prime(n, p): - if n == 0: - return 0 - if p == 2: - return n # We should never get here but it might be useful - if legendre(n, p) != 1: - raise ValueError("No square root") - # Optimizations - if p % 4 == 3: - return pow(n, (p + 1) // 4, p) - # 1. By factoring out powers of 2, find Q and S such that p - 1 = - # Q * 2 ** S with Q odd - q = p - 1 - s = 0 - while q % 2 == 0: - q //= 2 - s += 1 - # 2. Search for z in Z/pZ which is a quadratic non-residue - z = 1 - while legendre(z, p) != -1: - z += 1 - m, c, t, r = s, pow(z, q, p), pow(n, q, p), pow(n, (q + 1) // 2, p) - while True: - if t == 0: - return 0 - elif t == 1: - return r - # Use repeated squaring to find the least i, 0 < i < M, such - # that t ** (2 ** i) = 1 - t_sq = t - i = 0 - for i in range(1, m): - t_sq = t_sq * t_sq % p - if t_sq == 1: - break - else: - raise ValueError("Should never get here") - # Let b = c ** (2 ** (m - i - 1)) - b = pow(c, 2 ** (m - i - 1), p) - m = i - c = b * b % p - t = t * b * b % p - r = r * b % p - return r diff --git a/src/lib/sslcrypto/fallback/aes.py b/src/lib/sslcrypto/fallback/aes.py deleted file mode 100644 index e168bf34..00000000 --- a/src/lib/sslcrypto/fallback/aes.py +++ /dev/null @@ -1,101 +0,0 @@ -import os -import pyaes -from .._aes import AES - - -__all__ = ["aes"] - -class AESBackend: - def _get_algo_cipher_type(self, algo): - if not algo.startswith("aes-") or algo.count("-") != 2: - raise ValueError("Unknown cipher algorithm {}".format(algo)) - key_length, cipher_type = algo[4:].split("-") - if key_length not in ("128", "192", "256"): - raise ValueError("Unknown cipher algorithm {}".format(algo)) - if cipher_type not in ("cbc", "ctr", "cfb", "ofb"): - raise ValueError("Unknown cipher algorithm {}".format(algo)) - return cipher_type - - - def is_algo_supported(self, algo): - try: - self._get_algo_cipher_type(algo) - return True - except ValueError: - return False - - - def random(self, length): - return os.urandom(length) - - - def encrypt(self, data, key, algo="aes-256-cbc"): - cipher_type = self._get_algo_cipher_type(algo) - - # Generate random IV - iv = os.urandom(16) - - if cipher_type == "cbc": - cipher = pyaes.AESModeOfOperationCBC(key, iv=iv) - elif cipher_type == "ctr": - # The IV is actually a counter, not an IV but it does almost the - # same. Notice: pyaes always uses 1 as initial counter! Make sure - # not to call pyaes directly. - - # We kinda do two conversions here: from byte array to int here, and - # from int to byte array in pyaes internals. It's possible to fix that - # but I didn't notice any performance changes so I'm keeping clean code. - iv_int = 0 - for byte in iv: - iv_int = (iv_int * 256) + byte - counter = pyaes.Counter(iv_int) - cipher = pyaes.AESModeOfOperationCTR(key, counter=counter) - elif cipher_type == "cfb": - # Change segment size from default 8 bytes to 16 bytes for OpenSSL - # compatibility - cipher = pyaes.AESModeOfOperationCFB(key, iv, segment_size=16) - elif cipher_type == "ofb": - cipher = pyaes.AESModeOfOperationOFB(key, iv) - - encrypter = pyaes.Encrypter(cipher) - ciphertext = encrypter.feed(data) - ciphertext += encrypter.feed() - return ciphertext, iv - - - def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): - cipher_type = self._get_algo_cipher_type(algo) - - if cipher_type == "cbc": - cipher = pyaes.AESModeOfOperationCBC(key, iv=iv) - elif cipher_type == "ctr": - # The IV is actually a counter, not an IV but it does almost the - # same. Notice: pyaes always uses 1 as initial counter! Make sure - # not to call pyaes directly. - - # We kinda do two conversions here: from byte array to int here, and - # from int to byte array in pyaes internals. It's possible to fix that - # but I didn't notice any performance changes so I'm keeping clean code. - iv_int = 0 - for byte in iv: - iv_int = (iv_int * 256) + byte - counter = pyaes.Counter(iv_int) - cipher = pyaes.AESModeOfOperationCTR(key, counter=counter) - elif cipher_type == "cfb": - # Change segment size from default 8 bytes to 16 bytes for OpenSSL - # compatibility - cipher = pyaes.AESModeOfOperationCFB(key, iv, segment_size=16) - elif cipher_type == "ofb": - cipher = pyaes.AESModeOfOperationOFB(key, iv) - - decrypter = pyaes.Decrypter(cipher) - data = decrypter.feed(ciphertext) - data += decrypter.feed() - return data - - - def get_backend(self): - return "fallback" - - -aes = AES(AESBackend()) diff --git a/src/lib/sslcrypto/fallback/ecc.py b/src/lib/sslcrypto/fallback/ecc.py deleted file mode 100644 index 3a438d4d..00000000 --- a/src/lib/sslcrypto/fallback/ecc.py +++ /dev/null @@ -1,371 +0,0 @@ -import hmac -import os -from ._jacobian import JacobianCurve -from .._ecc import ECC -from .aes import aes -from ._util import int_to_bytes, bytes_to_int, inverse, square_root_mod_prime - - -# pylint: disable=line-too-long -CURVES = { - # nid: (p, n, a, b, (Gx, Gy)), - 704: ( - # secp112r1 - 0xDB7C2ABF62E35E668076BEAD208B, - 0xDB7C2ABF62E35E7628DFAC6561C5, - 0xDB7C2ABF62E35E668076BEAD2088, - 0x659EF8BA043916EEDE8911702B22, - ( - 0x09487239995A5EE76B55F9C2F098, - 0xA89CE5AF8724C0A23E0E0FF77500 - ) - ), - 705: ( - # secp112r2 - 0xDB7C2ABF62E35E668076BEAD208B, - 0x36DF0AAFD8B8D7597CA10520D04B, - 0x6127C24C05F38A0AAAF65C0EF02C, - 0x51DEF1815DB5ED74FCC34C85D709, - ( - 0x4BA30AB5E892B4E1649DD0928643, - 0xADCD46F5882E3747DEF36E956E97 - ) - ), - 706: ( - # secp128r1 - 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, - 0xFFFFFFFE0000000075A30D1B9038A115, - 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC, - 0xE87579C11079F43DD824993C2CEE5ED3, - ( - 0x161FF7528B899B2D0C28607CA52C5B86, - 0xCF5AC8395BAFEB13C02DA292DDED7A83 - ) - ), - 707: ( - # secp128r2 - 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, - 0x3FFFFFFF7FFFFFFFBE0024720613B5A3, - 0xD6031998D1B3BBFEBF59CC9BBFF9AEE1, - 0x5EEEFCA380D02919DC2C6558BB6D8A5D, - ( - 0x7B6AA5D85E572983E6FB32A7CDEBC140, - 0x27B6916A894D3AEE7106FE805FC34B44 - ) - ), - 708: ( - # secp160k1 - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, - 0x0100000000000000000001B8FA16DFAB9ACA16B6B3, - 0, - 7, - ( - 0x3B4C382CE37AA192A4019E763036F4F5DD4D7EBB, - 0x938CF935318FDCED6BC28286531733C3F03C4FEE - ) - ), - 709: ( - # secp160r1 - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF, - 0x0100000000000000000001F4C8F927AED3CA752257, - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC, - 0x001C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45, - ( - 0x4A96B5688EF573284664698968C38BB913CBFC82, - 0x23A628553168947D59DCC912042351377AC5FB32 - ) - ), - 710: ( - # secp160r2 - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, - 0x0100000000000000000000351EE786A818F3A1A16B, - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC70, - 0x00B4E134D3FB59EB8BAB57274904664D5AF50388BA, - ( - 0x52DCB034293A117E1F4FF11B30F7199D3144CE6D, - 0xFEAFFEF2E331F296E071FA0DF9982CFEA7D43F2E - ) - ), - 711: ( - # secp192k1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37, - 0xFFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D, - 0, - 3, - ( - 0xDB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D, - 0x9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D - ) - ), - 409: ( - # prime192v1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF, - 0xFFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC, - 0x64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1, - ( - 0x188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012, - 0x07192B95FFC8DA78631011ED6B24CDD573F977A11E794811 - ) - ), - 712: ( - # secp224k1 - 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFE56D, - 0x010000000000000000000000000001DCE8D2EC6184CAF0A971769FB1F7, - 0, - 5, - ( - 0xA1455B334DF099DF30FC28A169A467E9E47075A90F7E650EB6B7A45C, - 0x7E089FED7FBA344282CAFBD6F7E319F7C0B0BD59E2CA4BDB556D61A5 - ) - ), - 713: ( - # secp224r1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE, - 0xB4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4, - ( - 0xB70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21, - 0xBD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34 - ) - ), - 714: ( - # secp256k1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, - 0, - 7, - ( - 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, - 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 - ) - ), - 415: ( - # prime256v1 - 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, - 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, - 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, - 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B, - ( - 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, - 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5 - ) - ), - 715: ( - # secp384r1 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973, - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC, - 0xB3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF, - ( - 0xAA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7, - 0x3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F - ) - ), - 716: ( - # secp521r1 - 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, - 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409, - 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC, - 0x0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00, - ( - 0x00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66, - 0x011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650 - ) - ) -} -# pylint: enable=line-too-long - - -class EllipticCurveBackend: - def __init__(self, nid): - self.p, self.n, self.a, self.b, self.g = CURVES[nid] - self.jacobian = JacobianCurve(*CURVES[nid]) - - self.public_key_length = (len(bin(self.p).replace("0b", "")) + 7) // 8 - self.order_bitlength = len(bin(self.n).replace("0b", "")) - - - def _int_to_bytes(self, raw, len=None): - return int_to_bytes(raw, len or self.public_key_length) - - - def decompress_point(self, public_key): - # Parse & load data - x = bytes_to_int(public_key[1:]) - # Calculate Y - y_square = (pow(x, 3, self.p) + self.a * x + self.b) % self.p - try: - y = square_root_mod_prime(y_square, self.p) - except Exception: - raise ValueError("Invalid public key") from None - if y % 2 != public_key[0] - 0x02: - y = self.p - y - return self._int_to_bytes(x), self._int_to_bytes(y) - - - def new_private_key(self): - while True: - private_key = os.urandom(self.public_key_length) - if bytes_to_int(private_key) >= self.n: - continue - return private_key - - - def private_to_public(self, private_key): - raw = bytes_to_int(private_key) - x, y = self.jacobian.fast_multiply(self.g, raw) - return self._int_to_bytes(x), self._int_to_bytes(y) - - - def ecdh(self, private_key, public_key): - x, y = public_key - x, y = bytes_to_int(x), bytes_to_int(y) - private_key = bytes_to_int(private_key) - x, _ = self.jacobian.fast_multiply((x, y), private_key, secret=True) - return self._int_to_bytes(x) - - - def _subject_to_int(self, subject): - return bytes_to_int(subject[:(self.order_bitlength + 7) // 8]) - - - def sign(self, subject, raw_private_key, recoverable, is_compressed, entropy): - z = self._subject_to_int(subject) - private_key = bytes_to_int(raw_private_key) - k = bytes_to_int(entropy) - - # Fix k length to prevent Minerva. Increasing multiplier by a - # multiple of order doesn't break anything. This fix was ported - # from python-ecdsa - ks = k + self.n - kt = ks + self.n - ks_len = len(bin(ks).replace("0b", "")) // 8 - kt_len = len(bin(kt).replace("0b", "")) // 8 - if ks_len == kt_len: - k = kt - else: - k = ks - px, py = self.jacobian.fast_multiply(self.g, k, secret=True) - - r = px % self.n - if r == 0: - # Invalid k - raise ValueError("Invalid k") - - s = (inverse(k, self.n) * (z + (private_key * r))) % self.n - if s == 0: - # Invalid k - raise ValueError("Invalid k") - - inverted = False - if s * 2 >= self.n: - s = self.n - s - inverted = True - rs_buf = self._int_to_bytes(r) + self._int_to_bytes(s) - - if recoverable: - recid = (py % 2) ^ inverted - recid += 2 * int(px // self.n) - if is_compressed: - return bytes([31 + recid]) + rs_buf - else: - if recid >= 4: - raise ValueError("Too big recovery ID, use compressed address instead") - return bytes([27 + recid]) + rs_buf - else: - return rs_buf - - - def recover(self, signature, subject): - z = self._subject_to_int(subject) - - recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 - r = bytes_to_int(signature[1:self.public_key_length + 1]) - s = bytes_to_int(signature[self.public_key_length + 1:]) - - # Verify bounds - if not 0 <= recid < 2 * (self.p // self.n + 1): - raise ValueError("Invalid recovery ID") - if r >= self.n: - raise ValueError("r is out of bounds") - if s >= self.n: - raise ValueError("s is out of bounds") - - rinv = inverse(r, self.n) - u1 = (-z * rinv) % self.n - u2 = (s * rinv) % self.n - - # Recover R - rx = r + (recid // 2) * self.n - if rx >= self.p: - raise ValueError("Rx is out of bounds") - - # Almost copied from decompress_point - ry_square = (pow(rx, 3, self.p) + self.a * rx + self.b) % self.p - try: - ry = square_root_mod_prime(ry_square, self.p) - except Exception: - raise ValueError("Invalid recovered public key") from None - - # Ensure the point is correct - if ry % 2 != recid % 2: - # Fix Ry sign - ry = self.p - ry - - x, y = self.jacobian.fast_shamir(self.g, u1, (rx, ry), u2) - return self._int_to_bytes(x), self._int_to_bytes(y) - - - def verify(self, signature, subject, public_key): - z = self._subject_to_int(subject) - - r = bytes_to_int(signature[:self.public_key_length]) - s = bytes_to_int(signature[self.public_key_length:]) - - # Verify bounds - if r >= self.n: - raise ValueError("r is out of bounds") - if s >= self.n: - raise ValueError("s is out of bounds") - - public_key = [bytes_to_int(c) for c in public_key] - - # Ensure that the public key is correct - if not self.jacobian.is_on_curve(public_key): - raise ValueError("Public key is not on curve") - - sinv = inverse(s, self.n) - u1 = (z * sinv) % self.n - u2 = (r * sinv) % self.n - - x1, _ = self.jacobian.fast_shamir(self.g, u1, public_key, u2) - if r != x1 % self.n: - raise ValueError("Invalid signature") - - return True - - - def derive_child(self, seed, child): - # Round 1 - h = hmac.new(key=b"Bitcoin seed", msg=seed, digestmod="sha512").digest() - private_key1 = h[:32] - x, y = self.private_to_public(private_key1) - public_key1 = bytes([0x02 + (y[-1] % 2)]) + x - private_key1 = bytes_to_int(private_key1) - - # Round 2 - msg = public_key1 + self._int_to_bytes(child, 4) - h = hmac.new(key=h[32:], msg=msg, digestmod="sha512").digest() - private_key2 = bytes_to_int(h[:32]) - - return self._int_to_bytes((private_key1 + private_key2) % self.n) - - - @classmethod - def get_backend(cls): - return "fallback" - - -ecc = ECC(EllipticCurveBackend, aes) diff --git a/src/lib/sslcrypto/fallback/rsa.py b/src/lib/sslcrypto/fallback/rsa.py deleted file mode 100644 index 54b8d2cb..00000000 --- a/src/lib/sslcrypto/fallback/rsa.py +++ /dev/null @@ -1,8 +0,0 @@ -# pylint: disable=too-few-public-methods - -class RSA: - def get_backend(self): - return "fallback" - - -rsa = RSA() diff --git a/src/lib/sslcrypto/openssl/__init__.py b/src/lib/sslcrypto/openssl/__init__.py deleted file mode 100644 index a32ae692..00000000 --- a/src/lib/sslcrypto/openssl/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .aes import aes -from .ecc import ecc -from .rsa import rsa diff --git a/src/lib/sslcrypto/openssl/aes.py b/src/lib/sslcrypto/openssl/aes.py deleted file mode 100644 index c58451d5..00000000 --- a/src/lib/sslcrypto/openssl/aes.py +++ /dev/null @@ -1,156 +0,0 @@ -import ctypes -import threading -from .._aes import AES -from ..fallback.aes import aes as fallback_aes -from .library import lib, openssl_backend - - -# Initialize functions -try: - lib.EVP_CIPHER_CTX_new.restype = ctypes.POINTER(ctypes.c_char) -except AttributeError: - pass -lib.EVP_get_cipherbyname.restype = ctypes.POINTER(ctypes.c_char) - - -thread_local = threading.local() - - -class Context: - def __init__(self, ptr, do_free): - self.lib = lib - self.ptr = ptr - self.do_free = do_free - - - def __del__(self): - if self.do_free: - self.lib.EVP_CIPHER_CTX_free(self.ptr) - - -class AESBackend: - ALGOS = ( - "aes-128-cbc", "aes-192-cbc", "aes-256-cbc", - "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", - "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", - "aes-128-ofb", "aes-192-ofb", "aes-256-ofb" - ) - - def __init__(self): - self.is_supported_ctx_new = hasattr(lib, "EVP_CIPHER_CTX_new") - self.is_supported_ctx_reset = hasattr(lib, "EVP_CIPHER_CTX_reset") - - - def _get_ctx(self): - if not hasattr(thread_local, "ctx"): - if self.is_supported_ctx_new: - thread_local.ctx = Context(lib.EVP_CIPHER_CTX_new(), True) - else: - # 1 KiB ought to be enough for everybody. We don't know the real - # size of the context buffer because we are unsure about padding and - # pointer size - thread_local.ctx = Context(ctypes.create_string_buffer(1024), False) - return thread_local.ctx.ptr - - - def get_backend(self): - return openssl_backend - - - def _get_cipher(self, algo): - if algo not in self.ALGOS: - raise ValueError("Unknown cipher algorithm {}".format(algo)) - cipher = lib.EVP_get_cipherbyname(algo.encode()) - if not cipher: - raise ValueError("Unknown cipher algorithm {}".format(algo)) - return cipher - - - def is_algo_supported(self, algo): - try: - self._get_cipher(algo) - return True - except ValueError: - return False - - - def random(self, length): - entropy = ctypes.create_string_buffer(length) - lib.RAND_bytes(entropy, length) - return bytes(entropy) - - - def encrypt(self, data, key, algo="aes-256-cbc"): - # Initialize context - ctx = self._get_ctx() - if not self.is_supported_ctx_new: - lib.EVP_CIPHER_CTX_init(ctx) - try: - lib.EVP_EncryptInit_ex(ctx, self._get_cipher(algo), None, None, None) - - # Generate random IV - iv_length = 16 - iv = self.random(iv_length) - - # Set key and IV - lib.EVP_EncryptInit_ex(ctx, None, None, key, iv) - - # Actually encrypt - block_size = 16 - output = ctypes.create_string_buffer((len(data) // block_size + 1) * block_size) - output_len = ctypes.c_int() - - if not lib.EVP_CipherUpdate(ctx, output, ctypes.byref(output_len), data, len(data)): - raise ValueError("Could not feed cipher with data") - - new_output = ctypes.byref(output, output_len.value) - output_len2 = ctypes.c_int() - if not lib.EVP_CipherFinal_ex(ctx, new_output, ctypes.byref(output_len2)): - raise ValueError("Could not finalize cipher") - - ciphertext = output[:output_len.value + output_len2.value] - return ciphertext, iv - finally: - if self.is_supported_ctx_reset: - lib.EVP_CIPHER_CTX_reset(ctx) - else: - lib.EVP_CIPHER_CTX_cleanup(ctx) - - - def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): - # Initialize context - ctx = self._get_ctx() - if not self.is_supported_ctx_new: - lib.EVP_CIPHER_CTX_init(ctx) - try: - lib.EVP_DecryptInit_ex(ctx, self._get_cipher(algo), None, None, None) - - # Make sure IV length is correct - iv_length = 16 - if len(iv) != iv_length: - raise ValueError("Expected IV to be {} bytes, got {} bytes".format(iv_length, len(iv))) - - # Set key and IV - lib.EVP_DecryptInit_ex(ctx, None, None, key, iv) - - # Actually decrypt - output = ctypes.create_string_buffer(len(ciphertext)) - output_len = ctypes.c_int() - - if not lib.EVP_DecryptUpdate(ctx, output, ctypes.byref(output_len), ciphertext, len(ciphertext)): - raise ValueError("Could not feed decipher with ciphertext") - - new_output = ctypes.byref(output, output_len.value) - output_len2 = ctypes.c_int() - if not lib.EVP_DecryptFinal_ex(ctx, new_output, ctypes.byref(output_len2)): - raise ValueError("Could not finalize decipher") - - return output[:output_len.value + output_len2.value] - finally: - if self.is_supported_ctx_reset: - lib.EVP_CIPHER_CTX_reset(ctx) - else: - lib.EVP_CIPHER_CTX_cleanup(ctx) - - -aes = AES(AESBackend(), fallback_aes) diff --git a/src/lib/sslcrypto/openssl/ecc.py b/src/lib/sslcrypto/openssl/ecc.py deleted file mode 100644 index 4284646b..00000000 --- a/src/lib/sslcrypto/openssl/ecc.py +++ /dev/null @@ -1,551 +0,0 @@ -import ctypes -import hmac -import threading -from .._ecc import ECC -from .aes import aes -from .library import lib, openssl_backend - - -# Initialize functions -lib.BN_new.restype = ctypes.POINTER(ctypes.c_char) -lib.BN_bin2bn.restype = ctypes.POINTER(ctypes.c_char) -lib.BN_CTX_new.restype = ctypes.POINTER(ctypes.c_char) -lib.EC_GROUP_new_by_curve_name.restype = ctypes.POINTER(ctypes.c_char) -lib.EC_KEY_new_by_curve_name.restype = ctypes.POINTER(ctypes.c_char) -lib.EC_POINT_new.restype = ctypes.POINTER(ctypes.c_char) -lib.EC_KEY_get0_private_key.restype = ctypes.POINTER(ctypes.c_char) -lib.EVP_PKEY_new.restype = ctypes.POINTER(ctypes.c_char) -try: - lib.EVP_PKEY_CTX_new.restype = ctypes.POINTER(ctypes.c_char) -except AttributeError: - pass - - -thread_local = threading.local() - - -class BN: - # BN_CTX - class Context: - def __init__(self): - self.ptr = lib.BN_CTX_new() - self.lib = lib # For finalizer - - - def __del__(self): - self.lib.BN_CTX_free(self.ptr) - - - @classmethod - def get(cls): - # Get thread-safe contexf - if not hasattr(thread_local, "bn_ctx"): - thread_local.bn_ctx = cls() - return thread_local.bn_ctx.ptr - - - def __init__(self, value=None, link_only=False): - if link_only: - self.bn = value - self._free = False - else: - if value is None: - self.bn = lib.BN_new() - self._free = True - elif isinstance(value, bytes): - self.bn = lib.BN_bin2bn(value, len(value), None) - self._free = True - else: - self.bn = lib.BN_new() - lib.BN_clear(self.bn) - lib.BN_add_word(self.bn, value) - self._free = True - - - def __del__(self): - if self._free: - lib.BN_free(self.bn) - - - def bytes(self, length=None): - buf = ctypes.create_string_buffer((len(self) + 7) // 8) - lib.BN_bn2bin(self.bn, buf) - buf = bytes(buf) - if length is None: - return buf - else: - if length < len(buf): - raise ValueError("Too little space for BN") - return b"\x00" * (length - len(buf)) + buf - - def __int__(self): - value = 0 - for byte in self.bytes(): - value = value * 256 + byte - return value - - def __len__(self): - return lib.BN_num_bits(self.bn) - - - def inverse(self, modulo): - result = BN() - if not lib.BN_mod_inverse(result.bn, self.bn, modulo.bn, BN.Context.get()): - raise ValueError("Could not compute inverse") - return result - - - def __floordiv__(self, other): - if not isinstance(other, BN): - raise TypeError("Can only divide BN by BN, not {}".format(other)) - result = BN() - if not lib.BN_div(result.bn, None, self.bn, other.bn, BN.Context.get()): - raise ZeroDivisionError("Division by zero") - return result - - def __mod__(self, other): - if not isinstance(other, BN): - raise TypeError("Can only divide BN by BN, not {}".format(other)) - result = BN() - if not lib.BN_div(None, result.bn, self.bn, other.bn, BN.Context.get()): - raise ZeroDivisionError("Division by zero") - return result - - def __add__(self, other): - if not isinstance(other, BN): - raise TypeError("Can only sum BN's, not BN and {}".format(other)) - result = BN() - if not lib.BN_add(result.bn, self.bn, other.bn): - raise ValueError("Could not sum two BN's") - return result - - def __sub__(self, other): - if not isinstance(other, BN): - raise TypeError("Can only subtract BN's, not BN and {}".format(other)) - result = BN() - if not lib.BN_sub(result.bn, self.bn, other.bn): - raise ValueError("Could not subtract BN from BN") - return result - - def __mul__(self, other): - if not isinstance(other, BN): - raise TypeError("Can only multiply BN by BN, not {}".format(other)) - result = BN() - if not lib.BN_mul(result.bn, self.bn, other.bn, BN.Context.get()): - raise ValueError("Could not multiply two BN's") - return result - - def __neg__(self): - return BN(0) - self - - - # A dirty but nice way to update current BN and free old BN at the same time - def __imod__(self, other): - res = self % other - self.bn, res.bn = res.bn, self.bn - return self - def __iadd__(self, other): - res = self + other - self.bn, res.bn = res.bn, self.bn - return self - def __isub__(self, other): - res = self - other - self.bn, res.bn = res.bn, self.bn - return self - def __imul__(self, other): - res = self * other - self.bn, res.bn = res.bn, self.bn - return self - - - def cmp(self, other): - if not isinstance(other, BN): - raise TypeError("Can only compare BN with BN, not {}".format(other)) - return lib.BN_cmp(self.bn, other.bn) - - def __eq__(self, other): - return self.cmp(other) == 0 - def __lt__(self, other): - return self.cmp(other) < 0 - def __gt__(self, other): - return self.cmp(other) > 0 - def __ne__(self, other): - return self.cmp(other) != 0 - def __le__(self, other): - return self.cmp(other) <= 0 - def __ge__(self, other): - return self.cmp(other) >= 0 - - - def __repr__(self): - return "".format(int(self)) - - def __str__(self): - return str(int(self)) - - -class EllipticCurveBackend: - def __init__(self, nid): - self.lib = lib # For finalizer - self.nid = nid - self.group = lib.EC_GROUP_new_by_curve_name(self.nid) - if not self.group: - raise ValueError("The curve is not supported by OpenSSL") - - self.order = BN() - self.p = BN() - bn_ctx = BN.Context.get() - lib.EC_GROUP_get_order(self.group, self.order.bn, bn_ctx) - lib.EC_GROUP_get_curve_GFp(self.group, self.p.bn, None, None, bn_ctx) - - self.public_key_length = (len(self.p) + 7) // 8 - - self.is_supported_evp_pkey_ctx = hasattr(lib, "EVP_PKEY_CTX_new") - - - def __del__(self): - self.lib.EC_GROUP_free(self.group) - - - def _private_key_to_ec_key(self, private_key): - eckey = lib.EC_KEY_new_by_curve_name(self.nid) - if not eckey: - raise ValueError("Failed to allocate EC_KEY") - private_key = BN(private_key) - if not lib.EC_KEY_set_private_key(eckey, private_key.bn): - lib.EC_KEY_free(eckey) - raise ValueError("Invalid private key") - return eckey, private_key - - - def _public_key_to_point(self, public_key): - x = BN(public_key[0]) - y = BN(public_key[1]) - # EC_KEY_set_public_key_affine_coordinates is not supported by - # OpenSSL 1.0.0 so we can't use it - point = lib.EC_POINT_new(self.group) - if not lib.EC_POINT_set_affine_coordinates_GFp(self.group, point, x.bn, y.bn, BN.Context.get()): - raise ValueError("Could not set public key affine coordinates") - return point - - - def _public_key_to_ec_key(self, public_key): - eckey = lib.EC_KEY_new_by_curve_name(self.nid) - if not eckey: - raise ValueError("Failed to allocate EC_KEY") - try: - # EC_KEY_set_public_key_affine_coordinates is not supported by - # OpenSSL 1.0.0 so we can't use it - point = self._public_key_to_point(public_key) - if not lib.EC_KEY_set_public_key(eckey, point): - raise ValueError("Could not set point") - lib.EC_POINT_free(point) - return eckey - except Exception as e: - lib.EC_KEY_free(eckey) - raise e from None - - - def _point_to_affine(self, point): - # Convert to affine coordinates - x = BN() - y = BN() - if lib.EC_POINT_get_affine_coordinates_GFp(self.group, point, x.bn, y.bn, BN.Context.get()) != 1: - raise ValueError("Failed to convert public key to affine coordinates") - # Convert to binary - if (len(x) + 7) // 8 > self.public_key_length: - raise ValueError("Public key X coordinate is too large") - if (len(y) + 7) // 8 > self.public_key_length: - raise ValueError("Public key Y coordinate is too large") - return x.bytes(self.public_key_length), y.bytes(self.public_key_length) - - - def decompress_point(self, public_key): - point = lib.EC_POINT_new(self.group) - if not point: - raise ValueError("Could not create point") - try: - if not lib.EC_POINT_oct2point(self.group, point, public_key, len(public_key), BN.Context.get()): - raise ValueError("Invalid compressed public key") - return self._point_to_affine(point) - finally: - lib.EC_POINT_free(point) - - - def new_private_key(self): - # Create random key - eckey = lib.EC_KEY_new_by_curve_name(self.nid) - lib.EC_KEY_generate_key(eckey) - # To big integer - private_key = BN(lib.EC_KEY_get0_private_key(eckey), link_only=True) - # To binary - private_key_buf = private_key.bytes() - # Cleanup - lib.EC_KEY_free(eckey) - return private_key_buf - - - def private_to_public(self, private_key): - eckey, private_key = self._private_key_to_ec_key(private_key) - try: - # Derive public key - point = lib.EC_POINT_new(self.group) - try: - if not lib.EC_POINT_mul(self.group, point, private_key.bn, None, None, BN.Context.get()): - raise ValueError("Failed to derive public key") - return self._point_to_affine(point) - finally: - lib.EC_POINT_free(point) - finally: - lib.EC_KEY_free(eckey) - - - def ecdh(self, private_key, public_key): - if not self.is_supported_evp_pkey_ctx: - # Use ECDH_compute_key instead - # Create EC_KEY from private key - eckey, _ = self._private_key_to_ec_key(private_key) - try: - # Create EC_POINT from public key - point = self._public_key_to_point(public_key) - try: - key = ctypes.create_string_buffer(self.public_key_length) - if lib.ECDH_compute_key(key, self.public_key_length, point, eckey, None) == -1: - raise ValueError("Could not compute shared secret") - return bytes(key) - finally: - lib.EC_POINT_free(point) - finally: - lib.EC_KEY_free(eckey) - - # Private key: - # Create EC_KEY - eckey, _ = self._private_key_to_ec_key(private_key) - try: - # Convert to EVP_PKEY - pkey = lib.EVP_PKEY_new() - if not pkey: - raise ValueError("Could not create private key object") - try: - lib.EVP_PKEY_set1_EC_KEY(pkey, eckey) - - # Public key: - # Create EC_KEY - peer_eckey = self._public_key_to_ec_key(public_key) - try: - # Convert to EVP_PKEY - peer_pkey = lib.EVP_PKEY_new() - if not peer_pkey: - raise ValueError("Could not create public key object") - try: - lib.EVP_PKEY_set1_EC_KEY(peer_pkey, peer_eckey) - - # Create context - ctx = lib.EVP_PKEY_CTX_new(pkey, None) - if not ctx: - raise ValueError("Could not create EVP context") - try: - if lib.EVP_PKEY_derive_init(ctx) != 1: - raise ValueError("Could not initialize key derivation") - if not lib.EVP_PKEY_derive_set_peer(ctx, peer_pkey): - raise ValueError("Could not set peer") - - # Actually derive - key_len = ctypes.c_int(0) - lib.EVP_PKEY_derive(ctx, None, ctypes.byref(key_len)) - key = ctypes.create_string_buffer(key_len.value) - lib.EVP_PKEY_derive(ctx, key, ctypes.byref(key_len)) - - return bytes(key) - finally: - lib.EVP_PKEY_CTX_free(ctx) - finally: - lib.EVP_PKEY_free(peer_pkey) - finally: - lib.EC_KEY_free(peer_eckey) - finally: - lib.EVP_PKEY_free(pkey) - finally: - lib.EC_KEY_free(eckey) - - - def _subject_to_bn(self, subject): - return BN(subject[:(len(self.order) + 7) // 8]) - - - def sign(self, subject, private_key, recoverable, is_compressed, entropy): - z = self._subject_to_bn(subject) - private_key = BN(private_key) - k = BN(entropy) - - rp = lib.EC_POINT_new(self.group) - bn_ctx = BN.Context.get() - try: - # Fix Minerva - k1 = k + self.order - k2 = k1 + self.order - if len(k1) == len(k2): - k = k2 - else: - k = k1 - if not lib.EC_POINT_mul(self.group, rp, k.bn, None, None, bn_ctx): - raise ValueError("Could not generate R") - # Convert to affine coordinates - rx = BN() - ry = BN() - if lib.EC_POINT_get_affine_coordinates_GFp(self.group, rp, rx.bn, ry.bn, bn_ctx) != 1: - raise ValueError("Failed to convert R to affine coordinates") - r = rx % self.order - if r == BN(0): - raise ValueError("Invalid k") - # Calculate s = k^-1 * (z + r * private_key) mod n - s = (k.inverse(self.order) * (z + r * private_key)) % self.order - if s == BN(0): - raise ValueError("Invalid k") - - inverted = False - if s * BN(2) >= self.order: - s = self.order - s - inverted = True - - r_buf = r.bytes(self.public_key_length) - s_buf = s.bytes(self.public_key_length) - if recoverable: - # Generate recid - recid = int(ry % BN(2)) ^ inverted - # The line below is highly unlikely to matter in case of - # secp256k1 but might make sense for other curves - recid += 2 * int(rx // self.order) - if is_compressed: - return bytes([31 + recid]) + r_buf + s_buf - else: - if recid >= 4: - raise ValueError("Too big recovery ID, use compressed address instead") - return bytes([27 + recid]) + r_buf + s_buf - else: - return r_buf + s_buf - finally: - lib.EC_POINT_free(rp) - - - def recover(self, signature, subject): - recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 - r = BN(signature[1:self.public_key_length + 1]) - s = BN(signature[self.public_key_length + 1:]) - - # Verify bounds - if r >= self.order: - raise ValueError("r is out of bounds") - if s >= self.order: - raise ValueError("s is out of bounds") - - bn_ctx = BN.Context.get() - - z = self._subject_to_bn(subject) - - rinv = r.inverse(self.order) - u1 = (-z * rinv) % self.order - u2 = (s * rinv) % self.order - - # Recover R - rx = r + BN(recid // 2) * self.order - if rx >= self.p: - raise ValueError("Rx is out of bounds") - rp = lib.EC_POINT_new(self.group) - if not rp: - raise ValueError("Could not create R") - try: - init_buf = b"\x02" + rx.bytes(self.public_key_length) - if not lib.EC_POINT_oct2point(self.group, rp, init_buf, len(init_buf), bn_ctx): - raise ValueError("Could not use Rx to initialize point") - ry = BN() - if lib.EC_POINT_get_affine_coordinates_GFp(self.group, rp, None, ry.bn, bn_ctx) != 1: - raise ValueError("Failed to convert R to affine coordinates") - if int(ry % BN(2)) != recid % 2: - # Fix Ry sign - ry = self.p - ry - if lib.EC_POINT_set_affine_coordinates_GFp(self.group, rp, rx.bn, ry.bn, bn_ctx) != 1: - raise ValueError("Failed to update R coordinates") - - # Recover public key - result = lib.EC_POINT_new(self.group) - if not result: - raise ValueError("Could not create point") - try: - if not lib.EC_POINT_mul(self.group, result, u1.bn, rp, u2.bn, bn_ctx): - raise ValueError("Could not recover public key") - return self._point_to_affine(result) - finally: - lib.EC_POINT_free(result) - finally: - lib.EC_POINT_free(rp) - - - def verify(self, signature, subject, public_key): - r_raw = signature[:self.public_key_length] - r = BN(r_raw) - s = BN(signature[self.public_key_length:]) - if r >= self.order: - raise ValueError("r is out of bounds") - if s >= self.order: - raise ValueError("s is out of bounds") - - bn_ctx = BN.Context.get() - - z = self._subject_to_bn(subject) - - pub_p = lib.EC_POINT_new(self.group) - if not pub_p: - raise ValueError("Could not create public key point") - try: - init_buf = b"\x04" + public_key[0] + public_key[1] - if not lib.EC_POINT_oct2point(self.group, pub_p, init_buf, len(init_buf), bn_ctx): - raise ValueError("Could initialize point") - - sinv = s.inverse(self.order) - u1 = (z * sinv) % self.order - u2 = (r * sinv) % self.order - - # Recover public key - result = lib.EC_POINT_new(self.group) - if not result: - raise ValueError("Could not create point") - try: - if not lib.EC_POINT_mul(self.group, result, u1.bn, pub_p, u2.bn, bn_ctx): - raise ValueError("Could not recover public key") - if BN(self._point_to_affine(result)[0]) % self.order != r: - raise ValueError("Invalid signature") - return True - finally: - lib.EC_POINT_free(result) - finally: - lib.EC_POINT_free(pub_p) - - - def derive_child(self, seed, child): - # Round 1 - h = hmac.new(key=b"Bitcoin seed", msg=seed, digestmod="sha512").digest() - private_key1 = h[:32] - x, y = self.private_to_public(private_key1) - public_key1 = bytes([0x02 + (y[-1] % 2)]) + x - private_key1 = BN(private_key1) - - # Round 2 - child_bytes = [] - for _ in range(4): - child_bytes.append(child & 255) - child >>= 8 - child_bytes = bytes(child_bytes[::-1]) - msg = public_key1 + child_bytes - h = hmac.new(key=h[32:], msg=msg, digestmod="sha512").digest() - private_key2 = BN(h[:32]) - - return ((private_key1 + private_key2) % self.order).bytes(self.public_key_length) - - - @classmethod - def get_backend(cls): - return openssl_backend - - -ecc = ECC(EllipticCurveBackend, aes) diff --git a/src/lib/sslcrypto/openssl/library.py b/src/lib/sslcrypto/openssl/library.py deleted file mode 100644 index 8b3e1d2f..00000000 --- a/src/lib/sslcrypto/openssl/library.py +++ /dev/null @@ -1,93 +0,0 @@ -import os -import sys -import ctypes -import ctypes.util - - -# Disable false-positive _MEIPASS -# pylint: disable=no-member,protected-access - -# Discover OpenSSL library -def discover_paths(): - # Search local files first - if "win" in sys.platform: - # Windows - names = [ - "libeay32.dll" - ] - openssl_paths = [os.path.abspath(path) for path in names] - if hasattr(sys, "_MEIPASS"): - openssl_paths += [os.path.join(sys._MEIPASS, path) for path in openssl_paths] - openssl_paths.append(ctypes.util.find_library("libeay32")) - elif "darwin" in sys.platform: - # Mac OS - names = [ - "libcrypto.dylib", - "libcrypto.1.1.0.dylib", - "libcrypto.1.0.2.dylib", - "libcrypto.1.0.1.dylib", - "libcrypto.1.0.0.dylib", - "libcrypto.0.9.8.dylib" - ] - openssl_paths = [os.path.abspath(path) for path in names] - openssl_paths += names - openssl_paths += [ - "/usr/local/opt/openssl/lib/libcrypto.dylib" - ] - if hasattr(sys, "_MEIPASS") and "RESOURCEPATH" in os.environ: - openssl_paths += [ - os.path.join(os.environ["RESOURCEPATH"], "..", "Frameworks", name) - for name in names - ] - openssl_paths.append(ctypes.util.find_library("ssl")) - else: - # Linux, BSD and such - names = [ - "libcrypto.so", - "libssl.so", - "libcrypto.so.1.1.0", - "libssl.so.1.1.0", - "libcrypto.so.1.0.2", - "libssl.so.1.0.2", - "libcrypto.so.1.0.1", - "libssl.so.1.0.1", - "libcrypto.so.1.0.0", - "libssl.so.1.0.0", - "libcrypto.so.0.9.8", - "libssl.so.0.9.8" - ] - openssl_paths = [os.path.abspath(path) for path in names] - openssl_paths += names - if hasattr(sys, "_MEIPASS"): - openssl_paths += [os.path.join(sys._MEIPASS, path) for path in names] - openssl_paths.append(ctypes.util.find_library("ssl")) - - return openssl_paths - - -def discover_library(): - for path in discover_paths(): - if path: - try: - return ctypes.CDLL(path) - except OSError: - pass - raise OSError("OpenSSL is unavailable") - - -lib = discover_library() - -# Initialize internal state -try: - lib.OPENSSL_add_all_algorithms_conf() -except AttributeError: - pass - -try: - lib.OpenSSL_version.restype = ctypes.c_char_p - openssl_backend = lib.OpenSSL_version(0).decode() -except AttributeError: - lib.SSLeay_version.restype = ctypes.c_char_p - openssl_backend = lib.SSLeay_version(0).decode() - -openssl_backend += " at " + lib._name diff --git a/src/lib/sslcrypto/openssl/rsa.py b/src/lib/sslcrypto/openssl/rsa.py deleted file mode 100644 index afd8b51c..00000000 --- a/src/lib/sslcrypto/openssl/rsa.py +++ /dev/null @@ -1,11 +0,0 @@ -# pylint: disable=too-few-public-methods - -from .library import openssl_backend - - -class RSA: - def get_backend(self): - return openssl_backend - - -rsa = RSA() diff --git a/src/util/Electrum.py b/src/util/Electrum.py deleted file mode 100644 index 112151aa..00000000 --- a/src/util/Electrum.py +++ /dev/null @@ -1,39 +0,0 @@ -import hashlib -import struct - - -# Electrum, the heck?! - -def bchr(i): - return struct.pack("B", i) - -def encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = b"".join([bchr(x) for x in range(256)]) - result = b"" - while val > 0: - index = val % base - result = code_string[index:index + 1] + result - val //= base - return code_string[0:1] * max(minlen - len(result), 0) + result - -def insane_int(x): - x = int(x) - if x < 253: - return bchr(x) - elif x < 65536: - return bchr(253) + encode(x, 256, 2)[::-1] - elif x < 4294967296: - return bchr(254) + encode(x, 256, 4)[::-1] - else: - return bchr(255) + encode(x, 256, 8)[::-1] - - -def magic(message): - return b"\x18Bitcoin Signed Message:\n" + insane_int(len(message)) + message - -def format(message): - return hashlib.sha256(magic(message)).digest() - -def dbl_format(message): - return hashlib.sha256(format(message)).digest() From d19cc6461183c28e6fb6562ffa3b4d91df4786b6 Mon Sep 17 00:00:00 2001 From: Gigantic Black Bear <58946898+GiganticBlackBear@users.noreply.github.com> Date: Mon, 16 Dec 2019 15:19:42 +0000 Subject: [PATCH 289/483] Update hu.json --- plugins/Sidebar/languages/hu.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/languages/hu.json b/plugins/Sidebar/languages/hu.json index 40ed8fab..21216825 100644 --- a/plugins/Sidebar/languages/hu.json +++ b/plugins/Sidebar/languages/hu.json @@ -76,7 +76,7 @@ "Delete this site": "Az oldal törlése", "File write error: ": "Fájl írási hiba: ", "Site settings saved!": "Az oldal beállításai elmentve!", - "Enter your private key:": "Add meg a prviát kulcsod:", + "Enter your private key:": "Add meg a privát kulcsod:", " Signed!": " Aláírva!", "WebGL not supported": "WebGL nem támogatott" -} \ No newline at end of file +} From 1e175bc41f9ccde5bc1af4dbaee2b0f6aa4ec203 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:10:05 +0100 Subject: [PATCH 290/483] Remove used cursors from benchmark db test --- plugins/Benchmark/BenchmarkDb.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/Benchmark/BenchmarkDb.py b/plugins/Benchmark/BenchmarkDb.py index 6aa3a028..a767a3f4 100644 --- a/plugins/Benchmark/BenchmarkDb.py +++ b/plugins/Benchmark/BenchmarkDb.py @@ -116,6 +116,7 @@ class ActionsPlugin: for row in res: found_total += 1 found += 1 + del(res) yield "." assert found == 100, "%s != 100 (i: %s)" % (found, i) yield "Found: %s" % found_total @@ -134,6 +135,7 @@ class ActionsPlugin: found_total += 1 found += 1 yield "." + del(res) if i == 0 or i > 100: assert found == 0, "%s != 0 (i: %s)" % (found, i) else: From 1be56b5a3943b5414bc34538af00b002163f8dc1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:10:42 +0100 Subject: [PATCH 291/483] Return exit code 1 if any test failed --- plugins/Benchmark/BenchmarkPlugin.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index 73b95d22..1916a664 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -4,6 +4,7 @@ import io import math import hashlib import re +import sys from Config import config from Crypt import CryptHash @@ -188,15 +189,21 @@ class ActionsPlugin: if not res: yield "! No tests found" + sys.exit(1) else: + num_failed = len([res_key for res_key, res_val in res.items() if res_val != "ok"]) + num_success = len([res_key for res_key, res_val in res.items() if res_val != "ok"]) yield "* Result:\n" yield " - Total: %s tests\n" % len(res) - yield " - Success: %s tests\n" % len([res_key for res_key, res_val in res.items() if res_val == "ok"]) - yield " - Failed: %s tests\n" % len([res_key for res_key, res_val in res.items() if res_val != "ok"]) + yield " - Success: %s tests\n" % num_success + yield " - Failed: %s tests\n" % num_failed if any(multiplers): multipler_avg = sum(multiplers) / len(multiplers) multipler_title = self.getMultiplerTitle(multipler_avg) yield " - Average speed factor: %.2fx (%s)" % (multipler_avg, multipler_title) + if num_failed == 0: + sys.exit(1) + def testHttps(self, num_run=1): """ From 2a402a067405cecc17d64ed9339c6f63cb6412dc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:18:54 +0100 Subject: [PATCH 292/483] Use thread-safe mode to create directories --- plugins/Bigfile/BigfilePlugin.py | 8 ++------ src/Site/SiteStorage.py | 20 ++++++++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index e3974ef6..13254df9 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -406,9 +406,7 @@ class SiteStoragePlugin(object): def createSparseFile(self, inner_path, size, sha512=None): file_path = self.getPath(inner_path) - file_dir = os.path.dirname(file_path) - if not os.path.isdir(file_dir): - os.makedirs(file_dir) + self.ensureDir(os.path.dirname(file_path)) f = open(file_path, 'wb') f.truncate(min(1024 * 1024 * 5, size)) # Only pre-allocate up to 5MB @@ -432,9 +430,7 @@ class SiteStoragePlugin(object): file_path = self.getPath(inner_path) # Create dir if not exist - file_dir = os.path.dirname(file_path) - if not os.path.isdir(file_dir): - os.makedirs(file_dir) + self.ensureDir(os.path.dirname(inner_path)) if not os.path.isfile(file_path): file_info = self.site.content_manager.getFileInfo(inner_path) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 260f6827..f95417dc 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -3,6 +3,7 @@ import re import shutil import json import time +import errno from collections import defaultdict import sqlite3 @@ -225,13 +226,22 @@ class SiteStorage(object): raise err return res + def ensureDir(self, inner_path): + try: + os.makedirs(self.getPath(inner_path)) + except OSError as err: + if err.errno == errno.EEXIST: + return False + else: + raise err + return True + # Open file object def open(self, inner_path, mode="rb", create_dirs=False, **kwargs): file_path = self.getPath(inner_path) if create_dirs: - file_dir = os.path.dirname(file_path) - if not os.path.isdir(file_dir): - os.makedirs(file_dir) + file_inner_dir = os.path.dirname(inner_path) + self.ensureDir(file_inner_dir) return open(file_path, mode, **kwargs) # Open file object @@ -243,9 +253,7 @@ class SiteStorage(object): def writeThread(self, inner_path, content): file_path = self.getPath(inner_path) # Create dir if not exist - file_dir = os.path.dirname(file_path) - if not os.path.isdir(file_dir): - os.makedirs(file_dir) + self.ensureDir(os.path.dirname(inner_path)) # Write file if hasattr(content, 'read'): # File-like object From 02d45e9c39697b0aa9f6722505857532bd26af33 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:20:49 +0100 Subject: [PATCH 293/483] Use separate threadpool for batch site storage operations --- src/Site/SiteStorage.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index f95417dc..3c4c7b70 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -22,6 +22,7 @@ from Translate import translate as _ thread_pool_fs_read = ThreadPool.ThreadPool(config.threads_fs_read) thread_pool_fs_write = ThreadPool.ThreadPool(config.threads_fs_write) +thread_pool_fs_batch = ThreadPool.ThreadPool(1, name="FS batch") @PluginManager.acceptPlugins @@ -128,9 +129,9 @@ class SiteStorage(object): # Rebuild sql cache @util.Noparallel() - @thread_pool_fs_write.wrap def rebuildDb(self, delete_db=True): self.log.info("Rebuilding db...") + @thread_pool_fs_batch.wrap self.has_db = self.isFile("dbschema.json") if not self.has_db: return False @@ -524,7 +525,7 @@ class SiteStorage(object): self.log.debug("Checked files in %.2fs... Found bad files: %s, Quick:%s" % (time.time() - s, len(bad_files), quick_check)) # Delete site's all file - @thread_pool_fs_write.wrap + @thread_pool_fs_batch.wrap def deleteFiles(self): site_title = self.site.content_manager.contents.get("content.json", {}).get("title", self.site.address) message_id = "delete-%s" % self.site.address From 8ed7d0385d955e236ef5efff82762831ee23b375 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:21:47 +0100 Subject: [PATCH 294/483] If possible use loaded db to get db file inner_path --- src/Site/SiteStorage.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 3c4c7b70..fd546913 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -44,11 +44,14 @@ class SiteStorage(object): raise Exception("Directory not exists: %s" % self.directory) def getDbFile(self): - if self.isFile("dbschema.json"): - schema = self.loadJson("dbschema.json") - return schema["db_file"] + if self.db: + return self.db.schema["db_file"] else: - return False + if self.isFile("dbschema.json"): + schema = self.loadJson("dbschema.json") + return schema["db_file"] + else: + return False # Create new databaseobject with the site's schema @util.Noparallel() From 08a0a6363117aa9e8806fe2b26da45ff6ddfa234 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:22:29 +0100 Subject: [PATCH 295/483] Create ssl contexts only once --- src/Crypt/CryptConnection.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 6c19ba68..689357fa 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -18,6 +18,9 @@ class CryptConnectionManager: else: self.openssl_bin = "openssl" + self.context_client = None + self.context_server = None + self.openssl_conf_template = "src/lib/openssl/openssl.cnf" self.openssl_conf = config.data_dir + "/openssl.cnf" @@ -47,6 +50,8 @@ class CryptConnectionManager: ] def createSslContexts(self): + if self.context_server and self.context_client: + return False ciphers = "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:AES128-SHA256:AES256-SHA:" ciphers += "!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK" From 0171cb08446fef7efbb02eb1c20b1474325f8ce6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:23:18 +0100 Subject: [PATCH 296/483] Avoid get db_inner_path for every file on signing --- src/Content/ContentManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index fe2a74dd..dc9d5144 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -615,6 +615,7 @@ class ContentManager(object): def hashFiles(self, dir_inner_path, ignore_pattern=None, optional_pattern=None): files_node = {} files_optional_node = {} + db_inner_path = self.site.storage.getDbFile() if dir_inner_path and not self.isValidRelativePath(dir_inner_path): ignored = True self.log.error("- [ERROR] Only ascii encoded directories allowed: %s" % dir_inner_path) @@ -630,7 +631,7 @@ class ContentManager(object): elif not self.isValidRelativePath(file_relative_path): ignored = True self.log.error("- [ERROR] Invalid filename: %s" % file_relative_path) - elif dir_inner_path == "" and self.site.storage.getDbFile() and file_relative_path.startswith(self.site.storage.getDbFile()): + elif dir_inner_path == "" and db_inner_path and file_relative_path.startswith(db_inner_path): ignored = True elif optional_pattern and SafeRe.match(optional_pattern, file_relative_path): optional = True From 1eda3258dedd100c9e4076f41231e17716e682cc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:23:31 +0100 Subject: [PATCH 297/483] Always raise error on verify error --- src/Content/ContentManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index dc9d5144..4ee02bfb 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -891,7 +891,7 @@ class ContentManager(object): self.site.settings["size_optional"] = site_size_optional return True else: - return False + raise VerifyError("Content verify error") def verifyContentInclude(self, inner_path, content, content_size, content_size_optional): # Load include details From c2d218903982149ec8bb9cb3d1a1e54adbcb2868 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:23:47 +0100 Subject: [PATCH 298/483] Log content init failed as info --- src/Content/ContentManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 4ee02bfb..7ef7e54e 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -40,7 +40,7 @@ class ContentManager(object): # Load all content.json files def loadContents(self): if len(self.contents) == 0: - self.log.debug("ContentDb not initialized, load files from filesystem") + self.log.info("ContentDb not initialized, load files from filesystem...") self.loadContent(add_bad_files=False, delete_removed_files=False) self.site.settings["size"], self.site.settings["size_optional"] = self.getTotalSize() From b4f7e51e96214e423430d2cbf9142859552f528b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:24:08 +0100 Subject: [PATCH 299/483] Limit stack size on formatting --- src/Debug/Debug.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Debug/Debug.py b/src/Debug/Debug.py index a48bc2ba..2e38683e 100644 --- a/src/Debug/Debug.py +++ b/src/Debug/Debug.py @@ -54,13 +54,18 @@ def formatException(err=None, format="text"): return "%s: %s in %s" % (exc_type.__name__, err, " > ".join(tb)) -def formatStack(): +def formatStack(limit=99): import inspect back = [] + i = 0 for stack in inspect.stack(): + i += 1 frame, path, line, function, source, index = stack file = os.path.split(path)[1] back.append("%s line %s" % (file, line)) + if i > limit: + back.append("...") + break return " > ".join(back) From eb63eb7b1db8e7518480a243447e56ec1a1c1751 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:24:44 +0100 Subject: [PATCH 300/483] Log startup errors in log file --- src/main.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main.py b/src/main.py index ce034454..c6cb61e8 100644 --- a/src/main.py +++ b/src/main.py @@ -5,6 +5,11 @@ import stat import time import logging +startup_errors = [] +def startupError(msg): + startup_errors.append(msg) + print("Startup error: %s" % msg) + # Third party modules import gevent @@ -25,7 +30,7 @@ if not os.path.isdir(config.data_dir): try: os.chmod(config.data_dir, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) except Exception as err: - print("Can't change permission of %s: %s" % (config.data_dir, err)) + startupError("Can't change permission of %s: %s" % (config.data_dir, err)) if not os.path.isfile("%s/sites.json" % config.data_dir): open("%s/sites.json" % config.data_dir, "w").write("{}") @@ -38,7 +43,7 @@ if config.action == "main": lock = helper.openLocked("%s/lock.pid" % config.data_dir, "w") lock.write("%s" % os.getpid()) except BlockingIOError as err: - print("Can't open lock file, your ZeroNet client is probably already running, exiting... (%s)" % err) + startupError("Can't open lock file, your ZeroNet client is probably already running, exiting... (%s)" % err) if config.open_browser and config.open_browser != "False": print("Opening browser: %s...", config.open_browser) import webbrowser @@ -51,12 +56,11 @@ if config.action == "main": config.ui_ip if config.ui_ip != "*" else "127.0.0.1", config.ui_port, config.homepage ), new=2) except Exception as err: - print("Error starting browser: %s" % err) + startupError("Error starting browser: %s" % err) sys.exit() config.initLogging() - # Debug dependent configuration from Debug import DebugHook @@ -135,6 +139,9 @@ class Actions(object): ui_server = UiServer() file_server.ui_server = ui_server + for startup_error in startup_errors: + logging.error("Startup error: %s" % startup_error) + logging.info("Removing old SSL certs...") from Crypt import CryptConnection CryptConnection.manager.removeCerts() From 51f49cd45aa0677427683828a15d23316c91a9bd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:25:04 +0100 Subject: [PATCH 301/483] Always use libev if possible --- src/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.py b/src/main.py index c6cb61e8..4083ce88 100644 --- a/src/main.py +++ b/src/main.py @@ -12,6 +12,12 @@ def startupError(msg): # Third party modules import gevent +try: + # Workaround for random crash when libuv used with threads + if "libev" not in str(gevent.config.loop): + gevent.config.loop = "libev-cext" +except Exception as err: + startupError("Unable to switch gevent loop to libev: %s" % err) import gevent.monkey gevent.monkey.patch_all(thread=False, subprocess=False) From a54f5f3e9f5d284fcd01e41e065719b3d9cb4c7d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:26:14 +0100 Subject: [PATCH 302/483] Change trackers to more stable onces --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index 7ea187ea..2db47801 100644 --- a/src/Config.py +++ b/src/Config.py @@ -78,11 +78,11 @@ class Config(object): "zero://boot3rdez4rzn36x.onion:15441", "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443", # US/NY "udp://tracker.coppersurfer.tk:6969", # DE - "udp://amigacity.xyz:6969", # US/NY + "udp://tracker.zum.bi:6969", # US/NY "udp://104.238.198.186:8000", # US/LA "http://tracker01.loveapp.com:6789/announce", # Google "http://open.acgnxtracker.com:80/announce", # DE - "http://open.trackerlist.xyz:80/announce", # Cloudflare + "http://tracker.bt4g.com:2095/announce", # Cloudflare "zero://2602:ffc5::c5b2:5360:26312" # US/ATL ] # Platform specific From dca1dcdd2de99e9b5f032a1f8ed8ab62e0cf9e5b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:28:52 +0100 Subject: [PATCH 303/483] Use always active connection in DbCursor --- src/Db/Db.py | 2 +- src/Db/DbCursor.py | 34 ++++++++++++++-------------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index 0a2f280c..7acea90d 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -222,7 +222,7 @@ class Db(object): if not self.conn: self.connect() - cur = DbCursor(self.conn, self) + cur = DbCursor(self) return cur def getSharedCursor(self): diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index b7d354da..4a337273 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -2,14 +2,12 @@ import time import re from util import helper - # Special sqlite cursor class DbCursor: - def __init__(self, conn, db): - self.conn = conn + def __init__(self, db): self.db = db self.logging = False @@ -96,24 +94,19 @@ class DbCursor: query, params = self.parseQuery(query, params) s = time.time() - cursor = self.conn.cursor() + cursor = self.db.getConn().cursor() + self.db.cursors.add(cursor) - try: - if self.db.lock.locked(): - self.db.log.debug("Query delayed: db locked") - self.db.lock.acquire(True) - if params: - res = cursor.execute(query, params) + if params: + res = cursor.execute(query, params) + else: + res = cursor.execute(query) + taken_query = time.time() - s + if self.logging or taken_query > 0.1: + if params: # Query has parameters + self.db.log.debug("Query: " + query + " " + str(params) + " (Done in %.4f)" % (time.time() - s)) else: - res = cursor.execute(query) - taken_query = time.time() - s - if self.logging or taken_query > 0.1: - if params: # Query has parameters - self.db.log.debug("Query: " + query + " " + str(params) + " (Done in %.4f)" % (time.time() - s)) - else: - self.db.log.debug("Query: " + query + " (Done in %.4f)" % (time.time() - s)) - finally: - self.db.lock.release() + self.db.log.debug("Query: " + query + " (Done in %.4f)" % (time.time() - s)) # Log query stats if self.db.collect_stats: @@ -139,7 +132,8 @@ class DbCursor: self.db.last_query_time = time.time() s = time.time() - cursor = self.conn.cursor() + cursor = self.db.getConn().cursor() + self.db.cursors.add(cursor) try: self.db.lock.acquire(True) From 31a6e3ee9a24d9da58315e0bd01b681b5a86eb09 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:29:48 +0100 Subject: [PATCH 304/483] Don't allow clone to run in parallel --- src/Site/Site.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Site/Site.py b/src/Site/Site.py index f8a1d7d7..923fd8c7 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -616,6 +616,7 @@ class Site(object): return len(published) # Copy this site + @util.Noparallel() def clone(self, address, privatekey=None, address_index=None, root_inner_path="", overwrite=False): import shutil new_site = SiteManager.site_manager.need(address, all_file=False) From af1ac9bce8542601f29586127aa9491b14581224 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:30:14 +0100 Subject: [PATCH 305/483] Try to find already running task for file before start a new one --- src/Site/Site.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 923fd8c7..8078ba40 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -760,7 +760,13 @@ class Site(object): # Check and download if file not exist def needFile(self, inner_path, update=False, blocking=True, peer=None, priority=0): - if self.storage.isFile(inner_path) and not update: # File exist, no need to do anything + if self.worker_manager.tasks.findTask(inner_path): + task = self.worker_manager.addTask(inner_path, peer, priority=priority) + if blocking: + return task["evt"].get() + else: + return task["evt"] + elif self.storage.isFile(inner_path) and not update: # File exist, no need to do anything return True elif not self.isServing(): # Site not serving return False From 20ba9cd589b5af3dfb859e63636e05a33e9cfe09 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:30:29 +0100 Subject: [PATCH 306/483] Log site download time --- src/Site/Site.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Site/Site.py b/src/Site/Site.py index 8078ba40..67220afc 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -324,6 +324,7 @@ class Site(object): self.log.debug("No connection server found, skipping download") return False + s = time.time() self.log.debug( "Start downloading, bad_files: %s, check_size: %s, blind_includes: %s" % (self.bad_files, check_size, blind_includes) @@ -338,6 +339,7 @@ class Site(object): valid = self.downloadContent("content.json", check_modifications=blind_includes) self.onComplete.once(lambda: self.retryBadFiles(force=True)) + self.log.debug("Download done in %.3fs" % (time.time () - s)) return valid From 8de1714f08ef4338c4331c4378bd19bc1ea1199e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:31:12 +0100 Subject: [PATCH 307/483] Fix onComplete call when donwload end --- src/Site/Site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 67220afc..08f0327f 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -273,7 +273,7 @@ class Site(object): if config.verbose: self.log.debug("%s: DownloadContent ended in %.3fs" % (inner_path, time.time() - s)) - if not self.worker_manager.tasks: + if len(self.worker_manager.tasks) == 0: self.onComplete() # No more task trigger site complete return True From d7cabb47ca097edd545a4e20b742f2f0e2737d33 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:31:41 +0100 Subject: [PATCH 308/483] Log task numbers on content.json start --- src/Site/Site.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 08f0327f..fd790342 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -271,7 +271,9 @@ class Site(object): self.log.debug("%s: Downloading %s files, changed: %s..." % (inner_path, len(file_threads), len(changed))) gevent.joinall(file_threads) if config.verbose: - self.log.debug("%s: DownloadContent ended in %.3fs" % (inner_path, time.time() - s)) + self.log.debug("%s: DownloadContent ended in %.3fs (tasks left: %s)" % ( + inner_path, time.time() - s, len(self.worker_manager.tasks) + )) if len(self.worker_manager.tasks) == 0: self.onComplete() # No more task trigger site complete From 10c1986c54652d0342423c87d6a1164e8223deb6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:31:55 +0100 Subject: [PATCH 309/483] Fix site list changing during listing --- src/Site/SiteManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 9c33d8b7..f9477b69 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -104,7 +104,7 @@ class SiteManager(object): data = {} # Generate data file s = time.time() - for address, site in self.list().items(): + for address, site in list(self.list().items()): if recalculate_size: site.settings["size"], site.settings["size_optional"] = site.content_manager.getTotalSize() # Update site size data[address] = site.settings From 79c1cd15ab61d4842123786056e92c8e0d7f3130 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:32:17 +0100 Subject: [PATCH 310/483] Use libev when running test --- src/Test/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 74f27712..8155cdc2 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -13,6 +13,10 @@ import pytest import mock import gevent +if "libev" not in str(gevent.config.loop): + # Workaround for random crash when libuv used with threads + gevent.config.loop = "libev-cext" + import gevent.event from gevent import monkey monkey.patch_all(thread=False, subprocess=False) From b138ebc519422a013eeb2237ed94afd4c76a505a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:32:43 +0100 Subject: [PATCH 311/483] Capture fd for pytest --- src/Test/pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/pytest.ini b/src/Test/pytest.ini index c1a341c5..556389a2 100644 --- a/src/Test/pytest.ini +++ b/src/Test/pytest.ini @@ -1,6 +1,6 @@ [pytest] python_files = Test*.py -addopts = -rsxX -v --durations=6 --no-print-logs --capture=sys +addopts = -rsxX -v --durations=6 --no-print-logs --capture=fd markers = slow: mark a tests as slow. webtest: mark a test as a webtest. From 6539ca5eb032a1fd2d9b5650c50e356e488bdcd8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:33:06 +0100 Subject: [PATCH 312/483] Log spy actions to file when running tests --- src/Test/Spy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Test/Spy.py b/src/Test/Spy.py index 8062d063..44422550 100644 --- a/src/Test/Spy.py +++ b/src/Test/Spy.py @@ -1,3 +1,5 @@ +import logging + class Spy: def __init__(self, obj, func_name): self.obj = obj @@ -6,11 +8,12 @@ class Spy: self.calls = [] def __enter__(self, *args, **kwargs): + logging.debug("Spy started") def loggedFunc(cls, *args, **kwargs): call = dict(enumerate(args, 1)) call[0] = cls call.update(kwargs) - print("Logging", call) + logging.debug("Spy call: %s" % call) self.calls.append(call) return self.func_original(cls, *args, **kwargs) setattr(self.obj, self.__name__, loggedFunc) From e91fb90a4525b61dc950b49b8f64ca3b1a00f626 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:34:29 +0100 Subject: [PATCH 313/483] Fix tests when running for long time --- plugins/OptionalManager/ContentDbPlugin.py | 2 +- plugins/OptionalManager/OptionalManagerPlugin.py | 4 ++++ src/Test/conftest.py | 6 ++++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index ccfd6637..21193fa0 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -25,7 +25,7 @@ class ContentDbPlugin(object): self.my_optional_files = {} # Last 50 site_address/inner_path called by fileWrite (auto-pinning these files) self.optional_files = collections.defaultdict(dict) self.optional_files_loading = False - helper.timer(60 * 5, self.checkOptionalLimit) + self.timer_check_optional = helper.timer(60 * 5, self.checkOptionalLimit) super(ContentDbPlugin, self).__init__(*args, **kwargs) def getSchema(self): diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py index c33063dc..fbb8158c 100644 --- a/plugins/OptionalManager/OptionalManagerPlugin.py +++ b/plugins/OptionalManager/OptionalManagerPlugin.py @@ -19,6 +19,8 @@ def importPluginnedClasses(): def processAccessLog(): if access_log: content_db = ContentDbPlugin.content_db + if not content_db.conn: + return False now = int(time.time()) num = 0 for site_id in access_log: @@ -33,6 +35,8 @@ def processAccessLog(): def processRequestLog(): if request_log: content_db = ContentDbPlugin.content_db + if not content_db.conn: + return False cur = content_db.getCursor() num = 0 for site_id in request_log: diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 8155cdc2..b1944cd7 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -186,7 +186,8 @@ def site(request): def cleanup(): site.delete() - site.content_manager.contents.db.close() + site.content_manager.contents.db.close("Test cleanup") + site.content_manager.contents.db.timer_check_optional.kill() SiteManager.site_manager.sites.clear() db_path = "%s/content.db" % config.data_dir os.unlink(db_path) @@ -213,7 +214,8 @@ def site_temp(request): def cleanup(): site_temp.delete() - site_temp.content_manager.contents.db.close() + site_temp.content_manager.contents.db.close("Test cleanup") + site_temp.content_manager.contents.db.timer_check_optional.kill() db_path = "%s-temp/content.db" % config.data_dir os.unlink(db_path) del ContentDb.content_dbs[db_path] From d062f011274a783fbb9b8bff3ad99ba5ee6fa13f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:34:53 +0100 Subject: [PATCH 314/483] Log temp site events under different name --- src/Test/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index b1944cd7..6cfac984 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -221,6 +221,7 @@ def site_temp(request): del ContentDb.content_dbs[db_path] gevent.killall([obj for obj in gc.get_objects() if isinstance(obj, gevent.Greenlet) and obj not in threads_before]) request.addfinalizer(cleanup) + site_temp.log = logging.getLogger("Temp:%s" % site_temp.address_short) return site_temp From 0839fdfc5e4be658317a5803b28b01e0d01839e8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:35:49 +0100 Subject: [PATCH 315/483] Add reason for db close --- src/Db/Db.py | 11 ++++++----- src/Test/conftest.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index 7acea90d..22a10516 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -29,7 +29,7 @@ def dbCleanup(): for db in opened_dbs[:]: idle = time.time() - db.last_query_time if idle > 60 * 5 and db.close_idle: - db.close() + db.close("Cleanup") def dbCommitCheck(): @@ -47,7 +47,7 @@ def dbCommitCheck(): def dbCloseAll(): for db in opened_dbs[:]: - db.close() + db.close("Close all") gevent.spawn(dbCleanup) @@ -196,7 +196,7 @@ class Db(object): self.delayed_queue = [] self.delayed_queue_thread = None - def close(self): + def close(self, reason="Unknown"): if not self.conn: return False s = time.time() @@ -205,7 +205,7 @@ class Db(object): if self in opened_dbs: opened_dbs.remove(self) self.need_commit = False - self.commit("Closing") + self.commit("Closing: %s" % reason) self.log.debug("Close called by %s" % Debug.formatStack()) if self.cur: self.cur.close() @@ -213,7 +213,8 @@ class Db(object): self.conn.close() self.conn = None self.cur = None - self.log.debug("%s closed in %.3fs, opened: %s" % (self.db_path, time.time() - s, len(opened_dbs))) + self.log.debug("%s closed (reason: %s) in %.3fs, opened: %s" % (self.db_path, reason, time.time() - s, len(opened_dbs))) + self.connect_lock.release() return True # Gets a cursor object to database diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 6cfac984..91cd10c4 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -407,7 +407,7 @@ def db(request): db.checkTables() def stop(): - db.close() + db.close("Test db cleanup") os.unlink(db_path) request.addfinalizer(stop) From 4c31aae97b2023da660870600c53aea474a63ba8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:42:33 +0100 Subject: [PATCH 316/483] Refactor worker, fix concurrent write errors --- src/Worker/Worker.py | 287 ++++++++++++++++++++++-------------- src/Worker/WorkerManager.py | 6 +- 2 files changed, 177 insertions(+), 116 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 5b97099a..4cdb83cb 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -1,9 +1,23 @@ import time import gevent +import gevent.lock from Debug import Debug from Config import config +from Content.ContentManager import VerifyError + + +class WorkerDownloadError(Exception): + pass + + +class WorkerIOError(Exception): + pass + + +class WorkerStop(Exception): + pass class Worker(object): @@ -24,134 +38,181 @@ class Worker(object): def __repr__(self): return "<%s>" % self.__str__() - # Downloader thread + def waitForTask(self, task, timeout): # Wait for other workers to finish the task + for sleep_i in range(1, timeout * 10): + time.sleep(0.1) + if task["done"] or task["workers_num"] == 0: + if config.verbose: + self.manager.log.debug("%s: %s, picked task free after %ss sleep. (done: %s)" % ( + self.key, task["inner_path"], 0.1 * sleep_i, task["done"] + )) + break + + if sleep_i % 10 == 0: + workers = self.manager.findWorkers(task) + if not workers or not workers[0].peer.connection: + break + worker_idle = time.time() - workers[0].peer.connection.last_recv_time + if worker_idle > 1: + if config.verbose: + self.manager.log.debug("%s: %s, worker %s seems idle, picked up task after %ss sleep. (done: %s)" % ( + self.key, task["inner_path"], workers[0].key, 0.1 * sleep_i, task["done"] + )) + break + return True + + def pickTask(self): # Find and select a new task for the worker + task = self.manager.getTask(self.peer) + if not task: # No more task + time.sleep(0.1) # Wait a bit for new tasks + task = self.manager.getTask(self.peer) + if not task: # Still no task, stop it + stats = "downloaded files: %s, failed: %s" % (self.num_downloaded, self.num_failed) + self.manager.log.debug("%s: No task found, stopping (%s)" % (self.key, stats)) + return False + + if not task["time_started"]: + task["time_started"] = time.time() # Task started now + + if task["workers_num"] > 0: # Wait a bit if someone already working on it + if task["peers"]: # It's an update + timeout = 3 + else: + timeout = 1 + + if task["size"] > 100 * 1024 * 1024: + timeout = timeout * 2 + + if config.verbose: + self.manager.log.debug("%s: Someone already working on %s (pri: %s), sleeping %s sec..." % ( + self.key, task["inner_path"], task["priority"], timeout + )) + + self.waitForTask(task, timeout) + return task + + def downloadTask(self, task): + try: + buff = self.peer.getFile(task["site"].address, task["inner_path"], task["size"]) + except Exception as err: + self.manager.log.debug("%s: getFile error: %s" % (self.key, err)) + raise WorkerDownloadError(str(err)) + + if not buff: + raise WorkerDownloadError("No response") + + return buff + + def getTaskLock(self, task): + if task["lock"] is None: + task["lock"] = gevent.lock.Semaphore() + return task["lock"] + + def writeTask(self, task, buff): + buff.seek(0) + try: + task["site"].storage.write(task["inner_path"], buff) + except Exception as err: + if type(err) == Debug.Notify: + self.manager.log.debug("%s: Write aborted: %s (%s)" % (self.key, task["inner_path"], err)) + else: + self.manager.log.error("%s: Error writing: %s (%s)" % (self.key, task["inner_path"], err)) + raise WorkerIOError(str(err)) + + def onTaskVerifyFail(self, task, error_message): + self.num_failed += 1 + if self.manager.started_task_num < 50 or config.verbose: + self.manager.log.debug( + "%s: Verify failed: %s, error: %s, failed peers: %s, workers: %s" % + (self.key, task["inner_path"], error_message, len(task["failed"]), task["workers_num"]) + ) + task["failed"].append(self.peer) + self.peer.hash_failed += 1 + if self.peer.hash_failed >= max(len(self.manager.tasks), 3) or self.peer.connection_error > 10: + # Broken peer: More fails than tasks number but atleast 3 + raise WorkerStop( + "Too many errors (hash failed: %s, connection error: %s)" % + (self.peer.hash_failed, self.peer.connection_error) + ) + + def handleTask(self, task): + download_err = write_err = False + + write_lock = None + try: + buff = self.downloadTask(task) + + if task["done"] is True: # Task done, try to find new one + return None + + if self.running is False: # Worker no longer needed or got killed + self.manager.log.debug("%s: No longer needed, returning: %s" % (self.key, task["inner_path"])) + raise WorkerStop("Running got disabled") + + write_lock = self.getTaskLock(task) + write_lock.acquire() + if task["site"].content_manager.verifyFile(task["inner_path"], buff) is None: + is_same = True + else: + is_same = False + is_valid = True + except (WorkerDownloadError, VerifyError) as err: + download_err = err + is_valid = False + is_same = False + + if is_valid and not is_same: + if self.manager.started_task_num < 50 or config.verbose: + self.manager.log.debug("%s: Verify correct: %s" % (self.key, task["inner_path"])) + try: + self.writeTask(task, buff) + except WorkerIOError as err: + write_err = err + + if not task["done"]: + if write_err: + self.manager.failTask(task) + self.num_failed += 1 + self.manager.log.error("%s: Error writing %s: %s" % (self.key, task["inner_path"], write_err)) + elif is_valid: + self.manager.doneTask(task) + self.num_downloaded += 1 + + if write_lock is not None and write_lock.locked(): + write_lock.release() + + if not is_valid: + self.onTaskVerifyFail(task, download_err) + time.sleep(1) + return False + + return True + def downloader(self): self.peer.hash_failed = 0 # Reset hash error counter while self.running: # Try to pickup free file download task - task = self.manager.getTask(self.peer) - if not task: # No more task - time.sleep(0.1) # Wait a bit for new tasks - task = self.manager.getTask(self.peer) - if not task: # Still no task, stop it - stats = "downloaded files: %s, failed: %s" % (self.num_downloaded, self.num_failed) - self.manager.log.debug("%s: No task found, stopping (%s)" % (self.key, stats)) - break - if not task["time_started"]: - task["time_started"] = time.time() # Task started now + task = self.pickTask() - if task["workers_num"] > 0: # Wait a bit if someone already working on it - if task["peers"]: # It's an update - timeout = 3 - else: - timeout = 1 - - if task["size"] > 100 * 1024 * 1024: - timeout = timeout * 2 - - if config.verbose: - self.manager.log.debug("%s: Someone already working on %s (pri: %s), sleeping %s sec..." % ( - self.key, task["inner_path"], task["priority"], timeout - )) - - for sleep_i in range(1, timeout * 10): - time.sleep(0.1) - if task["done"] or task["workers_num"] == 0: - if config.verbose: - self.manager.log.debug("%s: %s, picked task free after %ss sleep. (done: %s)" % ( - self.key, task["inner_path"], 0.1 * sleep_i, task["done"] - )) - break - - if sleep_i % 10 == 0: - workers = self.manager.findWorkers(task) - if not workers or not workers[0].peer.connection: - break - worker_idle = time.time() - workers[0].peer.connection.last_recv_time - if worker_idle > 1: - if config.verbose: - self.manager.log.debug("%s: %s, worker %s seems idle, picked up task after %ss sleep. (done: %s)" % ( - self.key, task["inner_path"], workers[0].key, 0.1 * sleep_i, task["done"] - )) - break + if not task: + break if task["done"]: continue self.task = task - site = task["site"] + self.manager.addTaskWorker(task, self) - error_message = "Unknown error" try: - buff = self.peer.getFile(site.address, task["inner_path"], task["size"]) - except Exception as err: - self.manager.log.debug("%s: getFile error: %s" % (self.key, err)) - error_message = str(err) - buff = None - if self.running is False: # Worker no longer needed or got killed - self.manager.log.debug("%s: No longer needed, returning: %s" % (self.key, task["inner_path"])) + success = self.handleTask(task) + except WorkerStop as err: + self.manager.log.debug("%s: Worker stopped: %s" % (self.key, err)) + self.manager.removeTaskWorker(task, self) break - if task["done"] is True: # Task done, try to find new one - continue - if buff: # Download ok - try: - correct = site.content_manager.verifyFile(task["inner_path"], buff) - except Exception as err: - error_message = str(err) - correct = False - else: # Download error - error_message = "Download failed" - correct = False - if correct is True or correct is None: # Verify ok or same file - if self.manager.started_task_num < 50 or config.verbose: - self.manager.log.debug("%s: Verify correct: %s" % (self.key, task["inner_path"])) - write_error = None - task_finished = False - if correct is True and task["locked"] is False: # Save if changed and task not done yet - task["locked"] = True - buff.seek(0) - try: - site.storage.write(task["inner_path"], buff) - write_error = False - except Exception as err: - if type(err) == Debug.Notify: - self.manager.log.debug("%s: Write aborted: %s (%s)" % (self.key, task["inner_path"], err)) - else: - self.manager.log.error("%s: Error writing: %s (%s)" % (self.key, task["inner_path"], err)) - write_error = err - task_finished = True - if correct is None and task["locked"] is False: # Mark as done if same file - task["locked"] = True - task_finished = True + self.manager.removeTaskWorker(task, self) - if task_finished and not task["done"]: - if write_error: - self.manager.failTask(task) - self.num_failed += 1 - else: - self.manager.doneTask(task) - self.num_downloaded += 1 - - self.manager.removeTaskWorker(task, self) - else: # Verify failed - self.num_failed += 1 - self.manager.removeTaskWorker(task, self) - if self.manager.started_task_num < 50 or config.verbose: - self.manager.log.debug( - "%s: Verify failed: %s, error: %s, failed peers: %s, workers: %s" % - (self.key, task["inner_path"], error_message, len(task["failed"]), task["workers_num"]) - ) - task["failed"].append(self.peer) - self.peer.hash_failed += 1 - if self.peer.hash_failed >= max(len(self.manager.tasks), 3) or self.peer.connection_error > 10: - # Broken peer: More fails than tasks number but atleast 3 - break - if task["inner_path"] not in site.bad_files: - # Don't need this file anymore - break - time.sleep(1) self.peer.onWorkerDone() self.running = False self.manager.removeWorker(self) @@ -175,4 +236,4 @@ class Worker(object): if self.thread: self.thread.kill(exception=Debug.Notify("Worker stopped")) del self.thread - self.manager.removeWorker(self) \ No newline at end of file + self.manager.removeWorker(self) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index bce3a3dd..252d34ca 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -21,7 +21,7 @@ class WorkerManager(object): self.tasks = WorkerTaskManager() self.next_task_id = 1 # {"id": 1, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "optional_hash_id": None, - # "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0, "failed": peer_ids, "locked": False} + # "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0, "failed": peer_ids, "lock": None or gevent.lock.RLock} self.started_task_num = 0 # Last added task num self.asked_peers = [] self.running = True @@ -500,7 +500,7 @@ class WorkerManager(object): task = { "id": self.next_task_id, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, - "optional_hash_id": optional_hash_id, "time_added": time.time(), "time_started": None, "locked": False, + "optional_hash_id": optional_hash_id, "time_added": time.time(), "time_started": None, "lock": None, "time_action": None, "peers": peers, "priority": priority, "failed": [], "size": size } @@ -575,4 +575,4 @@ class WorkerManager(object): self.site.onFileFail(task["inner_path"]) task["evt"].set(False) if not self.tasks: - self.started_task_num = 0 \ No newline at end of file + self.started_task_num = 0 From c1df78b97fcade7c1501e359e31868ecc602beb3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:43:33 +0100 Subject: [PATCH 317/483] Name threadpools --- src/Site/SiteStorage.py | 4 ++-- src/util/ThreadPool.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index fd546913..c67df89a 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -20,8 +20,8 @@ from Plugin import PluginManager from Translate import translate as _ -thread_pool_fs_read = ThreadPool.ThreadPool(config.threads_fs_read) -thread_pool_fs_write = ThreadPool.ThreadPool(config.threads_fs_write) +thread_pool_fs_read = ThreadPool.ThreadPool(config.threads_fs_read, name="FS read") +thread_pool_fs_write = ThreadPool.ThreadPool(config.threads_fs_write, name="FS write") thread_pool_fs_batch = ThreadPool.ThreadPool(1, name="FS batch") diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 54f6e699..1611ed3f 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -4,8 +4,12 @@ import threading class ThreadPool: - def __init__(self, max_size): + def __init__(self, max_size, name=None): self.setMaxSize(max_size) + if name: + self.name = name + else: + self.name = "ThreadPool#%s" % id(self) def setMaxSize(self, max_size): self.max_size = max_size From 8a5a75e68f2244b94df20650dd4ba8bd0a89174c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:47:27 +0100 Subject: [PATCH 318/483] Allow pass calls to the main loop --- src/util/ThreadPool.py | 93 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 1611ed3f..3ed39d61 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -1,6 +1,10 @@ +import threading +import time + +import gevent +import gevent.monkey import gevent.threadpool import gevent._threading -import threading class ThreadPool: @@ -29,8 +33,12 @@ class ThreadPool: return wrapper +lock_pool = gevent.threadpool.ThreadPool(50) main_thread_id = threading.current_thread().ident -lock_pool = gevent.threadpool.ThreadPool(10) + + +def isMainThread(): + return threading.current_thread().ident == main_thread_id class Lock: @@ -40,7 +48,86 @@ class Lock: self.release = self.lock.release def acquire(self, *args, **kwargs): - if self.locked() and threading.current_thread().ident == main_thread_id: + if self.locked() and isMainThread(): + # Start in new thread to avoid blocking gevent loop return lock_pool.apply(self.lock.acquire, args, kwargs) else: return self.lock.acquire(*args, **kwargs) + + def __del__(self): + while self.locked(): + self.release() + + +class Event: + def __init__(self): + self.get_lock = Lock() + self.res = None + self.get_lock.acquire(False) + self.done = False + + def set(self, res): + if self.done: + raise Exception("Event already has value") + self.res = res + self.get_lock.release() + self.done = True + + def get(self): + if not self.done: + self.get_lock.acquire(True) + if self.get_lock.locked(): + self.get_lock.release() + back = self.res + return back + + def __del__(self): + self.res = None + while self.get_lock.locked(): + self.get_lock.release() + + +# Execute function calls in main loop from other threads +class MainLoopCaller(): + def __init__(self): + self.queue_call = gevent._threading.Queue() + + self.pool = gevent.threadpool.ThreadPool(1) + self.num_direct = 0 + + def caller(self, func, args, kwargs, event_done): + try: + res = func(*args, **kwargs) + event_done.set((True, res)) + except Exception as err: + event_done.set((False, err)) + + def start(self): + gevent.spawn(self.run) + time.sleep(0.001) + + def run(self): + while 1: + if self.queue_call.qsize() == 0: # Get queue in new thread to avoid gevent blocking + func, args, kwargs, event_done = self.pool.apply(self.queue_call.get) + else: + func, args, kwargs, event_done = self.queue_call.get() + gevent.spawn(self.caller, func, args, kwargs, event_done) + del func, args, kwargs, event_done + + def call(self, func, *args, **kwargs): + if threading.current_thread().ident == main_thread_id: + return func(*args, **kwargs) + else: + event_done = Event() + self.queue_call.put((func, args, kwargs, event_done)) + success, res = event_done.get() + del event_done + self.queue_call.task_done() + if success: + return res + else: + raise res +main_loop = MainLoopCaller() +main_loop.start() +patchSleep() From 3309489c247ba8333e3694a339d11d8104fa77c8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:48:11 +0100 Subject: [PATCH 319/483] Only call the function in separate thread when in the main loop --- src/util/ThreadPool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 3ed39d61..f11ca677 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -27,7 +27,9 @@ class ThreadPool: return func def wrapper(*args, **kwargs): - res = self.pool.apply(func, args, kwargs) + if not isMainThread(): # Call directly if not in main thread + return func(*args, **kwargs) + res = self.apply(func, args, kwargs) return res return wrapper From 495d695c5a23212342b082f7657bdaa23db3585a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:49:50 +0100 Subject: [PATCH 320/483] Fix threadpool apply and spawn when threadpool is full --- src/util/ThreadPool.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index f11ca677..87f3419f 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -34,6 +34,19 @@ class ThreadPool: return wrapper + def spawn(self, *args, **kwargs): + if not isMainThread() and not self.pool._semaphore.ready(): + # Avoid semaphore error when spawning from other thread and the pool is full + return main_loop.call(self.spawn, *args, **kwargs) + res = self.pool.spawn(*args, **kwargs) + return res + + def apply(self, func, args=(), kwargs={}): + t = self.spawn(func, *args, **kwargs) + if self.pool._apply_immediately(): + return main_loop.call(t.get) + else: + return t.get() lock_pool = gevent.threadpool.ThreadPool(50) main_thread_id = threading.current_thread().ident From b21895fa78a80dc5794be726654b8e46b966ea9d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:50:10 +0100 Subject: [PATCH 321/483] Kill threadpool properly --- src/util/ThreadPool.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 87f3419f..fb958c2a 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -48,6 +48,17 @@ class ThreadPool: else: return t.get() + def kill(self): + if self.pool is not None and self.pool.size > 0 and main_loop: + main_loop.call(self.pool.kill) + + del self.pool + self.pool = None + + def __del__(self): + self.kill() + + lock_pool = gevent.threadpool.ThreadPool(50) main_thread_id = threading.current_thread().ident From dfd55c3957b0faaeac56a1769cd2f477ee2ac57c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:50:38 +0100 Subject: [PATCH 322/483] Fix memory leak when using sleep in threads --- src/util/ThreadPool.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index fb958c2a..7ffeb3b2 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -154,6 +154,19 @@ class MainLoopCaller(): return res else: raise res + + +def patchSleep(): # Fix memory leak by using real sleep in threads + real_sleep = gevent.monkey.get_original("time", "sleep") + + def patched_sleep(seconds): + if isMainThread(): + gevent.sleep(seconds) + else: + real_sleep(seconds) + time.sleep = patched_sleep + + main_loop = MainLoopCaller() main_loop.start() patchSleep() From 5c1b34387cdd2dc51c38fa8e75596b612edcfe1b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:51:57 +0100 Subject: [PATCH 323/483] Noparallel multi thread compatibility --- src/util/Noparallel.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/util/Noparallel.py b/src/util/Noparallel.py index 541c8699..4a4a854d 100644 --- a/src/util/Noparallel.py +++ b/src/util/Noparallel.py @@ -2,19 +2,24 @@ import gevent import time from gevent.event import AsyncResult +from . import ThreadPool -class Noparallel(object): # Only allow function running once in same time + +class Noparallel: # Only allow function running once in same time def __init__(self, blocking=True, ignore_args=False, ignore_class=False, queue=False): self.threads = {} self.blocking = blocking # Blocking: Acts like normal function else thread returned - self.queue = queue + self.queue = queue # Execute again when blocking is done self.queued = False - self.ignore_args = ignore_args - self.ignore_class = ignore_class + self.ignore_args = ignore_args # Block does not depend on function call arguments + self.ignore_class = ignore_class # Block does not depeds on class instance def __call__(self, func): def wrapper(*args, **kwargs): + if not ThreadPool.isMainThread(): + return ThreadPool.main_loop.call(wrapper, *args, **kwargs) + if self.ignore_class: key = func # Unique key only by function and class object elif self.ignore_args: From f01d33583520221094fbd8401224f2996db29ad9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:52:13 +0100 Subject: [PATCH 324/483] Test noparallel multi thread compatibility --- src/Test/TestNoparallel.py | 82 +++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/src/Test/TestNoparallel.py b/src/Test/TestNoparallel.py index b48dd229..4d1a28f0 100644 --- a/src/Test/TestNoparallel.py +++ b/src/Test/TestNoparallel.py @@ -4,6 +4,16 @@ import gevent import pytest import util +from util import ThreadPool + + +@pytest.fixture(params=['gevent.spawn', 'thread_pool.spawn']) +def queue_spawn(request): + thread_pool = ThreadPool.ThreadPool(10) + if request.param == "gevent.spawn": + return gevent.spawn + else: + return thread_pool.spawn class ExampleClass(object): @@ -13,7 +23,7 @@ class ExampleClass(object): @util.Noparallel() def countBlocking(self, num=5): for i in range(1, num + 1): - time.sleep(0.01) + time.sleep(0.1) self.counted += 1 return "counted:%s" % i @@ -33,20 +43,20 @@ class ExampleClass(object): class TestNoparallel: - def testBlocking(self): + def testBlocking(self, queue_spawn): obj1 = ExampleClass() obj2 = ExampleClass() # Dont allow to call again until its running and wait until its running threads = [ - gevent.spawn(obj1.countBlocking), - gevent.spawn(obj1.countBlocking), - gevent.spawn(obj1.countBlocking), - gevent.spawn(obj2.countBlocking) + queue_spawn(obj1.countBlocking), + queue_spawn(obj1.countBlocking), + queue_spawn(obj1.countBlocking), + queue_spawn(obj2.countBlocking) ] assert obj2.countBlocking() == "counted:5" # The call is ignored as obj2.countBlocking already counting, but block until its finishes gevent.joinall(threads) - assert [thread.value for thread in threads] == ["counted:5", "counted:5", "counted:5", "counted:5"] # Check the return value for every call + assert [thread.value for thread in threads] == ["counted:5", "counted:5", "counted:5", "counted:5"] obj2.countBlocking() # Allow to call again as obj2.countBlocking finished assert obj1.counted == 5 @@ -54,7 +64,6 @@ class TestNoparallel: def testNoblocking(self): obj1 = ExampleClass() - obj2 = ExampleClass() thread1 = obj1.countNoblocking() thread2 = obj1.countNoblocking() # Ignored @@ -68,24 +77,24 @@ class TestNoparallel: obj1.countNoblocking().join() # Allow again and wait until finishes assert obj1.counted == 10 - def testQueue(self): + def testQueue(self, queue_spawn): obj1 = ExampleClass() - gevent.spawn(obj1.countQueue, num=10) - gevent.spawn(obj1.countQueue, num=10) - gevent.spawn(obj1.countQueue, num=10) + queue_spawn(obj1.countQueue, num=1) + queue_spawn(obj1.countQueue, num=1) + queue_spawn(obj1.countQueue, num=1) - time.sleep(3.0) - assert obj1.counted == 20 # No multi-queue supported + time.sleep(0.3) + assert obj1.counted == 2 # No multi-queue supported obj2 = ExampleClass() - gevent.spawn(obj2.countQueue, num=10) - gevent.spawn(obj2.countQueue, num=10) + queue_spawn(obj2.countQueue, num=10) + queue_spawn(obj2.countQueue, num=10) time.sleep(1.5) # Call 1 finished, call 2 still working assert 10 < obj2.counted < 20 - gevent.spawn(obj2.countQueue, num=10) + queue_spawn(obj2.countQueue, num=10) time.sleep(2.0) assert obj2.counted == 30 @@ -101,16 +110,16 @@ class TestNoparallel: gevent.joinall(threads) assert obj1.counted == 5 * 2 # Only called twice (no multi-queue allowed) - def testIgnoreClass(self): + def testIgnoreClass(self, queue_spawn): obj1 = ExampleClass() obj2 = ExampleClass() threads = [ - gevent.spawn(obj1.countQueue), - gevent.spawn(obj1.countQueue), - gevent.spawn(obj1.countQueue), - gevent.spawn(obj2.countQueue), - gevent.spawn(obj2.countQueue) + queue_spawn(obj1.countQueue), + queue_spawn(obj1.countQueue), + queue_spawn(obj1.countQueue), + queue_spawn(obj2.countQueue), + queue_spawn(obj2.countQueue) ] s = time.time() time.sleep(0.001) @@ -122,7 +131,7 @@ class TestNoparallel: taken = time.time() - s assert 1.2 > taken >= 1.0 # 2 * 0.5s count = ~1s - def testException(self): + def testException(self, queue_spawn): @util.Noparallel() def raiseException(): raise Exception("Test error!") @@ -130,3 +139,28 @@ class TestNoparallel: with pytest.raises(Exception) as err: raiseException() assert str(err.value) == "Test error!" + + with pytest.raises(Exception) as err: + queue_spawn(raiseException).get() + assert str(err.value) == "Test error!" + + def testMultithreadMix(self, queue_spawn): + obj1 = ExampleClass() + thread_pool = ThreadPool.ThreadPool(10) + + s = time.time() + t1 = queue_spawn(obj1.countBlocking, 5) + time.sleep(0.01) + t2 = thread_pool.spawn(obj1.countBlocking, 5) + time.sleep(0.01) + t3 = thread_pool.spawn(obj1.countBlocking, 5) + time.sleep(0.3) + t4 = gevent.spawn(obj1.countBlocking, 5) + threads = [t1, t2, t3, t4] + for thread in threads: + assert thread.get() == "counted:5" + + time_taken = time.time() - s + assert obj1.counted == 5 + assert 0.5 < time_taken < 0.7 + thread_pool.kill() From 61f1a741fc22147ad1de5137ba67c792c857c116 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:52:58 +0100 Subject: [PATCH 325/483] Test main loop caller --- src/Test/TestThreadPool.py | 112 +++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py index bb88b3bf..6c7f35e7 100644 --- a/src/Test/TestThreadPool.py +++ b/src/Test/TestThreadPool.py @@ -1,6 +1,8 @@ import time +import threading import gevent +import pytest from util import ThreadPool @@ -23,19 +25,18 @@ class TestThreadPool: return out threads = [] - for i in range(2): + for i in range(3): threads.append(gevent.spawn(blocker)) gevent.joinall(threads) - assert events == ["S"] * 2 + ["M"] * 2 + ["D"] * 2 + assert events == ["S"] * 3 + ["M"] * 3 + ["D"] * 3 res = blocker() assert res == 10000000 + pool.kill() def testLockBlockingSameThread(self): - from gevent.lock import Semaphore - - lock = Semaphore() + lock = ThreadPool.Lock() s = time.time() @@ -54,24 +55,113 @@ class TestThreadPool: def testLockBlockingDifferentThread(self): lock = ThreadPool.Lock() - s = time.time() - def locker(): lock.acquire(True) - time.sleep(1) + time.sleep(0.5) lock.release() - pool = gevent.threadpool.ThreadPool(10) - pool.spawn(locker) + pool = ThreadPool.ThreadPool(10) threads = [ pool.spawn(locker), + pool.spawn(locker), + gevent.spawn(locker), + pool.spawn(locker) ] time.sleep(0.1) + s = time.time() + lock.acquire(True, 5.0) unlock_taken = time.time() - s - assert 2.0 < unlock_taken < 2.5 + assert 1.8 < unlock_taken < 2.2 gevent.joinall(threads) + + def testMainLoopCallerThreadId(self): + main_thread_id = threading.current_thread().ident + pool = ThreadPool.ThreadPool(5) + + def getThreadId(*args, **kwargs): + return threading.current_thread().ident + + t = pool.spawn(getThreadId) + assert t.get() != main_thread_id + + t = pool.spawn(lambda: ThreadPool.main_loop.call(getThreadId)) + assert t.get() == main_thread_id + + def testMainLoopCallerGeventSpawn(self): + main_thread_id = threading.current_thread().ident + pool = ThreadPool.ThreadPool(5) + def waiter(): + time.sleep(1) + return threading.current_thread().ident + + def geventSpawner(): + event = ThreadPool.main_loop.call(gevent.spawn, waiter) + + with pytest.raises(Exception) as greenlet_err: + event.get() + assert str(greenlet_err.value) == "cannot switch to a different thread" + + waiter_thread_id = ThreadPool.main_loop.call(event.get) + return waiter_thread_id + + s = time.time() + waiter_thread_id = pool.apply(geventSpawner) + assert main_thread_id == waiter_thread_id + time_taken = time.time() - s + assert 0.9 < time_taken < 1.2 + + def testEvent(self): + pool = ThreadPool.ThreadPool(5) + event = ThreadPool.Event() + + def setter(): + time.sleep(1) + event.set("done!") + + def getter(): + return event.get() + + pool.spawn(setter) + t_gevent = gevent.spawn(getter) + t_pool = pool.spawn(getter) + s = time.time() + assert event.get() == "done!" + time_taken = time.time() - s + gevent.joinall([t_gevent, t_pool]) + + assert t_gevent.get() == "done!" + assert t_pool.get() == "done!" + + assert 0.9 < time_taken < 1.2 + + with pytest.raises(Exception) as err: + event.set("another result") + + assert "Event already has value" in str(err.value) + + def testMemoryLeak(self): + import gc + thread_objs_before = [id(obj) for obj in gc.get_objects() if "threadpool" in str(type(obj))] + + def worker(): + time.sleep(0.1) + return "ok" + + def poolTest(): + pool = ThreadPool.ThreadPool(5) + for i in range(20): + pool.spawn(worker) + pool.kill() + + for i in range(5): + poolTest() + new_thread_objs = [obj for obj in gc.get_objects() if "threadpool" in str(type(obj)) and id(obj) not in thread_objs_before] + #print("New objs:", new_thread_objs, "run:", num_run) + + # Make sure no threadpool object left behind + assert not new_thread_objs From f1b19f5fc7db616df08484603e041ae8d42553da Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 14:59:54 +0100 Subject: [PATCH 326/483] Fix DbQuery logging --- src/Ui/UiWebsocket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index a0d1b95b..ddbb6839 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -668,7 +668,7 @@ class UiWebsocket(object): try: res = self.site.storage.query(query, params) except Exception as err: # Response the error to client - self.log.error("DbQuery error: %s" % err) + self.log.error("DbQuery error: %s" % Debug.formatException(err)) return self.response(to, {"error": Debug.formatExceptionMessage(err)}) # Convert result to dict for row in res: @@ -686,7 +686,7 @@ class UiWebsocket(object): self.site.needFile(inner_path, priority=priority) body = self.site.storage.read(inner_path, "rb") except (Exception, gevent.Timeout) as err: - self.log.error("%s fileGet error: %s" % (inner_path, Debug.formatException(err))) + self.log.debug("%s fileGet error: %s" % (inner_path, Debug.formatException(err))) body = None if not body: From b4218934342d9057107b2b915f17343cd00b0a93 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:00:09 +0100 Subject: [PATCH 327/483] Return timer greenet --- src/util/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/helper.py b/src/util/helper.py index baaf313e..7377b943 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -234,7 +234,7 @@ def timerCaller(secs, func, *args, **kwargs): def timer(secs, func, *args, **kwargs): - gevent.spawn_later(secs, timerCaller, secs, func, *args, **kwargs) + return gevent.spawn_later(secs, timerCaller, secs, func, *args, **kwargs) def create_connection(address, timeout=None, source_address=None): From eac25caf28309da643d39cfba0a2d8bdd3b49f89 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:00:23 +0100 Subject: [PATCH 328/483] Log packing peer arrors as debug --- src/util/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/helper.py b/src/util/helper.py index 7377b943..018050ba 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -121,7 +121,7 @@ def packPeers(peers): ip_type = getIpType(peer.ip) packed_peers[ip_type].append(peer.packMyAddress()) except Exception: - logging.error("Error packing peer address: %s" % peer) + logging.debug("Error packing peer address: %s" % peer) return packed_peers From 2019093431bbfcd429601a32e377786d06b67920 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:01:15 +0100 Subject: [PATCH 329/483] Fix testing on slower storage --- src/Test/TestSiteDownload.py | 50 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index 34783238..650d424b 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -3,6 +3,7 @@ import time import pytest import mock import gevent +import gevent.event import os from Connection import ConnectionServer @@ -33,7 +34,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) assert site_temp.storage.isFile("content.json") @@ -52,7 +53,7 @@ class TestSiteDownload: with Spy.Spy(FileRequest, "route") as requests: site.publish() time.sleep(0.1) - site_temp.download(blind_includes=True).join(timeout=5) # Wait for download + assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download assert "streamFile" not in [req[1] for req in requests] content = site_temp.storage.loadJson("content.json") @@ -84,7 +85,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) assert site_temp.settings["optional_downloaded"] == 0 @@ -108,7 +109,7 @@ class TestSiteDownload: with Spy.Spy(FileRequest, "route") as requests: site.publish() time.sleep(0.1) - site_temp.download(blind_includes=True).join(timeout=5) # Wait for download + assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download assert "streamFile" not in [req[1] for req in requests] content = site_temp.storage.loadJson("content.json") @@ -138,7 +139,7 @@ class TestSiteDownload: # Download normally site_temp.addPeer(file_server.ip, 1544) - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] assert not bad_files @@ -162,7 +163,7 @@ class TestSiteDownload: assert not "archived" in site_temp.content_manager.contents["data/users/content.json"]["user_contents"] site.publish() time.sleep(0.1) - site_temp.download(blind_includes=True).join(timeout=5) # Wait for download + assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download # The archived content should disappear from remote client assert "archived" in site_temp.content_manager.contents["data/users/content.json"]["user_contents"] @@ -186,7 +187,7 @@ class TestSiteDownload: # Download normally site_temp.addPeer(file_server.ip, 1544) - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] assert not bad_files @@ -211,7 +212,7 @@ class TestSiteDownload: assert not "archived_before" in site_temp.content_manager.contents["data/users/content.json"]["user_contents"] site.publish() time.sleep(0.1) - site_temp.download(blind_includes=True).join(timeout=5) # Wait for download + assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download # The archived content should disappear from remote client assert "archived_before" in site_temp.content_manager.contents["data/users/content.json"]["user_contents"] @@ -238,7 +239,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) # Download optional data/optional.txt site.storage.verifyFiles(quick_check=True) # Find what optional files we have @@ -303,7 +304,7 @@ class TestSiteDownload: # Download normal files site_temp.log.info("Start Downloading site") - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) # Download optional data/optional.txt optional_file_info = site_temp.content_manager.getFileInfo("data/optional.txt") @@ -333,7 +334,7 @@ class TestSiteDownload: assert site_temp.storage.deleteFiles() file_server_full.stop() [connection.close() for connection in file_server.connections] - site_full.content_manager.contents.db.close() + site_full.content_manager.contents.db.close("FindOptional test end") def testUpdate(self, file_server, site, site_temp): assert site.storage.directory == config.data_dir + "/" + site.address @@ -356,7 +357,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) # Update file data_original = site.storage.open("data/data.json").read() @@ -374,7 +375,7 @@ class TestSiteDownload: site.content_manager.sign("content.json", privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv") site.publish() time.sleep(0.1) - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) assert len([request for request in requests if request[1] in ("getFile", "streamFile")]) == 1 assert site_temp.storage.open("data/data.json").read() == data_new @@ -405,9 +406,12 @@ class TestSiteDownload: site.log.info("Publish new data.json with patch") with Spy.Spy(FileRequest, "route") as requests: site.content_manager.sign("content.json", privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv") + + event_done = gevent.event.AsyncResult() site.publish(diffs=diffs) - site_temp.download(blind_includes=True).join(timeout=5) - assert len([request for request in requests if request[1] in ("getFile", "streamFile")]) == 0 + time.sleep(0.1) + assert site_temp.download(blind_includes=True).get(timeout=10) + assert [request for request in requests if request[1] in ("getFile", "streamFile")] == [] assert site_temp.storage.open("data/data.json").read() == data_new @@ -428,7 +432,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) # Update file data_original = site.storage.open("data/data.json").read() @@ -447,7 +451,7 @@ class TestSiteDownload: assert "data/data.json" in diffs content_json = site.storage.loadJson("content.json") - content_json["title"] = "BigZeroBlog" * 1024 * 10 + content_json["description"] = "BigZeroBlog" * 1024 * 10 site.storage.writeJson("content.json", content_json) site.content_manager.loadContent("content.json", force=True) @@ -457,7 +461,8 @@ class TestSiteDownload: site.content_manager.sign("content.json", privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv") assert site.storage.getSize("content.json") > 10 * 1024 # Make it a big content.json site.publish(diffs=diffs) - site_temp.download(blind_includes=True).join(timeout=5) + time.sleep(0.1) + assert site_temp.download(blind_includes=True).get(timeout=10) file_requests = [request for request in requests if request[1] in ("getFile", "streamFile")] assert len(file_requests) == 1 @@ -479,7 +484,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) site_temp.settings["size_limit"] = int(20 * 1024 *1024) site_temp.saveSettings() @@ -503,8 +508,9 @@ class TestSiteDownload: with Spy.Spy(FileRequest, "route") as requests: site.content_manager.sign("content.json", privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv") assert site.storage.getSize("content.json") > 10 * 1024 * 1024 # verify it over 10MB + time.sleep(0.1) site.publish(diffs=diffs) - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) assert site_temp.storage.getSize("content.json") < site_temp.getSizeLimit() * 1024 * 1024 assert site_temp.storage.open("content.json").read() == site.storage.open("content.json").read() @@ -525,7 +531,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) - site_temp.download(blind_includes=True).join(timeout=5) + assert site_temp.download(blind_includes=True).get(timeout=10) site.storage.write("data/img/árvíztűrő.png", b"test") @@ -539,7 +545,7 @@ class TestSiteDownload: with Spy.Spy(FileRequest, "route") as requests: site.publish() time.sleep(0.1) - site_temp.download(blind_includes=True).join(timeout=5) # Wait for download + assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download assert len([req[1] for req in requests if req[1] == "streamFile"]) == 1 content = site_temp.storage.loadJson("content.json") From 9b1f6337c3e2e02b308d70202cc4fcdf7c8b271b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:02:04 +0100 Subject: [PATCH 330/483] Wait for cursor finish on db close --- src/Db/Db.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index 22a10516..9f372fc3 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -5,7 +5,10 @@ import logging import re import os import atexit +import threading import sys +import weakref +import errno import gevent @@ -71,6 +74,7 @@ class Db(object): self.schema["version"] = self.schema.get("version", 1) self.conn = None self.cur = None + self.cursors = weakref.WeakSet() self.id = next_db_id next_db_id += 1 self.progress_sleeping = False @@ -121,10 +125,16 @@ class Db(object): "Connected to %s in %.3fs (opened: %s, sqlite version: %s)..." % (self.db_path, time.time() - s, len(opened_dbs), sqlite3.version) ) + self.log.debug("Connect by thread: %s" % threading.current_thread().ident) self.log.debug("Connect called by %s" % Debug.formatStack()) finally: self.connect_lock.release() + def getConn(self): + if not self.conn: + self.connect() + return self.conn + def progress(self, *args, **kwargs): self.progress_sleeping = True time.sleep(0.001) @@ -199,6 +209,7 @@ class Db(object): def close(self, reason="Unknown"): if not self.conn: return False + self.connect_lock.acquire() s = time.time() if self.delayed_queue: self.processDelayed() @@ -207,10 +218,19 @@ class Db(object): self.need_commit = False self.commit("Closing: %s" % reason) self.log.debug("Close called by %s" % Debug.formatStack()) + for i in range(10): + if len(self.cursors) == 0: + break + self.log.debug("Pending cursors: %s" % len(self.cursors)) + time.sleep(0.1 * i) + if len(self.cursors): + self.log.debug("Killing cursors: %s" % len(self.cursors)) + self.conn.interrupt() + if self.cur: self.cur.close() if self.conn: - self.conn.close() + ThreadPool.main_loop.call(self.conn.close) self.conn = None self.cur = None self.log.debug("%s closed (reason: %s) in %.3fs, opened: %s" % (self.db_path, reason, time.time() - s, len(opened_dbs))) From 98c98fbac788bb7c0db5d762eeff941fecc1c679 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:02:18 +0100 Subject: [PATCH 331/483] Thread safe method to create directory for db --- src/Db/Db.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index 9f372fc3..b7cfd501 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -108,9 +108,12 @@ class Db(object): if self not in opened_dbs: opened_dbs.append(self) s = time.time() - if not os.path.isdir(self.db_dir): # Directory not exist yet + try: # Directory not exist yet os.makedirs(self.db_dir) self.log.debug("Created Db path: %s" % self.db_dir) + except OSError as err: + if err.errno != errno.EEXIST: + raise err if not os.path.isfile(self.db_path): self.log.debug("Db file not exist yet: %s" % self.db_path) self.conn = sqlite3.connect(self.db_path, isolation_level="DEFERRED", check_same_thread=False) From 2778b17f8d689d76444daa39e2b385fad794d194 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:02:39 +0100 Subject: [PATCH 332/483] Ignore trayicon destroy errors --- plugins/Trayicon/TrayiconPlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index bae1e713..e9b12a26 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -36,7 +36,10 @@ class ActionsPlugin(object): @atexit.register def hideIcon(): - icon.die() + try: + icon.die() + except Exception as err: + print("Error removing trayicon: %s" % err) ui_ip = config.ui_ip if config.ui_ip != "*" else "127.0.0.1" From 9d777951dd66147caed82ca05e337866ec302a7f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:03:04 +0100 Subject: [PATCH 333/483] Fix console tabs display gitch on edge --- plugins/Sidebar/media/Console.css | 2 +- plugins/Sidebar/media/all.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/media/Console.css b/plugins/Sidebar/media/Console.css index 2532582f..127d15bf 100644 --- a/plugins/Sidebar/media/Console.css +++ b/plugins/Sidebar/media/Console.css @@ -4,7 +4,7 @@ .console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; box-sizing: border-box; letter-spacing: 0.5px;} .console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; } .console-tabs { - background-color: #41193fad; position: relative; margin-right: 17px; backdrop-filter: blur(2px); + background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/ box-shadow: -30px 0px 45px #7d2463; background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473; } .console-tabs a { diff --git a/plugins/Sidebar/media/all.css b/plugins/Sidebar/media/all.css index c769ebfa..4921d57f 100644 --- a/plugins/Sidebar/media/all.css +++ b/plugins/Sidebar/media/all.css @@ -8,7 +8,7 @@ .console-top { color: white; font-family: Consolas, monospace; font-size: 11px; line-height: 20px; height: 100%; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box ; letter-spacing: 0.5px;} .console-text { overflow-y: scroll; height: calc(100% - 10px); color: #DDD; padding: 5px; margin-top: -36px; overflow-wrap: break-word; } .console-tabs { - background-color: #41193fad; position: relative; margin-right: 17px; backdrop-filter: blur(2px); + background-color: #41193fad; position: relative; margin-right: 17px; /*backdrop-filter: blur(2px);*/ -webkit-box-shadow: -30px 0px 45px #7d2463; -moz-box-shadow: -30px 0px 45px #7d2463; -o-box-shadow: -30px 0px 45px #7d2463; -ms-box-shadow: -30px 0px 45px #7d2463; box-shadow: -30px 0px 45px #7d2463 ; background: -webkit-linear-gradient(-75deg, #591a48ed, #70305e66);background: -moz-linear-gradient(-75deg, #591a48ed, #70305e66);background: -o-linear-gradient(-75deg, #591a48ed, #70305e66);background: -ms-linear-gradient(-75deg, #591a48ed, #70305e66);background: linear-gradient(-75deg, #591a48ed, #70305e66); border-bottom: 1px solid #792e6473; } .console-tabs a { From 8c51e81a0bf522b548653785b128e8b17e154a20 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:05:21 +0100 Subject: [PATCH 334/483] Fix double opening of dbs --- src/Site/SiteStorage.py | 64 ++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index c67df89a..1574694b 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -54,15 +54,14 @@ class SiteStorage(object): return False # Create new databaseobject with the site's schema - @util.Noparallel() def openDb(self, close_idle=False): schema = self.getDbSchema() db_path = self.getPath(schema["db_file"]) return Db(schema, db_path, close_idle=close_idle) - def closeDb(self): + def closeDb(self, reason="Unknown (SiteStorage)"): if self.db: - self.db.close() + self.db.close(reason) self.event_db_busy = None self.db = None @@ -73,37 +72,48 @@ class SiteStorage(object): raise Exception("dbschema.json is not a valid JSON: %s" % err) return schema - # Return db class - def getDb(self): - if not self.db: - self.log.debug("No database, waiting for dbschema.json...") - self.site.needFile("dbschema.json", priority=3) - self.has_db = self.isFile("dbschema.json") # Recheck if dbschema exist - if self.has_db: - schema = self.getDbSchema() - db_path = self.getPath(schema["db_file"]) - if not os.path.isfile(db_path) or os.path.getsize(db_path) == 0: - try: - self.rebuildDb() - except Exception as err: - self.log.error(err) - pass - - if self.db: - self.db.close() - self.db = self.openDb(close_idle=True) + def loadDb(self): + self.log.debug("No database, waiting for dbschema.json...") + self.site.needFile("dbschema.json", priority=3) + self.log.debug("Got dbschema.json") + self.has_db = self.isFile("dbschema.json") # Recheck if dbschema exist + if self.has_db: + schema = self.getDbSchema() + db_path = self.getPath(schema["db_file"]) + if not os.path.isfile(db_path) or os.path.getsize(db_path) == 0: try: - changed_tables = self.db.checkTables() - if changed_tables: - self.rebuildDb(delete_db=False) # TODO: only update the changed table datas - except sqlite3.OperationalError: + self.rebuildDb(reason="Missing database") + except Exception as err: + self.log.error(err) pass + if self.db: + self.db.close("Gettig new db for SiteStorage") + self.db = self.openDb(close_idle=True) + try: + changed_tables = self.db.checkTables() + if changed_tables: + self.rebuildDb(delete_db=False, reason="Changed tables") # TODO: only update the changed table datas + except sqlite3.OperationalError: + pass + + # Return db class + @util.Noparallel() + def getDb(self): + if self.event_db_busy: # Db not ready for queries + self.log.debug("Wating for db...") + self.event_db_busy.get() # Wait for event + if not self.db: + self.loadDb() return self.db def updateDbFile(self, inner_path, file=None, cur=None): path = self.getPath(inner_path) - return self.getDb().updateJson(path, file, cur) + if cur: + db = cur.db + else: + db = self.getDb() + return db.updateJson(path, file, cur) # Return possible db files for the site @thread_pool_fs_read.wrap From ac45217816227090cc26fad3d44ba923fead3cce Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:05:59 +0100 Subject: [PATCH 335/483] Add reason for db close and rebuilds --- src/Site/SiteStorage.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 1574694b..d8be6900 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -142,9 +142,9 @@ class SiteStorage(object): # Rebuild sql cache @util.Noparallel() - def rebuildDb(self, delete_db=True): - self.log.info("Rebuilding db...") @thread_pool_fs_batch.wrap + def rebuildDb(self, delete_db=True, reason="Unknown"): + self.log.info("Rebuilding db (reason: %s)..." % reason) self.has_db = self.isFile("dbschema.json") if not self.has_db: return False @@ -153,7 +153,7 @@ class SiteStorage(object): db_path = self.getPath(schema["db_file"]) if os.path.isfile(db_path) and delete_db: if self.db: - self.closeDb() # Close db if open + self.closeDb("rebuilding") # Close db if open time.sleep(0.5) self.log.info("Deleting %s" % db_path) try: @@ -165,7 +165,7 @@ class SiteStorage(object): self.db = self.openDb() self.event_db_busy = gevent.event.AsyncResult() - self.log.info("Creating tables...") + self.log.info("Rebuild: Creating tables...") # raise DbTableError if not valid self.db.checkTables() @@ -173,7 +173,7 @@ class SiteStorage(object): cur = self.db.getCursor() cur.logging = False s = time.time() - self.log.info("Getting db files...") + self.log.info("Rebuild: Getting db files...") db_files = list(self.getDbFiles()) num_imported = 0 num_total = len(db_files) @@ -212,9 +212,10 @@ class SiteStorage(object): num_imported, num_total, num_error ), "rebuild", 100 ) - self.log.info("Imported %s data file in %.3fs" % (num_imported, time.time() - s)) + self.log.info("Rebuild: Imported %s data file in %.3fs" % (num_imported, time.time() - s)) self.event_db_busy.set(True) # Event done, notify waiters self.event_db_busy = None # Clear event + self.db.commit("Rebuilt") return True @@ -232,7 +233,7 @@ class SiteStorage(object): if err.__class__.__name__ == "DatabaseError": self.log.error("Database error: %s, query: %s, try to rebuilding it..." % (err, query)) try: - self.rebuildDb() + self.rebuildDb(reason="Query error") except sqlite3.OperationalError: pass res = self.db.cur.execute(query, params) @@ -356,8 +357,8 @@ class SiteStorage(object): self.has_db = self.isFile("dbschema.json") # Reopen DB to check changes if self.has_db: - self.closeDb() - self.getDb() + self.closeDb("New dbschema") + gevent.spawn(self.getDb) elif not config.disable_db and should_load_to_db and self.has_db: # Load json file to db if config.verbose: self.log.debug("Loading json file to db: %s (file: %s)" % (inner_path, file)) @@ -365,7 +366,7 @@ class SiteStorage(object): self.updateDbFile(inner_path, file) except Exception as err: self.log.error("Json %s load error: %s" % (inner_path, Debug.formatException(err))) - self.closeDb() + self.closeDb("Json load error") # Load and parse json file @thread_pool_fs_read.wrap @@ -567,7 +568,7 @@ class SiteStorage(object): if self.isFile("dbschema.json"): self.log.debug("Deleting db file...") - self.closeDb() + self.closeDb("Deleting site") self.has_db = False try: schema = self.loadJson("dbschema.json") From f7ee6744afd93edccd7c1d6e48e1ddf57ab3f3a9 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:06:36 +0100 Subject: [PATCH 336/483] Db busy event waited in getDb --- src/Site/SiteStorage.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index d8be6900..f46c32a1 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -224,9 +224,6 @@ class SiteStorage(object): if not query.strip().upper().startswith("SELECT"): raise Exception("Only SELECT query supported") - if self.event_db_busy: # Db not ready for queries - self.log.debug("Wating for db...") - self.event_db_busy.get() # Wait for event try: res = self.getDb().execute(query, params) except sqlite3.DatabaseError as err: From 23b3cd398637c03aedd6679439cdeda0d892cafc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:07:00 +0100 Subject: [PATCH 337/483] Better rebuild log message --- src/Site/SiteStorage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index f46c32a1..56ed32bf 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -179,7 +179,7 @@ class SiteStorage(object): num_total = len(db_files) num_error = 0 - self.log.info("Importing data...") + self.log.info("Rebuild: Importing data...") try: if num_total > 100: self.site.messageWebsocket( From f3665b172f8f631073ac9e54c17ebab623d37cd2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:07:32 +0100 Subject: [PATCH 338/483] Avoid unnecessary pool call --- src/Site/SiteStorage.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 56ed32bf..6e5f3541 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -372,7 +372,6 @@ class SiteStorage(object): return json.load(file) # Write formatted json file - @thread_pool_fs_write.wrap def writeJson(self, inner_path, data): # Write to disk self.write(inner_path, helper.jsonDumps(data).encode("utf8")) From e7e8e59c1ed50f23b64510ea01b69f40d7049776 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:08:42 +0100 Subject: [PATCH 339/483] Rev4353 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 2db47801..6fcdcccf 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4327 + self.rev = 4353 self.argv = argv self.action = None self.test_parser = None From 1ad97a669665158469ce1023af230955d366b781 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 15:16:23 +0100 Subject: [PATCH 340/483] Run internal test on CI --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1f81b60a..c61cef4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ before_script: sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi script: + - python zeronet.py test benchmark --num_multipler 10 - python -m pytest -x plugins/CryptMessage/Test - python -m pytest -x plugins/Bigfile/Test - python -m pytest -x plugins/AnnounceLocal/Test From afe0d82f18cdb94642976816076ae2c28403490a Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Tue, 17 Dec 2019 15:25:46 +0100 Subject: [PATCH 341/483] Run only selected benchmark tests --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c61cef4c..5dc5cf9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,9 @@ before_script: sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi script: - - python zeronet.py test benchmark --num_multipler 10 + - python zeronet.py test benchmark --num_multipler 5 --filter crypt + - python zeronet.py test benchmark --num_multipler 5 --filter msgpack + - python zeronet.py test benchmark --num_multipler 10 --filter verify - python -m pytest -x plugins/CryptMessage/Test - python -m pytest -x plugins/Bigfile/Test - python -m pytest -x plugins/AnnounceLocal/Test From 909967629b2433eb6c006117255ef07d5a00ab23 Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Tue, 17 Dec 2019 15:34:43 +0100 Subject: [PATCH 342/483] Remove incompatible tests --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5dc5cf9a..1f81b60a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,9 +24,6 @@ before_script: sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi script: - - python zeronet.py test benchmark --num_multipler 5 --filter crypt - - python zeronet.py test benchmark --num_multipler 5 --filter msgpack - - python zeronet.py test benchmark --num_multipler 10 --filter verify - python -m pytest -x plugins/CryptMessage/Test - python -m pytest -x plugins/Bigfile/Test - python -m pytest -x plugins/AnnounceLocal/Test From 87fc8ced5e73f00dbc346cc41f4d59bc8a82cc7a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 16:06:13 +0100 Subject: [PATCH 343/483] Accept only my exception when testing Noparallel --- src/Test/TestNoparallel.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Test/TestNoparallel.py b/src/Test/TestNoparallel.py index 4d1a28f0..d80cc5fb 100644 --- a/src/Test/TestNoparallel.py +++ b/src/Test/TestNoparallel.py @@ -132,15 +132,18 @@ class TestNoparallel: assert 1.2 > taken >= 1.0 # 2 * 0.5s count = ~1s def testException(self, queue_spawn): + class MyException(Exception): + pass + @util.Noparallel() def raiseException(): - raise Exception("Test error!") + raise MyException("Test error!") - with pytest.raises(Exception) as err: + with pytest.raises(MyException) as err: raiseException() assert str(err.value) == "Test error!" - with pytest.raises(Exception) as err: + with pytest.raises(MyException) as err: queue_spawn(raiseException).get() assert str(err.value) == "Test error!" From 77869830c5e7715424fc2781354e4feed882b606 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 20:36:52 +0100 Subject: [PATCH 344/483] Fix shutdown hang --- src/util/ThreadPool.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 7ffeb3b2..ea412ed6 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -50,7 +50,7 @@ class ThreadPool: def kill(self): if self.pool is not None and self.pool.size > 0 and main_loop: - main_loop.call(self.pool.kill) + gevent.spawn(main_loop.call, self.pool.kill) del self.pool self.pool = None @@ -120,6 +120,7 @@ class MainLoopCaller(): self.pool = gevent.threadpool.ThreadPool(1) self.num_direct = 0 + self.running = True def caller(self, func, args, kwargs, event_done): try: @@ -133,13 +134,14 @@ class MainLoopCaller(): time.sleep(0.001) def run(self): - while 1: + while self.running: if self.queue_call.qsize() == 0: # Get queue in new thread to avoid gevent blocking func, args, kwargs, event_done = self.pool.apply(self.queue_call.get) else: func, args, kwargs, event_done = self.queue_call.get() gevent.spawn(self.caller, func, args, kwargs, event_done) del func, args, kwargs, event_done + self.running = False def call(self, func, *args, **kwargs): if threading.current_thread().ident == main_thread_id: From fd43aa61ef7d5a32ade741b73e55e5a7c48bf9f0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 20:37:32 +0100 Subject: [PATCH 345/483] Current gevent in PyPI is not fully compatible with Python3.8 --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c063e3f..3a542131 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -gevent>=1.1.0 +gevent>=1.1.0; python_version < "3.8" +https://github.com/gevent/gevent/archive/master.zip; python_version >= "3.8" msgpack>=0.4.4 base58 merkletools From 24b8cdf87aa865637327d4c8cb5a106626e11c90 Mon Sep 17 00:00:00 2001 From: Hamid reza Zaefarani Date: Tue, 17 Dec 2019 23:15:51 +0330 Subject: [PATCH 346/483] Add Farsi (Persian) Translation to ZeroNet Persian Translation of ZeroNet Site --- src/Translate/languages/fa,json | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/Translate/languages/fa,json diff --git a/src/Translate/languages/fa,json b/src/Translate/languages/fa,json new file mode 100644 index 00000000..e644247a --- /dev/null +++ b/src/Translate/languages/fa,json @@ -0,0 +1,50 @@ +{ + "Congratulations, your port {0} is opened.
You are a full member of the ZeroNet network!": "تبریک، درگاه {0} شما باز شده است.
شما یک عضو تمام شبکه ZeroNet هستید!", + "Tor mode active, every connection using Onion route.": "حالت Tor فعال است، هر ارتباط از مسیریابی پیاز (Onion) استفاده می‌کند.", + "Successfully started Tor onion hidden services.": "خدمات پنهان پیاز (Onion) Tor با موفقیت راه‌اندازی شد.", + "Unable to start hidden services, please check your config.": "قادر به راه‌اندازی خدمات پنهان نیستیم، لطفا تنظیمات خود را بررسی نمایید.", + "For faster connections open {0} port on your router.": "برای ارتباطات سریعتر درگاه {0} را بر روی مسیریاب (روتر) خود باز نمایید.", + "Your connection is restricted. Please, open {0} port on your router": "ارتباط شما محدود‌شده است. لطفا درگاه {0} را در مسیریاب (روتر) خود باز نمایید", + "or configure Tor to become a full member of the ZeroNet network.": "یا پیکربندی Tor را انجام دهید تا به یک عضو تمام شبکه ZeroNet تبدیل شوید.", + + "Select account you want to use in this site:": "حسابی را که می‌خواهید در این سایت استفاده کنید، انتخاب کنید:", + "currently selected": "در حال حاضر انتخاب‌شده", + "Unique to site": "مختص به سایت", + + "Content signing failed": "امضای محتوا با شکست مواجه شد", + "Content publish queued for {0:.0f} seconds.": "محتوا در صف انتشار با {0:.0f} ثانیه تاخیر قرار گرفت.", + "Content published to {0} peers.": "محتوا برای {0} تعداد همتا انتشار یافت.", + "No peers found, but your content is ready to access.": "همتایی یافت نشد، اما محتوای شما آماده دسترسی است.", + "Your network connection is restricted. Please, open {0} port": "ارتباط شبکه شما محدود‌شده است. لطفا درگاه {0} را", + "on your router to make your site accessible for everyone.": "در مسیریاب (روتر) خود باز کنید تا سایت خود را برای همه در دسترس قرار دهید.", + "Content publish failed.": "انتشار محتوا موفق نبود.", + "This file still in sync, if you write it now, then the previous content may be lost.": "این فایل همچنان همگام است، اگز شما آن را بنویسید، ممکن است محتوای قبلی از‌بین رود.", + "Write content anyway": "در هر صورت محتوا را بنویس", + "New certificate added:": "گواهی جدیدی افزوده شد:", + "You current certificate:": "گواهی فعلی شما:", + "Change it to {auth_type}/{auth_user_name}@{domain}": "تغییرش بده به {auth_type}/{auth_user_name}@{domain}", + "Certificate changed to: {auth_type}/{auth_user_name}@{domain}.": "گواهینامه به: {auth_type}/{auth_user_name}@{domain} تغییر پیدا کرد.", + "Site cloned": "سایت همسان‌سازی شد", + + "You have successfully changed the web interface's language!": "شما با موفقیت زبان رابط وب را تغییر دادید!", + "Due to the browser's caching, the full transformation could take some minute.": "به دلیل ذخیره‌سازی در مرور‌گر، امکان دارد تغییر شکل کامل چند دقیقه طول بکشد.", + + "Connection with UiServer Websocket was lost. Reconnecting...": "اتصال با UiServer Websocket قطع شد. اتصال دوباره...", + "Connection with UiServer Websocket recovered.": "ارتباط با UiServer Websocket دوباره بر‌قرار شد.", + "UiServer Websocket error, please reload the page.": "خطای UiServer Websocket, لطفا صفحه را دوباره بارگیری کنید.", + "   Connecting...": "   برقراری ارتباط...", + "Site size: ": "حجم سایت: ", + "MB is larger than default allowed ": "MB بیشتر از پیش‌فرض مجاز است ", + "Open site and set size limit to \" + site_info.next_size_limit + \"MB": "سایت را باز کرده و محدوده حجم را به \" + site_info.next_size_limit + \"MB تنظیم کن", + " files needs to be downloaded": " فایل‌هایی که نیاز است، دانلود شوند", + " downloaded": " دانلود شد", + " download failed": " دانلود موفق نبود", + "Peers found: ": "چند همتا یافت شد: ", + "No peers found": "همتایی یافت نشد", + "Running out of size limit (": "عبور کرده از محدوده حجم (", + "Set limit to \" + site_info.next_size_limit + \"MB": "محدوده را به \" + site_info.next_size_limit + \"MB تنظیم کن", + "Site size limit changed to {0}MB": "محدوده حجم سایت به {0}MB تغییر کرد", + " New version of this page has just released.
Reload to see the modified content.": " نسخه جدیدی از این صفحه منتشر شده است.
برای مشاهده محتوای تغییر‌یافته دوباره بارگیری نمایید.", + "This site requests permission:": "این سایت درخواست مجوز می‌کند:", + "_(Accept)": "_(پذیرفتن)" +} From a7c26f893f99c32f57add890bc5ca6c78b6b03a6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 20:46:29 +0100 Subject: [PATCH 347/483] Rev4354 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 6fcdcccf..bebcb4c9 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4353 + self.rev = 4354 self.argv = argv self.action = None self.test_parser = None From d4b6f7974683ba27bb7e1133dc85b21b5f845906 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 21:01:04 +0100 Subject: [PATCH 348/483] Display logs after failure --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 1f81b60a..49700383 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,8 @@ script: after_success: - codecov - coveralls --rcfile=src/Test/coverage.ini +after_failure: + - cat log/*.log notifications: email: recipients: From abee87bbec1c480fcae789dc5c08c3421a088cb5 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 21:02:48 +0100 Subject: [PATCH 349/483] Wait for threadpool kill with 1s timeout to fix memory leak test --- src/util/ThreadPool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index ea412ed6..0e1757ea 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -50,7 +50,7 @@ class ThreadPool: def kill(self): if self.pool is not None and self.pool.size > 0 and main_loop: - gevent.spawn(main_loop.call, self.pool.kill) + main_loop.call(lambda: gevent.spawn(self.pool.kill).join(timeout=1)) del self.pool self.pool = None From 9c08e41b9e103fb0e590345e27316a43df5c8e45 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 21:03:01 +0100 Subject: [PATCH 350/483] Rev4355 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index bebcb4c9..6fbf9722 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4354 + self.rev = 4355 self.argv = argv self.action = None self.test_parser = None From 93d2ee65fe29217047165d099d9581b04b8f07a4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 17 Dec 2019 21:30:01 +0100 Subject: [PATCH 351/483] Validate json files in src and plugins dir --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 49700383..c1b935da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,8 @@ script: - python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini - mv plugins/disabled-Multiuser plugins/Multiuser && python -m pytest -x plugins/Multiuser/Test - mv plugins/disabled-Bootstrapper plugins/Bootstrapper && python -m pytest -x plugins/Bootstrapper/Test + - find src -name "*.json" | xargs -n 1 python -c "import json, sys; print(sys.argv[1]); json.load(open(sys.argv[1]))" + - find plugins -name "*.json" | xargs -n 1 python -c "import json, sys; print(sys.argv[1]); json.load(open(sys.argv[1]))" - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ after_success: - codecov From cfaaaf57ec3d248dcc70a210144aa8f64b0cc109 Mon Sep 17 00:00:00 2001 From: Decentralized Authority <59014492+decentralizedauthority@users.noreply.github.com> Date: Wed, 18 Dec 2019 10:59:48 +0000 Subject: [PATCH 352/483] Rename fa,json to fa.json --- src/Translate/languages/{fa,json => fa.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Translate/languages/{fa,json => fa.json} (100%) diff --git a/src/Translate/languages/fa,json b/src/Translate/languages/fa.json similarity index 100% rename from src/Translate/languages/fa,json rename to src/Translate/languages/fa.json From 1fe7127082aa7bfbaf7a34bb50a10a1a97bc8ca4 Mon Sep 17 00:00:00 2001 From: Hamid reza Zaefarani Date: Wed, 18 Dec 2019 14:35:54 +0330 Subject: [PATCH 353/483] Rename fa,json to fa.json --- src/Translate/languages/{fa,json => fa.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Translate/languages/{fa,json => fa.json} (100%) diff --git a/src/Translate/languages/fa,json b/src/Translate/languages/fa.json similarity index 100% rename from src/Translate/languages/fa,json rename to src/Translate/languages/fa.json From c08d266822d252afe2713e7f9fe281e9251f7883 Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Wed, 18 Dec 2019 14:55:37 +0100 Subject: [PATCH 354/483] Change to more simple way to create new site --- README.md | 44 ++++---------------------------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 151af389..2df14e71 100644 --- a/README.md +++ b/README.md @@ -103,49 +103,13 @@ Decentralized websites using Bitcoin crypto and the BitTorrent network - https:/ ## How can I create a ZeroNet site? -Shut down zeronet if you are running it already - -```bash -$ zeronet.py siteCreate -... -- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq -- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -... -- Site created! -$ zeronet.py -... -``` - -Congratulations, you're finished! Now anyone can access your site using -`http://127.0.0.1:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2` + * Click on **⋮** > **"Create new, empty site"** menu item on the site [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D). + * You will be **redirected** to a completely new site that is only modifiable by you! + * You can find and modify your site's content in **data/[yoursiteaddress]** directory + * After the modifications open your site, drag the topright "0" button to left, then press **sign** and **publish** buttons on the bottom Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_development/getting_started/) - -## How can I modify a ZeroNet site? - -* Modify files located in data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 directory. - After you're finished: - -```bash -$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2... -Private key (input hidden): -``` - -* Enter the private key you got when you created the site, then: - -```bash -$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -... -Site:13DNDk..bhC2 Publishing to 3/10 peers... -Site:13DNDk..bhC2 Successfuly published to 3 peers -- Serving files.... -``` - -* That's it! You've successfully signed and published your modifications. - - ## Help keep this project alive - Bitcoin: 1QDhxQ6PraUZa21ET5fYUCPgdrwBomnFgX From c0639fef751704b01e31efcf50ed6df3ca05f566 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Dec 2019 15:23:16 +0100 Subject: [PATCH 355/483] Lock task adding to avoid race condition when getFileInfo switches --- src/Worker/WorkerManager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 252d34ca..2a71b88e 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -20,6 +20,7 @@ class WorkerManager(object): self.workers = {} # Key: ip:port, Value: Worker.Worker self.tasks = WorkerTaskManager() self.next_task_id = 1 + self.lock_add_task = gevent.lock.Semaphore(1) # {"id": 1, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "optional_hash_id": None, # "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0, "failed": peer_ids, "lock": None or gevent.lock.RLock} self.started_task_num = 0 # Last added task num @@ -198,7 +199,6 @@ class WorkerManager(object): if type(peers) is set: peers = list(peers) - # Sort by ping peers.sort(key=lambda peer: peer.connection.last_ping_delay if peer.connection and peer.connection.last_ping_delay and len(peer.connection.waiting_requests) == 0 and peer.connection.connected else 9999) @@ -463,6 +463,7 @@ class WorkerManager(object): # Create new task and return asyncresult def addTask(self, inner_path, peer=None, priority=0, file_info=None): + self.lock_add_task.acquire() self.site.onFileStart(inner_path) # First task, trigger site download started task = self.tasks.findTask(inner_path) if task: # Already has task for that file @@ -476,7 +477,6 @@ class WorkerManager(object): task["failed"].remove(peer) # New update arrived, remove the peer from failed peers self.log.debug("Removed peer %s from failed %s" % (peer.key, task["inner_path"])) self.startWorkers([peer], reason="Added new task (peer failed before)") - return task else: # No task for that file yet evt = gevent.event.AsyncResult() if peer: @@ -526,7 +526,8 @@ class WorkerManager(object): else: self.startWorkers(peers, reason="Added new task") - return task + self.lock_add_task.release() + return task def addTaskWorker(self, task, worker): if task in self.tasks: From 7ecf09a4961669e2cfc26b7d627f778aef6c71fb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Dec 2019 15:24:05 +0100 Subject: [PATCH 356/483] Allow to change test log dir with environmental variable --- src/Test/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 91cd10c4..c18d395a 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -74,6 +74,8 @@ config.verbose = True # Use test data for unittests config.tor = "disable" # Don't start Tor client config.trackers = [] config.data_dir = TEST_DATA_PATH # Use test data for unittests +if "ZERONET_LOG_DIR" in os.environ: + config.log_dir = os.environ["ZERONET_LOG_DIR"] config.initLogging(console_logging=False) # Set custom formatter with realative time format (via: https://stackoverflow.com/questions/31521859/python-logging-module-time-since-last-log) From 1abaa6fddc242f226b88c7c7f6aa649c225741ff Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Dec 2019 15:24:40 +0100 Subject: [PATCH 357/483] Benchmark only exits when running as test from cli --- plugins/Benchmark/BenchmarkPlugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index 1916a664..f22e6a26 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -189,7 +189,8 @@ class ActionsPlugin: if not res: yield "! No tests found" - sys.exit(1) + if config.action == "test": + sys.exit(1) else: num_failed = len([res_key for res_key, res_val in res.items() if res_val != "ok"]) num_success = len([res_key for res_key, res_val in res.items() if res_val != "ok"]) @@ -201,7 +202,7 @@ class ActionsPlugin: multipler_avg = sum(multiplers) / len(multiplers) multipler_title = self.getMultiplerTitle(multipler_avg) yield " - Average speed factor: %.2fx (%s)" % (multipler_avg, multipler_title) - if num_failed == 0: + if num_failed == 0 and config.action == "test": sys.exit(1) From dbbad3097c60aa9d50a23c9ec1030e72827af78d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Dec 2019 15:32:42 +0100 Subject: [PATCH 358/483] Add segfault catcher, log plugins to separate directory --- .travis.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index c1b935da..238b63fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,15 +24,15 @@ before_script: sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6'; fi script: - - python -m pytest -x plugins/CryptMessage/Test - - python -m pytest -x plugins/Bigfile/Test - - python -m pytest -x plugins/AnnounceLocal/Test - - python -m pytest -x plugins/OptionalManager/Test - - python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini - - mv plugins/disabled-Multiuser plugins/Multiuser && python -m pytest -x plugins/Multiuser/Test - - mv plugins/disabled-Bootstrapper plugins/Bootstrapper && python -m pytest -x plugins/Bootstrapper/Test - - find src -name "*.json" | xargs -n 1 python -c "import json, sys; print(sys.argv[1]); json.load(open(sys.argv[1]))" - - find plugins -name "*.json" | xargs -n 1 python -c "import json, sys; print(sys.argv[1]); json.load(open(sys.argv[1]))" + - catchsegv python -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini + - export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python -m pytest -x plugins/CryptMessage/Test + - export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python -m pytest -x plugins/Bigfile/Test + - export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python -m pytest -x plugins/AnnounceLocal/Test + - export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python -m pytest -x plugins/OptionalManager/Test + - export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test + - export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test + - find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" + - find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ after_success: - codecov From 845b50915d124f4674a434f019cb5a6120cbcee7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Dec 2019 15:32:50 +0100 Subject: [PATCH 359/483] Rev4358 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 6fbf9722..84be35e1 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4355 + self.rev = 4358 self.argv = argv self.action = None self.test_parser = None From 7af8d1cd9399aff6bcd7468884e7a49e3203cdca Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Dec 2019 16:42:47 +0100 Subject: [PATCH 360/483] Save last lock time --- src/util/ThreadPool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 0e1757ea..2c78a2ad 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -72,8 +72,10 @@ class Lock: self.lock = gevent._threading.Lock() self.locked = self.lock.locked self.release = self.lock.release + self.time_lock = 0 def acquire(self, *args, **kwargs): + self.time_lock = time.time() if self.locked() and isMainThread(): # Start in new thread to avoid blocking gevent loop return lock_pool.apply(self.lock.acquire, args, kwargs) From c161140a90a6193ad1b553979574d67a9355f046 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Dec 2019 16:43:46 +0100 Subject: [PATCH 361/483] Add locking for db cursor --- src/Db/DbCursor.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 4a337273..245ed305 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -93,14 +93,21 @@ class DbCursor: query, params = self.parseQuery(query, params) - s = time.time() cursor = self.db.getConn().cursor() self.db.cursors.add(cursor) + if self.db.lock.locked(): + self.db.log.debug("Locked for %.3fs" % (time.time() - self.db.lock.time_lock)) + + try: + s = time.time() + self.db.lock.acquire(True) + if params: + res = cursor.execute(query, params) + else: + res = cursor.execute(query) + finally: + self.db.lock.release() - if params: - res = cursor.execute(query, params) - else: - res = cursor.execute(query) taken_query = time.time() - s if self.logging or taken_query > 0.1: if params: # Query has parameters From d660a268e8e692201a1ecd21bc37b2873f06e854 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 18 Dec 2019 16:43:58 +0100 Subject: [PATCH 362/483] Rev4360 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 84be35e1..7e69bd94 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4358 + self.rev = 4360 self.argv = argv self.action = None self.test_parser = None From 8bfef12ad4f95ffe85f403ba8994f9f64bc502db Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Dec 2019 02:16:41 +0100 Subject: [PATCH 363/483] Don't try to pack unknown peer addresses --- src/util/helper.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/util/helper.py b/src/util/helper.py index 018050ba..1c79dd8f 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -119,7 +119,8 @@ def packPeers(peers): for peer in peers: try: ip_type = getIpType(peer.ip) - packed_peers[ip_type].append(peer.packMyAddress()) + if ip_type in packed_peers: + packed_peers[ip_type].append(peer.packMyAddress()) except Exception: logging.debug("Error packing peer address: %s" % peer) return packed_peers @@ -295,8 +296,10 @@ def getIpType(ip): return "onion" elif ":" in ip: return "ipv6" - else: + elif re.match("[0-9\.]+$", ip): return "ipv4" + else: + return "unknown" def createSocket(ip, sock_type=socket.SOCK_STREAM): From 50bbe47bf23487142f817dbd9655d5944ee53675 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Dec 2019 02:17:00 +0100 Subject: [PATCH 364/483] Better logging on file update --- src/File/FileRequest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index 0846a714..b32ff273 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -122,12 +122,12 @@ class FileRequest(object): should_validate_content = False valid = None # Same or earlier content as we have elif not body: # No body sent, we have to download it first - self.log.debug("Missing body from update, downloading...") + site.log.debug("Missing body from update for file %s, downloading ..." % inner_path) peer = site.addPeer(self.connection.ip, self.connection.port, return_peer=True, source="update") # Add or get peer try: body = peer.getFile(site.address, inner_path).read() except Exception as err: - self.log.debug("Can't download updated file %s: %s" % (inner_path, err)) + site.log.debug("Can't download updated file %s: %s" % (inner_path, err)) self.response({"error": "File invalid update: Can't download updaed file"}) self.connection.badAction(5) return @@ -136,7 +136,7 @@ class FileRequest(object): try: content = json.loads(body.decode()) except Exception as err: - self.log.debug("Update for %s is invalid JSON: %s" % (inner_path, err)) + site.log.debug("Update for %s is invalid JSON: %s" % (inner_path, err)) self.response({"error": "File invalid JSON"}) self.connection.badAction(5) return @@ -149,7 +149,7 @@ class FileRequest(object): try: valid = site.content_manager.verifyFile(inner_path, content) except Exception as err: - self.log.debug("Update for %s is invalid: %s" % (inner_path, err)) + site.log.debug("Update for %s is invalid: %s" % (inner_path, err)) error = err valid = False @@ -187,7 +187,7 @@ class FileRequest(object): if inner_path in site.content_manager.contents: peer.last_content_json_update = site.content_manager.contents[inner_path]["modified"] if config.verbose: - self.log.debug( + site.log.debug( "Same version, adding new peer for locked files: %s, tasks: %s" % (peer.key, len(site.worker_manager.tasks)) ) From 99e6326974680ef1ad09b2a47cefe399ab9de26a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Dec 2019 02:17:13 +0100 Subject: [PATCH 365/483] More compact stack logging --- src/Debug/Debug.py | 67 +++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/src/Debug/Debug.py b/src/Debug/Debug.py index 2e38683e..b5ac3627 100644 --- a/src/Debug/Debug.py +++ b/src/Debug/Debug.py @@ -22,6 +22,47 @@ def formatExceptionMessage(err): return "%s: %s" % (err_type, err_message) +python_lib_dir = os.path.dirname(os.__file__) + + +def formatTraceback(items, limit=None, fold_builtin=True): + back = [] + i = 0 + prev_file_title = "" + for path, line in items: + i += 1 + is_last = i == len(items) + dir_name, file_name = os.path.split(path.replace("\\", "/")) + + plugin_match = re.match(".*/plugins/(.+)$", dir_name) + if plugin_match: + file_title = "%s/%s" % (plugin_match.group(1), file_name) + is_prev_builtin = False + elif path.startswith(python_lib_dir): + if is_prev_builtin and not is_last and fold_builtin: + if back[-1] != "...": + back.append("...") + continue + else: + file_title = path.replace(python_lib_dir, "").replace("\\", "/").strip("/").replace("site-packages/", "") + is_prev_builtin = True + else: + file_title = file_name + is_prev_builtin = False + + if file_title == prev_file_title: + back.append("%s" % line) + else: + back.append("%s line %s" % (file_title, line)) + + prev_file_title = file_title + + if limit and i >= limit: + back.append("...") + break + return back + + def formatException(err=None, format="text"): import traceback if type(err) == Notify: @@ -38,35 +79,17 @@ def formatException(err=None, format="text"): else: err = exc_obj - tb = [] - for frame in traceback.extract_tb(exc_tb): - path, line, function, text = frame - dir_name, file_name = os.path.split(path.replace("\\", "/")) - plugin_match = re.match(".*/plugins/(.+)$", dir_name) - if plugin_match: - file_title = "%s/%s" % (plugin_match.group(1), file_name) - else: - file_title = file_name - tb.append("%s line %s" % (file_title, line)) + tb = formatTraceback([[frame[0], frame[1]] for frame in traceback.extract_tb(exc_tb)]) if format == "html": return "%s: %s
%s" % (repr(err), err, " > ".join(tb)) else: return "%s: %s in %s" % (exc_type.__name__, err, " > ".join(tb)) -def formatStack(limit=99): +def formatStack(limit=None): import inspect - back = [] - i = 0 - for stack in inspect.stack(): - i += 1 - frame, path, line, function, source, index = stack - file = os.path.split(path)[1] - back.append("%s line %s" % (file, line)) - if i > limit: - back.append("...") - break - return " > ".join(back) + tb = formatTraceback([[frame[1], frame[2]] for frame in inspect.stack()[1:]], limit=limit) + return " > ".join(tb) # Test if gevent eventloop blocks From 69eb831c7e8a294792447751be95865986124b5a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Dec 2019 02:17:21 +0100 Subject: [PATCH 366/483] Rev4361 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 7e69bd94..52bde168 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4360 + self.rev = 4361 self.argv = argv self.action = None self.test_parser = None From eba81cc7d2013214775881eb3b3569bce231c4bc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Dec 2019 02:36:36 +0100 Subject: [PATCH 367/483] Print logs from subdirs --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 238b63fd..9fb58fce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ after_success: - codecov - coveralls --rcfile=src/Test/coverage.ini after_failure: - - cat log/*.log + - find a -name "*.log" -print -exec cat {} \; notifications: email: recipients: From 87d1c736e2dc22f190267d2632793c6006e604d8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 19 Dec 2019 02:45:00 +0100 Subject: [PATCH 368/483] Fix log printing typo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9fb58fce..deb96b5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ after_success: - codecov - coveralls --rcfile=src/Test/coverage.ini after_failure: - - find a -name "*.log" -print -exec cat {} \; + - find log -name "*.log" -print -exec cat {} \; notifications: email: recipients: From bde8b30d5cc2bbc25bf60d2a013880f7f7597e83 Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Fri, 20 Dec 2019 17:32:49 +0100 Subject: [PATCH 369/483] Upload logs after failure, remove sometimes failing coverage check --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index deb96b5d..148bd402 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,11 +34,9 @@ script: - find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" - find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ -after_success: - - codecov - - coveralls --rcfile=src/Test/coverage.ini after_failure: - - find log -name "*.log" -print -exec cat {} \; + - zip -r log.zip log/ + - curl --upload-file ./log.zip https://transfer.sh/log.zip notifications: email: recipients: From 7ca09ba75b12e424d008cbe5d4a3a56d8375b81c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:55:22 +0100 Subject: [PATCH 370/483] Fix updating key 0 in WorkerTaskManager --- src/Worker/WorkerTaskManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/WorkerTaskManager.py b/src/Worker/WorkerTaskManager.py index 791f5217..300a8e29 100644 --- a/src/Worker/WorkerTaskManager.py +++ b/src/Worker/WorkerTaskManager.py @@ -41,7 +41,7 @@ class CustomSortedList(MutableSequence): def updateItem(self, value, update_key=None, update_value=None): self.remove(value) - if update_key: + if update_key is not None: value[update_key] = update_value self.append(value) From 0881e274a945feaeaee5a01c350e10ce9f7682fa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:56:42 +0100 Subject: [PATCH 371/483] Log lock waits for task adding in WorkerManager --- src/Debug/DebugLock.py | 24 ++++++++++++++++++++++++ src/Worker/WorkerManager.py | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/Debug/DebugLock.py diff --git a/src/Debug/DebugLock.py b/src/Debug/DebugLock.py new file mode 100644 index 00000000..9cf22520 --- /dev/null +++ b/src/Debug/DebugLock.py @@ -0,0 +1,24 @@ +import time +import logging + +import gevent.lock + +from Debug import Debug + + +class DebugLock: + def __init__(self, log_after=0.01, name="Lock"): + self.name = name + self.log_after = log_after + self.lock = gevent.lock.Semaphore(1) + self.release = self.lock.release + + def acquire(self, *args, **kwargs): + s = time.time() + res = self.lock.acquire(*args, **kwargs) + time_taken = time.time() - s + if time_taken >= self.log_after: + logging.debug("%s: Waited %.3fs after called by %s" % + (self.name, time_taken, Debug.formatStack()) + ) + return res diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 2a71b88e..9054f283 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -9,6 +9,7 @@ from .WorkerTaskManager import WorkerTaskManager from Config import config from util import helper from Plugin import PluginManager +from Debug.DebugLock import DebugLock import util @@ -20,7 +21,7 @@ class WorkerManager(object): self.workers = {} # Key: ip:port, Value: Worker.Worker self.tasks = WorkerTaskManager() self.next_task_id = 1 - self.lock_add_task = gevent.lock.Semaphore(1) + self.lock_add_task = DebugLock() # {"id": 1, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "optional_hash_id": None, # "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0, "failed": peer_ids, "lock": None or gevent.lock.RLock} self.started_task_num = 0 # Last added task num From 8bf17d3a69fe7253b37908ae6009dde2fb4a496d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:57:25 +0100 Subject: [PATCH 372/483] Add reason for Worker actions --- src/Worker/Worker.py | 10 +++++----- src/Worker/WorkerManager.py | 14 ++++++-------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 4cdb83cb..81bbc7df 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -171,7 +171,7 @@ class Worker(object): if not task["done"]: if write_err: - self.manager.failTask(task) + self.manager.failTask(task, reason="Write error") self.num_failed += 1 self.manager.log.error("%s: Error writing %s: %s" % (self.key, task["inner_path"], write_err)) elif is_valid: @@ -223,15 +223,15 @@ class Worker(object): self.thread = gevent.spawn(self.downloader) # Skip current task - def skip(self): - self.manager.log.debug("%s: Force skipping" % self.key) + def skip(self, reason="Unknown"): + self.manager.log.debug("%s: Force skipping (reason: %s)" % (self.key, reason)) if self.thread: self.thread.kill(exception=Debug.Notify("Worker stopped")) self.start() # Force stop the worker - def stop(self): - self.manager.log.debug("%s: Force stopping" % self.key) + def stop(self, reason="Unknown"): + self.manager.log.debug("%s: Force stopping (reason: %s)" % (self.key, reason)) self.running = False if self.thread: self.thread.kill(exception=Debug.Notify("Worker stopped")) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 9054f283..932f9d6a 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -47,7 +47,7 @@ class WorkerManager(object): # Clean up workers for worker in list(self.workers.values()): if worker.task and worker.task["done"]: - worker.skip() # Stop workers with task done + worker.skip(reason="Task done") # Stop workers with task done if not self.tasks: continue @@ -67,14 +67,12 @@ class WorkerManager(object): workers = self.findWorkers(task) if workers: for worker in workers: - worker.skip() + worker.skip(reason="Task timeout") else: - self.failTask(task) + self.failTask(task, reason="No workers") elif time.time() >= task["time_added"] + 60 and not self.workers: # No workers left - self.log.debug("Timeout, Cleanup task: %s" % task) - # Remove task - self.failTask(task) + self.failTask(task, reason="Timeout") elif (task["time_started"] and time.time() >= task["time_started"] + 15) or not self.workers: # Find more workers: Task started more than 15 sec ago or no workers @@ -407,11 +405,11 @@ class WorkerManager(object): def stopWorkers(self): num = 0 for worker in list(self.workers.values()): - worker.stop() + worker.stop(reason="Stopping all workers") num += 1 tasks = self.tasks[:] # Copy for task in tasks: # Mark all current task as failed - self.failTask(task) + self.failTask(task, reason="Stopping all workers") return num # Find workers by task From 62d4edadf6553ff0d97f8062c88a553cfbaed3de Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:57:53 +0100 Subject: [PATCH 373/483] Fail task if no peer left to try --- src/Worker/WorkerManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 932f9d6a..5b69ceeb 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -539,6 +539,9 @@ class WorkerManager(object): self.tasks.updateItem(task, "workers_num", task["workers_num"] - 1) else: task["workers_num"] -= 1 + if len(task["failed"]) >= len(self.workers): + fail_reason = "Too many fails: %s (workers: %s)" % (len(task["failed"]), len(self.workers)) + self.failTask(task, reason=fail_reason) # Wait for other tasks def checkComplete(self): From f119f7d0d27a447b6b05c8ab3af2f451ddbb462b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:58:35 +0100 Subject: [PATCH 374/483] Use faster and thread safe way to re-sort tasks --- src/Worker/WorkerManager.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 5b69ceeb..a61832ab 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -529,15 +529,15 @@ class WorkerManager(object): return task def addTaskWorker(self, task, worker): - if task in self.tasks: + try: self.tasks.updateItem(task, "workers_num", task["workers_num"] + 1) - else: + except ValueError: task["workers_num"] += 1 def removeTaskWorker(self, task, worker): - if task in self.tasks: + try: self.tasks.updateItem(task, "workers_num", task["workers_num"] - 1) - else: + except ValueError: task["workers_num"] -= 1 if len(task["failed"]) >= len(self.workers): fail_reason = "Too many fails: %s (workers: %s)" % (len(task["failed"]), len(self.workers)) From c01245a4e05301886e126a6618cd124ae3811023 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:58:48 +0100 Subject: [PATCH 375/483] Log task fail --- src/Worker/WorkerManager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index a61832ab..a5c0c85c 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -571,8 +571,9 @@ class WorkerManager(object): self.site.greenlet_manager.spawn(self.checkComplete) # Mark a task failed - def failTask(self, task): + def failTask(self, task, reason="Unknown"): if task in self.tasks: + self.log.debug("Task %s failed (Reason: %s)" % (task["inner_path"], reason)) task["done"] = True self.tasks.remove(task) # Remove from queue self.site.onFileFail(task["inner_path"]) From 2c3f1ba7ad906e1d0fdccbddf8d7001ac04a5817 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:59:04 +0100 Subject: [PATCH 376/483] Check if all task are complete on fail task --- src/Worker/WorkerManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index a5c0c85c..980249a5 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -579,4 +579,4 @@ class WorkerManager(object): self.site.onFileFail(task["inner_path"]) task["evt"].set(False) if not self.tasks: - self.started_task_num = 0 + self.site.greenlet_manager.spawn(self.checkComplete) From 2acf24c3362b1387e188ae0eceb26847262ce162 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:59:18 +0100 Subject: [PATCH 377/483] Fix ipv4 checking regexp --- src/util/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/helper.py b/src/util/helper.py index 1c79dd8f..5383e5a3 100644 --- a/src/util/helper.py +++ b/src/util/helper.py @@ -296,7 +296,7 @@ def getIpType(ip): return "onion" elif ":" in ip: return "ipv6" - elif re.match("[0-9\.]+$", ip): + elif re.match(r"[0-9\.]+$", ip): return "ipv4" else: return "unknown" From 8a994b555956a97fe5176e5c94936f8182d0d3a0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 02:59:50 +0100 Subject: [PATCH 378/483] Ask before UiWebsocket server shutdown action --- src/Ui/UiWebsocket.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index ddbb6839..d8441af6 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -1148,10 +1148,20 @@ class UiWebsocket(object): @flag.no_multiuser def actionServerShutdown(self, to, restart=False): import main + def cbServerShutdown(res): + self.response(to, res) + if not res: + return False + if restart: + main.restart_after_shutdown = True + main.file_server.stop() + main.ui_server.stop() + if restart: - main.restart_after_shutdown = True - main.file_server.stop() - main.ui_server.stop() + message = [_["Restart ZeroNet client?"], _["Restart"]] + else: + message = [_["Shut down ZeroNet client?"], _["Shut down"]] + self.cmd("confirm", message, cbServerShutdown) @flag.admin @flag.no_multiuser From 975f53b95b65c01e1e5a46a7279e5088afe5363a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:01:45 +0100 Subject: [PATCH 379/483] New logging format for tests --- src/Test/conftest.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index c18d395a..98c68434 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -8,6 +8,7 @@ import shutil import gc import datetime import atexit +import threading import pytest import mock @@ -79,23 +80,39 @@ if "ZERONET_LOG_DIR" in os.environ: config.initLogging(console_logging=False) # Set custom formatter with realative time format (via: https://stackoverflow.com/questions/31521859/python-logging-module-time-since-last-log) +time_start = time.time() class TimeFilter(logging.Filter): + def __init__(self, *args, **kwargs): + self.time_last = time.time() + self.main_thread_id = threading.current_thread().ident + super().__init__(*args, **kwargs) def filter(self, record): - try: - last = self.last - except AttributeError: - last = record.relativeCreated + if threading.current_thread().ident != self.main_thread_id: + record.thread_marker = "T" + record.thread_title = "(Thread#%s)" % self.main_thread_id + else: + record.thread_marker = " " + record.thread_title = "" - delta = datetime.datetime.fromtimestamp(record.relativeCreated / 1000.0) - datetime.datetime.fromtimestamp(last / 1000.0) + since_last = time.time() - self.time_last + if since_last > 0.1: + line_marker = "!" + elif since_last > 0.02: + line_marker = "*" + elif since_last > 0.01: + line_marker = "-" + else: + line_marker = " " - record.relative = '{0:.3f}'.format(delta.seconds + delta.microseconds / 1000000.0) + since_start = time.time() - time_start + record.since_start = "%s%.3fs" % (line_marker, since_start) - self.last = record.relativeCreated + self.time_last = time.time() return True log = logging.getLogger() -fmt = logging.Formatter(fmt='+%(relative)ss %(levelname)-8s %(name)s %(message)s') +fmt = logging.Formatter(fmt='%(since_start)s %(thread_marker)s %(levelname)-8s %(name)s %(message)s %(thread_title)s') [hndl.addFilter(TimeFilter()) for hndl in log.handlers] [hndl.setFormatter(fmt) for hndl in log.handlers] @@ -337,10 +354,13 @@ def ui_websocket(site, user): self.result = gevent.event.AsyncResult() def send(self, data): + logging.debug("WsMock: Set result (data len: %s)" % len(data)) self.result.set(json.loads(data)["result"]) def getResult(self): + logging.debug("WsMock: Get result") back = self.result.get() + logging.debug("WsMock: Got result (data len: %s)" % len(back)) self.result = gevent.event.AsyncResult() return back @@ -424,6 +444,8 @@ def crypt_bitcoin_lib(request, monkeypatch): @pytest.fixture(scope='function', autouse=True) def logCaseStart(request): + global time_start + time_start = time.time() logging.debug("---- Start test case: %s ----" % request._pyfuncitem) yield None # Wait until all test done From e16ace433c8e3c0a7620a898e0747fdbce3c379e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:02:36 +0100 Subject: [PATCH 380/483] Better logging in site download content --- src/Site/Site.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index fd790342..cc3d1dc3 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -158,7 +158,10 @@ class Site(object): def downloadContent(self, inner_path, download_files=True, peer=None, check_modifications=False, diffs={}): s = time.time() if config.verbose: - self.log.debug("Downloading %s..." % inner_path) + self.log.debug( + "DownloadContent %s: Started. (download_files: %s, check_modifications: %s, diffs: %s)..." % + (inner_path, download_files, check_modifications, diffs.keys()) + ) if not inner_path.endswith("content.json"): return False @@ -166,15 +169,20 @@ class Site(object): found = self.needFile(inner_path, update=self.bad_files.get(inner_path)) content_inner_dir = helper.getDirname(inner_path) if not found: - self.log.debug("Download %s failed, check_modifications: %s" % (inner_path, check_modifications)) + self.log.debug("DownloadContent %s: Download failed, check_modifications: %s" % (inner_path, check_modifications)) if check_modifications: # Download failed, but check modifications if its succed later self.onFileDone.once(lambda file_name: self.checkModifications(0), "check_modifications") return False # Could not download content.json if config.verbose: - self.log.debug("Got %s" % inner_path) + self.log.debug("DownloadContent got %s" % inner_path) + sub_s = time.time() + changed, deleted = self.content_manager.loadContent(inner_path, load_includes=False) + if config.verbose: + self.log.debug("DownloadContent %s: loadContent done in %.3fs" % (inner_path, time.time() - sub_s)) + if inner_path == "content.json": self.saveSettings() @@ -187,7 +195,7 @@ class Site(object): content_size = len(json.dumps(self.content_manager.contents[inner_path], indent=1)) + sum([file["size"] for file in list(self.content_manager.contents[inner_path].get("files", {}).values()) if file["size"] >= 0]) # Size of new content if site_size_limit < content_size: # Not enought don't download anything - self.log.debug("Size limit reached (site too big please increase limit): %.2f MB > %.2f MB" % (content_size / 1024 / 1024, site_size_limit / 1024 / 1024)) + self.log.debug("DownloadContent Size limit reached (site too big please increase limit): %.2f MB > %.2f MB" % (content_size / 1024 / 1024, site_size_limit / 1024 / 1024)) return False # Start download files @@ -221,11 +229,11 @@ class Site(object): time_on_done = time.time() - s self.log.debug( - "Patched successfully: %s (diff: %.3fs, verify: %.3fs, write: %.3fs, on_done: %.3fs)" % + "DownloadContent Patched successfully: %s (diff: %.3fs, verify: %.3fs, write: %.3fs, on_done: %.3fs)" % (file_inner_path, time_diff, time_verify, time_write, time_on_done) ) except Exception as err: - self.log.debug("Failed to patch %s: %s" % (file_inner_path, err)) + self.log.debug("DownloadContent Failed to patch %s: %s" % (file_inner_path, err)) diff_success = False if not diff_success: @@ -259,19 +267,19 @@ class Site(object): include_threads.append(include_thread) if config.verbose: - self.log.debug("%s: Downloading %s includes..." % (inner_path, len(include_threads))) + self.log.debug("DownloadContent %s: Downloading %s includes..." % (inner_path, len(include_threads))) gevent.joinall(include_threads) if config.verbose: - self.log.debug("%s: Includes download ended" % inner_path) + self.log.debug("DownloadContent %s: Includes download ended" % inner_path) if check_modifications: # Check if every file is up-to-date self.checkModifications(0) if config.verbose: - self.log.debug("%s: Downloading %s files, changed: %s..." % (inner_path, len(file_threads), len(changed))) + self.log.debug("DownloadContent %s: Downloading %s files, changed: %s..." % (inner_path, len(file_threads), len(changed))) gevent.joinall(file_threads) if config.verbose: - self.log.debug("%s: DownloadContent ended in %.3fs (tasks left: %s)" % ( + self.log.debug("DownloadContent %s: ended in %.3fs (tasks left: %s)" % ( inner_path, time.time() - s, len(self.worker_manager.tasks) )) From c5de1447c82d98d0d2aa2e02bce4e6683c387e02 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:02:53 +0100 Subject: [PATCH 381/483] onComplete will be triggered by WorkerManager --- src/Site/Site.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index cc3d1dc3..2dc456b4 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -283,9 +283,6 @@ class Site(object): inner_path, time.time() - s, len(self.worker_manager.tasks) )) - if len(self.worker_manager.tasks) == 0: - self.onComplete() # No more task trigger site complete - return True # Return bad files with less than 3 retry From 7c1da5da52696a46f2c0a313d2104655502ac828 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:03:32 +0100 Subject: [PATCH 382/483] Abilty to disable file bad file retry at end of download --- src/Site/Site.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index 2dc456b4..ca59d326 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -326,15 +326,15 @@ class Site(object): # Download all files of the site @util.Noparallel(blocking=False) - def download(self, check_size=False, blind_includes=False): + def download(self, check_size=False, blind_includes=False, retry_bad_files=True): if not self.connection_server: self.log.debug("No connection server found, skipping download") return False s = time.time() self.log.debug( - "Start downloading, bad_files: %s, check_size: %s, blind_includes: %s" % - (self.bad_files, check_size, blind_includes) + "Start downloading, bad_files: %s, check_size: %s, blind_includes: %s, called by: %s" % + (self.bad_files, check_size, blind_includes, Debug.formatStack()) ) gevent.spawn(self.announce, force=True) if check_size: # Check the size first @@ -345,8 +345,9 @@ class Site(object): # Download everything valid = self.downloadContent("content.json", check_modifications=blind_includes) - self.onComplete.once(lambda: self.retryBadFiles(force=True)) - self.log.debug("Download done in %.3fs" % (time.time () - s)) + if retry_bad_files: + self.onComplete.once(lambda: self.retryBadFiles(force=True)) + self.log.debug("Download done in %.3fs" % (time.time() - s)) return valid From 3ccce4631497c0d6a7f4d58a39be61c1613e4fbd Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:03:49 +0100 Subject: [PATCH 383/483] Wait until downloadContent pool finishes --- src/Site/Site.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Site/Site.py b/src/Site/Site.py index ca59d326..d07b523a 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -370,6 +370,7 @@ class Site(object): del self.bad_files[aborted_inner_path] self.worker_manager.removeSolvedFileTasks(mark_as_good=False) break + pool.join() self.log.debug("Ended downloadContent pool len: %s, skipped: %s" % (len(inner_paths), num_skipped)) def pooledDownloadFile(self, inner_paths, pool_size=100, only_if_bad=False): From c6b07f1294390b81c325f003760c8813d76ac0fb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:04:36 +0100 Subject: [PATCH 384/483] Wait until checkmodification spawned pools are finishing --- src/Site/Site.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Site/Site.py b/src/Site/Site.py index d07b523a..09ff03c9 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -388,12 +388,13 @@ class Site(object): # Update worker, try to find client that supports listModifications command def updater(self, peers_try, queried, since): + threads = [] while 1: if not peers_try or len(queried) >= 3: # Stop after 3 successful query break peer = peers_try.pop(0) if config.verbose: - self.log.debug("Try to get updates from: %s Left: %s" % (peer, peers_try)) + self.log.debug("CheckModifications: Try to get updates from: %s Left: %s" % (peer, peers_try)) res = None with gevent.Timeout(20, exception=False): @@ -416,12 +417,16 @@ class Site(object): self.bad_files[inner_path] = self.bad_files.get(inner_path, 0) + 1 if has_older and num_old_files < 5: num_old_files += 1 - self.log.debug("%s client has older version of %s, publishing there (%s/5)..." % (peer, inner_path, num_old_files)) + self.log.debug("CheckModifications: %s client has older version of %s, publishing there (%s/5)..." % (peer, inner_path, num_old_files)) gevent.spawn(self.publisher, inner_path, [peer], [], 1) if modified_contents: - self.log.debug("%s new modified file from %s" % (len(modified_contents), peer)) + self.log.debug("CheckModifications: %s new modified file from %s" % (len(modified_contents), peer)) modified_contents.sort(key=lambda inner_path: 0 - res["modified_files"][inner_path]) # Download newest first - gevent.spawn(self.pooledDownloadContent, modified_contents, only_if_bad=True) + t = gevent.spawn(self.pooledDownloadContent, modified_contents, only_if_bad=True) + threads.append(t) + if config.verbose: + self.log.debug("CheckModifications: Waiting for %s pooledDownloadContent" % len(threads)) + gevent.joinall(threads) # Check modified content.json files from peers and add modified files to bad_files # Return: Successfully queried peers [Peer, Peer...] @@ -436,7 +441,7 @@ class Site(object): self.announce() for wait in range(10): time.sleep(5 + wait) - self.log.debug("Waiting for peers...") + self.log.debug("CheckModifications: Waiting for peers...") if self.peers: break @@ -450,7 +455,7 @@ class Site(object): if config.verbose: self.log.debug( - "Try to get listModifications from peers: %s, connected: %s, since: %s" % + "CheckModifications: Try to get listModifications from peers: %s, connected: %s, since: %s" % (peers_try, peers_connected_num, since) ) @@ -467,7 +472,7 @@ class Site(object): if queried: break - self.log.debug("Queried listModifications from: %s in %.3fs since %s" % (queried, time.time() - s, since)) + self.log.debug("CheckModifications: Queried listModifications from: %s in %.3fs since %s" % (queried, time.time() - s, since)) time.sleep(0.1) return queried From 17fb740c514810ecfcb153004641be7edaaa62f2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:05:19 +0100 Subject: [PATCH 385/483] Don't try to download bad files again in tests to avoid random test fails --- src/Test/TestSiteDownload.py | 43 +++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/Test/TestSiteDownload.py b/src/Test/TestSiteDownload.py index 650d424b..cd0a4c9f 100644 --- a/src/Test/TestSiteDownload.py +++ b/src/Test/TestSiteDownload.py @@ -34,7 +34,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) assert site_temp.storage.isFile("content.json") @@ -53,7 +53,7 @@ class TestSiteDownload: with Spy.Spy(FileRequest, "route") as requests: site.publish() time.sleep(0.1) - assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) # Wait for download assert "streamFile" not in [req[1] for req in requests] content = site_temp.storage.loadJson("content.json") @@ -85,7 +85,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) assert site_temp.settings["optional_downloaded"] == 0 @@ -109,7 +109,7 @@ class TestSiteDownload: with Spy.Spy(FileRequest, "route") as requests: site.publish() time.sleep(0.1) - assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) # Wait for download assert "streamFile" not in [req[1] for req in requests] content = site_temp.storage.loadJson("content.json") @@ -139,7 +139,7 @@ class TestSiteDownload: # Download normally site_temp.addPeer(file_server.ip, 1544) - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] assert not bad_files @@ -148,7 +148,7 @@ class TestSiteDownload: assert len(list(site_temp.storage.query("SELECT * FROM comment"))) == 2 # Add archived data - assert not "archived" in site.content_manager.contents["data/users/content.json"]["user_contents"] + assert "archived" not in site.content_manager.contents["data/users/content.json"]["user_contents"] assert not site.content_manager.isArchived("data/users/1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q/content.json", time.time()-1) site.content_manager.contents["data/users/content.json"]["user_contents"]["archived"] = {"1C5sgvWaSgfaTpV5kjBCnCiKtENNMYo69q": time.time()} @@ -163,7 +163,7 @@ class TestSiteDownload: assert not "archived" in site_temp.content_manager.contents["data/users/content.json"]["user_contents"] site.publish() time.sleep(0.1) - assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) # Wait for download # The archived content should disappear from remote client assert "archived" in site_temp.content_manager.contents["data/users/content.json"]["user_contents"] @@ -187,7 +187,7 @@ class TestSiteDownload: # Download normally site_temp.addPeer(file_server.ip, 1544) - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] assert not bad_files @@ -212,7 +212,7 @@ class TestSiteDownload: assert not "archived_before" in site_temp.content_manager.contents["data/users/content.json"]["user_contents"] site.publish() time.sleep(0.1) - assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) # Wait for download # The archived content should disappear from remote client assert "archived_before" in site_temp.content_manager.contents["data/users/content.json"]["user_contents"] @@ -239,7 +239,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) # Download site - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) # Download optional data/optional.txt site.storage.verifyFiles(quick_check=True) # Find what optional files we have @@ -304,7 +304,7 @@ class TestSiteDownload: # Download normal files site_temp.log.info("Start Downloading site") - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) # Download optional data/optional.txt optional_file_info = site_temp.content_manager.getFileInfo("data/optional.txt") @@ -357,7 +357,8 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) + assert len(site_temp.bad_files) == 1 # Update file data_original = site.storage.open("data/data.json").read() @@ -375,7 +376,8 @@ class TestSiteDownload: site.content_manager.sign("content.json", privatekey="5KUh3PvNm5HUWoCfSUfcYvfQ2g3PrRNJWr6Q9eqdBGu23mtMntv") site.publish() time.sleep(0.1) - assert site_temp.download(blind_includes=True).get(timeout=10) + site.log.info("Downloading site") + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) assert len([request for request in requests if request[1] in ("getFile", "streamFile")]) == 1 assert site_temp.storage.open("data/data.json").read() == data_new @@ -410,7 +412,7 @@ class TestSiteDownload: event_done = gevent.event.AsyncResult() site.publish(diffs=diffs) time.sleep(0.1) - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) assert [request for request in requests if request[1] in ("getFile", "streamFile")] == [] assert site_temp.storage.open("data/data.json").read() == data_new @@ -432,7 +434,8 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) + assert list(site_temp.bad_files.keys()) == ["data/users/1J6UrZMkarjVg5ax9W4qThir3BFUikbW6C/content.json"] # Update file data_original = site.storage.open("data/data.json").read() @@ -462,7 +465,7 @@ class TestSiteDownload: assert site.storage.getSize("content.json") > 10 * 1024 # Make it a big content.json site.publish(diffs=diffs) time.sleep(0.1) - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) file_requests = [request for request in requests if request[1] in ("getFile", "streamFile")] assert len(file_requests) == 1 @@ -484,7 +487,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) # Download site from site to site_temp - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) site_temp.settings["size_limit"] = int(20 * 1024 *1024) site_temp.saveSettings() @@ -510,7 +513,7 @@ class TestSiteDownload: assert site.storage.getSize("content.json") > 10 * 1024 * 1024 # verify it over 10MB time.sleep(0.1) site.publish(diffs=diffs) - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) assert site_temp.storage.getSize("content.json") < site_temp.getSizeLimit() * 1024 * 1024 assert site_temp.storage.open("content.json").read() == site.storage.open("content.json").read() @@ -531,7 +534,7 @@ class TestSiteDownload: site_temp.addPeer(file_server.ip, 1544) - assert site_temp.download(blind_includes=True).get(timeout=10) + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) site.storage.write("data/img/árvíztűrő.png", b"test") @@ -545,7 +548,7 @@ class TestSiteDownload: with Spy.Spy(FileRequest, "route") as requests: site.publish() time.sleep(0.1) - assert site_temp.download(blind_includes=True).get(timeout=10) # Wait for download + assert site_temp.download(blind_includes=True, retry_bad_files=False).get(timeout=10) # Wait for download assert len([req[1] for req in requests if req[1] == "streamFile"]) == 1 content = site_temp.storage.loadJson("content.json") From 48124e12d9bd6366f0c7012e8b9e8c33cdab3a68 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:05:49 +0100 Subject: [PATCH 386/483] Rev4372 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 52bde168..eec420cd 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4361 + self.rev = 4372 self.argv = argv self.action = None self.test_parser = None From 3d73599debff7bf9c6b029291502e32e45790f97 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:21:38 +0100 Subject: [PATCH 387/483] Don't retry bad files also in big file tests --- plugins/Bigfile/Test/TestBigfile.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/Bigfile/Test/TestBigfile.py b/plugins/Bigfile/Test/TestBigfile.py index 9f67838e..402646a6 100644 --- a/plugins/Bigfile/Test/TestBigfile.py +++ b/plugins/Bigfile/Test/TestBigfile.py @@ -134,7 +134,7 @@ class TestBigfile: peer_client = site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) bad_files = site_temp.storage.verifyFiles(quick_check=True)["bad_files"] assert not bad_files @@ -172,7 +172,7 @@ class TestBigfile: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Open virtual file assert not site_temp.storage.isFile(inner_path) @@ -255,7 +255,7 @@ class TestBigfile: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Download second block with site_temp.storage.openBigfile(inner_path) as f: @@ -380,7 +380,7 @@ class TestBigfile: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Open virtual file assert not site_temp.storage.isFile(inner_path) @@ -417,7 +417,7 @@ class TestBigfile: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Open virtual file assert not site_temp.storage.isFile(inner_path) @@ -453,7 +453,7 @@ class TestBigfile: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Open virtual file assert not site_temp.storage.isFile(inner_path) @@ -482,7 +482,7 @@ class TestBigfile: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Open virtual file assert not site_temp.storage.isFile(inner_path) @@ -507,7 +507,7 @@ class TestBigfile: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) with Spy.Spy(FileRequest, "route") as requests: site_temp.needFile("%s|%s-%s" % (inner_path, 0, 1 * self.piece_size)) @@ -529,7 +529,7 @@ class TestBigfile: with Spy.Spy(FileRequest, "route") as requests: site.publish() time.sleep(0.1) - site_temp.download(blind_includes=True).join(timeout=5) # Wait for download + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) # Wait for download assert len([req[1] for req in requests if req[1] == "streamFile"]) == 0 @@ -563,7 +563,7 @@ class TestBigfile: site_temp.addPeer(file_server.ip, 1544) # Download site - site_temp.download(blind_includes=True).join(timeout=5) + site_temp.download(blind_includes=True, retry_bad_files=False).join(timeout=10) if "piecemap" in site.content_manager.getFileInfo(inner_path): # Bigfile site_temp.needFile(inner_path + "|all") From df87bd41b40d0bb18bf406923562b1c8b578184c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:22:37 +0100 Subject: [PATCH 388/483] Log WsMock sent data itself to figure out random Crypt test fail --- src/Test/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 98c68434..b24f6a9a 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -354,7 +354,7 @@ def ui_websocket(site, user): self.result = gevent.event.AsyncResult() def send(self, data): - logging.debug("WsMock: Set result (data len: %s)" % len(data)) + logging.debug("WsMock: Set result (data: %s)" % data) self.result.set(json.loads(data)["result"]) def getResult(self): From 60146a083ce5844a286f65fc9b61ac0f3cbab3b3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 21 Dec 2019 03:30:27 +0100 Subject: [PATCH 389/483] Fix ui_websocket test result with None --- src/Test/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index b24f6a9a..9f5a1f32 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -360,7 +360,7 @@ def ui_websocket(site, user): def getResult(self): logging.debug("WsMock: Get result") back = self.result.get() - logging.debug("WsMock: Got result (data len: %s)" % len(back)) + logging.debug("WsMock: Got result (data: %s)" % back) self.result = gevent.event.AsyncResult() return back From 796ee572ceb9dd26eb0f4f3ddac6ab5f47913fb7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:44:47 +0100 Subject: [PATCH 390/483] Fix verify invalid json --- src/Content/ContentManager.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 7ef7e54e..81b5c0e1 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -936,10 +936,13 @@ class ContentManager(object): if type(file) is dict: new_content = file else: - if sys.version_info.major == 3 and sys.version_info.minor < 6: - new_content = json.loads(file.read().decode("utf8")) - else: - new_content = json.load(file) + try: + if sys.version_info.major == 3 and sys.version_info.minor < 6: + new_content = json.loads(file.read().decode("utf8")) + else: + new_content = json.load(file) + except Exception as err: + raise VerifyError("Invalid json file: %s" % err) if inner_path in self.contents: old_content = self.contents.get(inner_path, {"modified": 0}) # Checks if its newer the ours From 71d32d74141a703ccd06bae79d410ea2f1d30dc6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:45:36 +0100 Subject: [PATCH 391/483] Less slow query loggin --- src/Db/DbCursor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 245ed305..7d0c1626 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -109,7 +109,7 @@ class DbCursor: self.db.lock.release() taken_query = time.time() - s - if self.logging or taken_query > 0.1: + if self.logging or taken_query > 1: if params: # Query has parameters self.db.log.debug("Query: " + query + " " + str(params) + " (Done in %.4f)" % (time.time() - s)) else: From 32b0153d342d8f9250f6c11fa47ae0a6a4c9ddd7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:46:01 +0100 Subject: [PATCH 392/483] Log site address with getfile error --- src/File/FileRequest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File/FileRequest.py b/src/File/FileRequest.py index b32ff273..65c335a9 100644 --- a/src/File/FileRequest.py +++ b/src/File/FileRequest.py @@ -269,7 +269,7 @@ class FileRequest(object): return {"bytes_sent": bytes_sent, "file_size": file_size, "location": params["location"]} except RequestError as err: - self.log.debug("GetFile %s %s request error: %s" % (self.connection, params["inner_path"], Debug.formatException(err))) + self.log.debug("GetFile %s %s %s request error: %s" % (self.connection, params["site"], params["inner_path"], Debug.formatException(err))) self.response({"error": "File read error: %s" % err}) except OSError as err: if config.verbose: From 721d4a22f18ac3f57f0f9e4af7f8b9d977f559d6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:49:59 +0100 Subject: [PATCH 393/483] Remove unnecessary log from worker task manager --- src/Worker/WorkerTaskManager.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Worker/WorkerTaskManager.py b/src/Worker/WorkerTaskManager.py index 300a8e29..4b3edd87 100644 --- a/src/Worker/WorkerTaskManager.py +++ b/src/Worker/WorkerTaskManager.py @@ -15,16 +15,12 @@ class CustomSortedList(MutableSequence): return len(self.items) def __getitem__(self, index): - if self.logging: - print("getitem", index) if type(index) is int: return self.items[index][2] else: return [item[2] for item in self.items[index]] def __delitem__(self, index): - if self.logging: - print("delitem", index) del self.items[index] def __setitem__(self, index, value): @@ -67,17 +63,18 @@ class CustomSortedList(MutableSequence): item = (self.getPriority(value), self.getId(value), value) bisect_pos = bisect.bisect(self.items, item) - 1 if bisect_pos >= 0 and self.items[bisect_pos][2] == value: - if self.logging: - print("Fast index for", value) return bisect_pos # Item probably changed since added, switch to slow iteration pos = self.indexSlow(value) - if pos is not None: - if self.logging: - print("Slow index for %s in pos %s bisect: %s" % (item[2], pos, bisect_pos)) + + if self.logging: + print("Slow index for %s in pos %s bisect: %s" % (item[2], pos, bisect_pos)) + + if pos is None: + raise ValueError("%r not in list" % value) + else: return pos - raise ValueError("%r not in list" % value) def __contains__(self, value): try: From ba218974c482efb16ed82006a9c18f01daa13aeb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:50:21 +0100 Subject: [PATCH 394/483] Task remove optimization --- src/Worker/WorkerTaskManager.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Worker/WorkerTaskManager.py b/src/Worker/WorkerTaskManager.py index 4b3edd87..9359701d 100644 --- a/src/Worker/WorkerTaskManager.py +++ b/src/Worker/WorkerTaskManager.py @@ -98,6 +98,11 @@ class WorkerTaskManager(CustomSortedList): def __contains__(self, value): return value["inner_path"] in self.inner_paths + def __delitem__(self, index): + # Remove from inner path cache + del self.inner_paths[self.items[index][2]["inner_path"]] + super().__delitem__(index) + # Fast task search by inner_path def append(self, task): @@ -107,10 +112,11 @@ class WorkerTaskManager(CustomSortedList): # Create inner path cache for faster lookup by filename self.inner_paths[task["inner_path"]] = task - def __delitem__(self, index): - # Remove from inner path cache - del self.inner_paths[self.items[index][2]["inner_path"]] - super().__delitem__(index) + def remove(self, task): + if task not in self: + raise ValueError("%r not in list" % task) + else: + super().remove(task) def findTask(self, inner_path): return self.inner_paths.get(inner_path, None) From 5987274edf017cba5f52f925a64e63ba5ca4c4de Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:50:39 +0100 Subject: [PATCH 395/483] Name task adding lock --- src/Worker/WorkerManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index 980249a5..c4d11b25 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -21,7 +21,7 @@ class WorkerManager(object): self.workers = {} # Key: ip:port, Value: Worker.Worker self.tasks = WorkerTaskManager() self.next_task_id = 1 - self.lock_add_task = DebugLock() + self.lock_add_task = DebugLock(name="Lock AddTask:%s" % self.site.address_short) # {"id": 1, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, "optional_hash_id": None, # "time_started": None, "time_added": time.time(), "peers": peers, "priority": 0, "failed": peer_ids, "lock": None or gevent.lock.RLock} self.started_task_num = 0 # Last added task num From b2e7cbb927bd6c6c47cdc3c5c1cb8bddd0bef938 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:51:52 +0100 Subject: [PATCH 396/483] Refactor task adding with less locking --- src/Worker/WorkerManager.py | 135 ++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 60 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index c4d11b25..cf4b4bd8 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -460,72 +460,87 @@ class WorkerManager(object): return 2 return 0 + def addTaskUpdate(self, task, peer, priority=0): + if priority > task["priority"]: + self.tasks.updateItem(task, "priority", priority) + if peer and task["peers"]: # This peer also has new version, add it to task possible peers + task["peers"].append(peer) + self.log.debug("Added peer %s to %s" % (peer.key, task["inner_path"])) + self.startWorkers([peer], reason="Added new task (update received by peer)") + elif peer and peer in task["failed"]: + task["failed"].remove(peer) # New update arrived, remove the peer from failed peers + self.log.debug("Removed peer %s from failed %s" % (peer.key, task["inner_path"])) + self.startWorkers([peer], reason="Added new task (peer failed before)") + + def addTaskCreate(self, inner_path, peer, priority=0, file_info=None): + evt = gevent.event.AsyncResult() + if peer: + peers = [peer] # Only download from this peer + else: + peers = None + if not file_info: + file_info = self.site.content_manager.getFileInfo(inner_path) + if file_info and file_info["optional"]: + optional_hash_id = helper.toHashId(file_info["sha512"]) + else: + optional_hash_id = None + if file_info: + size = file_info.get("size", 0) + else: + size = 0 + + self.lock_add_task.acquire() + + # Check again if we have task for this file + task = self.tasks.findTask(inner_path) + if task: + self.addTaskUpdate(task, peer, priority) + return task + + priority += self.getPriorityBoost(inner_path) + + if self.started_task_num == 0: # Boost priority for first requested file + priority += 1 + + task = { + "id": self.next_task_id, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, + "optional_hash_id": optional_hash_id, "time_added": time.time(), "time_started": None, "lock": None, + "time_action": None, "peers": peers, "priority": priority, "failed": [], "size": size + } + + self.tasks.append(task) + self.lock_add_task.release() + + self.next_task_id += 1 + self.started_task_num += 1 + if config.verbose: + self.log.debug( + "New task: %s, peer lock: %s, priority: %s, optional_hash_id: %s, tasks started: %s" % + (task["inner_path"], peers, priority, optional_hash_id, self.started_task_num) + ) + + self.time_task_added = time.time() + + if optional_hash_id: + if self.asked_peers: + del self.asked_peers[:] # Reset asked peers + self.startFindOptional(high_priority=priority > 0) + + if peers: + self.startWorkers(peers, reason="Added new optional task") + + else: + self.startWorkers(peers, reason="Added new task") + return task + # Create new task and return asyncresult def addTask(self, inner_path, peer=None, priority=0, file_info=None): - self.lock_add_task.acquire() self.site.onFileStart(inner_path) # First task, trigger site download started task = self.tasks.findTask(inner_path) if task: # Already has task for that file - if priority > task["priority"]: - self.tasks.updateItem(task, "priority", priority) - if peer and task["peers"]: # This peer also has new version, add it to task possible peers - task["peers"].append(peer) - self.log.debug("Added peer %s to %s" % (peer.key, task["inner_path"])) - self.startWorkers([peer], reason="Added new task (update received by peer)") - elif peer and peer in task["failed"]: - task["failed"].remove(peer) # New update arrived, remove the peer from failed peers - self.log.debug("Removed peer %s from failed %s" % (peer.key, task["inner_path"])) - self.startWorkers([peer], reason="Added new task (peer failed before)") + self.addTaskUpdate(task, peer, priority) else: # No task for that file yet - evt = gevent.event.AsyncResult() - if peer: - peers = [peer] # Only download from this peer - else: - peers = None - if not file_info: - file_info = self.site.content_manager.getFileInfo(inner_path) - if file_info and file_info["optional"]: - optional_hash_id = helper.toHashId(file_info["sha512"]) - else: - optional_hash_id = None - if file_info: - size = file_info.get("size", 0) - else: - size = 0 - priority += self.getPriorityBoost(inner_path) - - if self.started_task_num == 0: # Boost priority for first requested file - priority += 1 - - task = { - "id": self.next_task_id, "evt": evt, "workers_num": 0, "site": self.site, "inner_path": inner_path, "done": False, - "optional_hash_id": optional_hash_id, "time_added": time.time(), "time_started": None, "lock": None, - "time_action": None, "peers": peers, "priority": priority, "failed": [], "size": size - } - - self.tasks.append(task) - - self.next_task_id += 1 - self.started_task_num += 1 - if config.verbose: - self.log.debug( - "New task: %s, peer lock: %s, priority: %s, optional_hash_id: %s, tasks started: %s" % - (task["inner_path"], peers, priority, optional_hash_id, self.started_task_num) - ) - - self.time_task_added = time.time() - - if optional_hash_id: - if self.asked_peers: - del self.asked_peers[:] # Reset asked peers - self.startFindOptional(high_priority=priority > 0) - - if peers: - self.startWorkers(peers, reason="Added new optional task") - - else: - self.startWorkers(peers, reason="Added new task") - self.lock_add_task.release() + task = self.addTaskCreate(inner_path, peer, priority, file_info) return task def addTaskWorker(self, task, worker): From 20b0db7ddbc81e82e74b44a38e42924b1b0aa5e6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:54:45 +0100 Subject: [PATCH 397/483] Thread safe task remove in failTask --- src/Worker/WorkerManager.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Worker/WorkerManager.py b/src/Worker/WorkerManager.py index cf4b4bd8..6cfd0d3d 100644 --- a/src/Worker/WorkerManager.py +++ b/src/Worker/WorkerManager.py @@ -587,11 +587,14 @@ class WorkerManager(object): # Mark a task failed def failTask(self, task, reason="Unknown"): - if task in self.tasks: - self.log.debug("Task %s failed (Reason: %s)" % (task["inner_path"], reason)) - task["done"] = True + try: self.tasks.remove(task) # Remove from queue - self.site.onFileFail(task["inner_path"]) - task["evt"].set(False) - if not self.tasks: - self.site.greenlet_manager.spawn(self.checkComplete) + except ValueError as err: + return False + + self.log.debug("Task %s failed (Reason: %s)" % (task["inner_path"], reason)) + task["done"] = True + self.site.onFileFail(task["inner_path"]) + task["evt"].set(False) + if not self.tasks: + self.site.greenlet_manager.spawn(self.checkComplete) From 3fc80f834dbead5064bc46a2a8471d5ffefa6673 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:55:09 +0100 Subject: [PATCH 398/483] New tests for worker task manager --- src/Test/TestWorkerTaskManager.py | 42 ++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/Test/TestWorkerTaskManager.py b/src/Test/TestWorkerTaskManager.py index 375100c9..eb5c4a2a 100644 --- a/src/Test/TestWorkerTaskManager.py +++ b/src/Test/TestWorkerTaskManager.py @@ -39,12 +39,34 @@ class TestUiWebsocket: task = {"id": i, "priority": i % 20, "workers_num": i % 3, "inner_path": "file%s.json" % i} assert task in tasks - tasks.remove(task) + with Spy.Spy(tasks, "indexSlow") as calls: + tasks.remove(task) + assert len(calls) == 0 assert task not in tasks + # Remove non existent item + with Spy.Spy(tasks, "indexSlow") as calls: + with pytest.raises(ValueError): + tasks.remove(task) + assert len(calls) == 0 + self.checkSort(tasks) + def testRemoveAll(self): + tasks = WorkerTaskManager.WorkerTaskManager() + tasks_list = [] + for i in range(1000): + task = {"id": i, "priority": i % 20, "workers_num": i % 3, "inner_path": "file%s.json" % i} + tasks.append(task) + tasks_list.append(task) + + for task in tasks_list: + tasks.remove(task) + + assert len(tasks.inner_paths) == 0 + assert len(tasks) == 0 + def testModify(self): tasks = WorkerTaskManager.WorkerTaskManager() for i in range(1000): @@ -65,13 +87,28 @@ class TestUiWebsocket: self.checkSort(tasks) # Check reorder optimization - with Spy.Spy(tasks, "indexSlow") as calls: tasks.updateItem(task, "priority", task["priority"] + 10) assert len(calls) == 0 + with Spy.Spy(tasks, "indexSlow") as calls: + tasks.updateItem(task, "priority", task["workers_num"] - 1) + assert len(calls) == 0 + self.checkSort(tasks) + def testModifySamePriority(self): + tasks = WorkerTaskManager.WorkerTaskManager() + for i in range(1000): + tasks.append({"id": i, "priority": 10, "workers_num": 5, "inner_path": "file%s.json" % i}) + + task = tasks[333] + + # Check reorder optimization + with Spy.Spy(tasks, "indexSlow") as calls: + tasks.updateItem(task, "priority", task["workers_num"] - 1) + assert len(calls) == 0 + def testIn(self): tasks = WorkerTaskManager.WorkerTaskManager() @@ -80,7 +117,6 @@ class TestUiWebsocket: assert task not in tasks - def testFindTask(self): tasks = WorkerTaskManager.WorkerTaskManager() for i in range(1000): From 163825c03e37e560e63e6c640f289deaae9035b2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 12:56:10 +0100 Subject: [PATCH 399/483] Rev4381 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index eec420cd..d3198372 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4372 + self.rev = 4381 self.argv = argv self.action = None self.test_parser = None From feb58e4b0e38ff309172ac7770a410469a0144db Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 31 Dec 2019 18:15:17 +0100 Subject: [PATCH 400/483] Rev4382, Fix is_prev_builtin startup error --- src/Config.py | 2 +- src/Debug/Debug.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index d3198372..244b7f80 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4381 + self.rev = 4382 self.argv = argv self.action = None self.test_parser = None diff --git a/src/Debug/Debug.py b/src/Debug/Debug.py index b5ac3627..10a4d627 100644 --- a/src/Debug/Debug.py +++ b/src/Debug/Debug.py @@ -29,6 +29,8 @@ def formatTraceback(items, limit=None, fold_builtin=True): back = [] i = 0 prev_file_title = "" + is_prev_builtin = False + for path, line in items: i += 1 is_last = i == len(items) From 7d5f3354b69018c62aa3b12ce966383ee5413dee Mon Sep 17 00:00:00 2001 From: rllola Date: Thu, 2 Jan 2020 11:32:47 +0100 Subject: [PATCH 401/483] Update README; 'valueencoding' configuration required for rpc call; --- plugins/Zeroname/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Zeroname/README.md b/plugins/Zeroname/README.md index 8a306789..baa43db5 100644 --- a/plugins/Zeroname/README.md +++ b/plugins/Zeroname/README.md @@ -22,6 +22,7 @@ rpcpassword=your-password rpcport=8336 server=1 txindex=1 +valueencoding=utf8 ``` Don't forget to change the `rpcuser` value and `rpcpassword` value! From b6d0bf8f6b0a550d037f909b9ccf4a0e4e33da10 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:48:37 +0100 Subject: [PATCH 402/483] Use msvcrt 110 and 120 when 110 is not avaliable --- src/util/Platform.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/util/Platform.py b/src/util/Platform.py index 74613302..5bdde2f8 100644 --- a/src/util/Platform.py +++ b/src/util/Platform.py @@ -6,10 +6,22 @@ def setMaxfilesopened(limit): try: if sys.platform == "win32": import ctypes - maxstdio = ctypes.cdll.msvcr100._getmaxstdio() + dll = None + last_err = None + for dll_name in ["msvcr100", "msvcr110", "msvcr120"]: + try: + dll = getattr(ctypes.cdll, dll_name) + break + except OSError as err: + last_err = err + + if not dll: + raise last_err + + maxstdio = dll._getmaxstdio() if maxstdio < limit: - logging.debug("Current maxstdio: %s, changing to %s..." % (maxstdio, limit)) - ctypes.cdll.msvcr100._setmaxstdio(limit) + logging.debug("%s: Current maxstdio: %s, changing to %s..." % (dll, maxstdio, limit)) + dll._setmaxstdio(limit) return True else: import resource From fe739fa84856960edcc878d1dc574fcfc6aed1b8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:48:56 +0100 Subject: [PATCH 403/483] Log tasks with larger priority --- src/Worker/Worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index 81bbc7df..b8eb4471 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -162,7 +162,7 @@ class Worker(object): is_same = False if is_valid and not is_same: - if self.manager.started_task_num < 50 or config.verbose: + if self.manager.started_task_num < 50 or task["priority"] > 10 or config.verbose: self.manager.log.debug("%s: Verify correct: %s" % (self.key, task["inner_path"])) try: self.writeTask(task, buff) From 995d87c16758b680fac7d652489aaeb2e87be7e0 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:52:18 +0100 Subject: [PATCH 404/483] Don't add escaping iframe message for link without target=_top --- src/Ui/template/wrapper.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ui/template/wrapper.html b/src/Ui/template/wrapper.html index 07569990..e0270542 100644 --- a/src/Ui/template/wrapper.html +++ b/src/Ui/template/wrapper.html @@ -20,7 +20,6 @@ // If we are inside iframe escape from it if (window.self !== window.top) { window.open(window.location.toString().replace(/([&?])wrapper=False/, "$1").replace(/&$/, "").replace(/[&?]wrapper_nonce=[A-Za-z0-9]+/, ""), "_top"); - document.write("Escaping from iframe..."); window.stop(); document.execCommand("Stop", false); } From 820346c98d9f219a59cef478bf7dc02063d8dda7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:52:51 +0100 Subject: [PATCH 405/483] More logging to wrapper --- src/Ui/media/Loading.coffee | 7 +++++-- src/Ui/media/Wrapper.coffee | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Ui/media/Loading.coffee b/src/Ui/media/Loading.coffee index 7cd2479d..6b4af264 100644 --- a/src/Ui/media/Loading.coffee +++ b/src/Ui/media/Loading.coffee @@ -10,7 +10,7 @@ class Loading $(".progressbar").css("transform": "scaleX(#{parseInt(percent*100)/100})").css("opacity", "1").css("display", "block") hideProgress: -> - console.log "hideProgress" + @log "hideProgress" @timer_hide = setTimeout ( => $(".progressbar").css("transform": "scaleX(1)").css("opacity", "0").hideLater(1000) ), 300 @@ -23,6 +23,7 @@ class Loading showTooLarge: (site_info) -> + @log "Displaying large site confirmation" if $(".console .button-setlimit").length == 0 # Not displaying it yet line = @printLine("Site size: #{parseInt(site_info.settings.size/1024/1024)}MB is larger than default allowed #{parseInt(site_info.size_limit)}MB", "warning") button = $("" + "Open site and set size limit to #{site_info.next_size_limit}MB" + "") @@ -52,7 +53,7 @@ class Loading # We dont need loadingscreen anymore hideScreen: -> - console.log "hideScreen" + @log "hideScreen" if not $(".loadingscreen").hasClass("done") # Only if its not animating already if @screen_visible # Hide with animate $(".loadingscreen").addClass("done").removeLater(2000) @@ -80,6 +81,8 @@ class Loading if type == "warning" then line.addClass("console-warning") return line + log: (args...) -> + console.log "[Loading]", args... window.Loading = Loading diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 74232512..a1988af7 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -417,6 +417,7 @@ class Wrapper @reload(message.params[0]) reload: (url_post="") -> + @log "Reload" current_url = window.location.toString().replace(/#.*/g, "") if url_post if current_url.indexOf("?") > 0 @@ -492,6 +493,7 @@ class Wrapper # Iframe loaded onPageLoad: (e) => + @log "onPageLoad" @inner_loaded = true if not @inner_ready then @sendInner {"cmd": "wrapperReady"} # Inner frame loaded before wrapper #if not @site_error then @loading.hideScreen() # Hide loading screen @@ -553,8 +555,8 @@ class Wrapper @loading.hideScreen() if not @site_info then @reloadSiteInfo() if site_info.content - window.document.title = site_info.content.title+" - ZeroNet" - @log "Required file done, setting title to", window.document.title + window.document.title = site_info.content.title + " - ZeroNet" + @log "Required file #{window.file_inner_path} done, setting title to", window.document.title if not window.show_loadingscreen @notifications.add("modified", "info", "New version of this page has just released.
Reload to see the modified content.") # File failed downloading @@ -675,6 +677,7 @@ class Wrapper setSizeLimit: (size_limit, reload=true) => + @log "setSizeLimit: #{size_limit}, reload: #{reload}" @ws.cmd "siteSetLimit", [size_limit], (res) => if res != "ok" return false From 9085a4b0cce64c4d315f6d9d74a254db7ee5fced Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:53:11 +0100 Subject: [PATCH 406/483] Less frequent update of progress bar --- src/Ui/media/Loading.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ui/media/Loading.coffee b/src/Ui/media/Loading.coffee index 6b4af264..877087c6 100644 --- a/src/Ui/media/Loading.coffee +++ b/src/Ui/media/Loading.coffee @@ -6,7 +6,7 @@ class Loading setProgress: (percent) -> if @timer_hide clearInterval @timer_hide - RateLimit 200, -> + RateLimit 500, -> $(".progressbar").css("transform": "scaleX(#{parseInt(percent*100)/100})").css("opacity", "1").css("display", "block") hideProgress: -> From c1ad7914f1834bdcee945237d1f24e74d812afb1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:53:49 +0100 Subject: [PATCH 407/483] Always update loading screen site too large message with site info received --- src/Ui/media/Wrapper.coffee | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index a1988af7..9346eb3a 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -528,14 +528,11 @@ class Wrapper @address = site_info.address @setSiteInfo site_info - if site_info.settings.size > site_info.size_limit*1024*1024 # Site size too large and not displaying it yet - if @loading.screen_visible - @loading.showTooLarge(site_info) - else - @displayConfirm "Site is larger than allowed: #{(site_info.settings.size/1024/1024).toFixed(1)}MB/#{site_info.size_limit}MB", "Set limit to #{site_info.next_size_limit}MB", => - @ws.cmd "siteSetLimit", [site_info.next_size_limit], (res) => - if res == "ok" - @notifications.add("size_limit", "done", "Site storage limit modified!", 5000) + if site_info.settings.size > site_info.size_limit * 1024 * 1024 and not @loading.screen_visible # Site size too large and not displaying it yet + @displayConfirm "Site is larger than allowed: #{(site_info.settings.size/1024/1024).toFixed(1)}MB/#{site_info.size_limit}MB", "Set limit to #{site_info.next_size_limit}MB", => + @ws.cmd "siteSetLimit", [site_info.next_size_limit], (res) => + if res == "ok" + @notifications.add("size_limit", "done", "Site storage limit modified!", 5000) if site_info.content?.title? window.document.title = site_info.content.title + " - ZeroNet" @@ -586,12 +583,17 @@ class Wrapper @notifications.add("size_limit", "done", "Site storage limit modified!", 5000) return false - if @loading.screen_visible and @inner_loaded and site_info.settings.size < site_info.size_limit*1024*1024 and site_info.settings.size > 0 # Loading screen still visible, but inner loaded + if @loading.screen_visible and @inner_loaded and site_info.settings.size < site_info.size_limit * 1024 * 1024 and site_info.settings.size > 0 # Loading screen still visible, but inner loaded + @log "Loading screen visible, but inner loaded" @loading.hideScreen() if site_info?.settings?.own and site_info?.settings?.modified != @site_info?.settings?.modified @updateModifiedPanel() + if @loading.screen_visible and site_info.settings.size > site_info.size_limit * 1024 * 1024 + @log "Site too large" + @loading.showTooLarge(site_info) + @site_info = site_info @event_site_info.resolve() From 76e4b75c2d4d7bc2580feeb5296586dcf3fd814d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:54:13 +0100 Subject: [PATCH 408/483] Fix removing loading screen without loaded content --- src/Ui/media/Wrapper.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ui/media/Wrapper.coffee b/src/Ui/media/Wrapper.coffee index 9346eb3a..cecd1bc6 100644 --- a/src/Ui/media/Wrapper.coffee +++ b/src/Ui/media/Wrapper.coffee @@ -680,11 +680,12 @@ class Wrapper setSizeLimit: (size_limit, reload=true) => @log "setSizeLimit: #{size_limit}, reload: #{reload}" + @inner_loaded = false # Inner frame not loaded, just a 404 page displayed @ws.cmd "siteSetLimit", [size_limit], (res) => if res != "ok" return false @loading.printLine res - @inner_loaded = false # Inner frame not loaded, just a 404 page displayed + @inner_loaded = false if reload then @reloadIframe() return false From 0dbcec8092edefb1d20f9cdc2c3551bbe6900175 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:54:20 +0100 Subject: [PATCH 409/483] Merge wrapper --- src/Ui/media/all.js | 47 +++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/Ui/media/all.js b/src/Ui/media/all.js index b94be436..eb3dc905 100644 --- a/src/Ui/media/all.js +++ b/src/Ui/media/all.js @@ -625,7 +625,8 @@ $.extend( $.easing, (function() { - var Loading; + var Loading, + slice = [].slice; Loading = (function() { function Loading(wrapper) { @@ -640,7 +641,7 @@ $.extend( $.easing, if (this.timer_hide) { clearInterval(this.timer_hide); } - return RateLimit(200, function() { + return RateLimit(500, function() { return $(".progressbar").css({ "transform": "scaleX(" + (parseInt(percent * 100) / 100) + ")" }).css("opacity", "1").css("display", "block"); @@ -648,7 +649,7 @@ $.extend( $.easing, }; Loading.prototype.hideProgress = function() { - console.log("hideProgress"); + this.log("hideProgress"); return this.timer_hide = setTimeout(((function(_this) { return function() { return $(".progressbar").css({ @@ -666,6 +667,7 @@ $.extend( $.easing, Loading.prototype.showTooLarge = function(site_info) { var button, line; + this.log("Displaying large site confirmation"); if ($(".console .button-setlimit").length === 0) { line = this.printLine("Site size: " + (parseInt(site_info.settings.size / 1024 / 1024)) + "MB is larger than default allowed " + (parseInt(site_info.size_limit)) + "MB", "warning"); button = $("" + ("Open site and set size limit to " + site_info.next_size_limit + "MB") + ""); @@ -711,7 +713,7 @@ $.extend( $.easing, }; Loading.prototype.hideScreen = function() { - console.log("hideScreen"); + this.log("hideScreen"); if (!$(".loadingscreen").hasClass("done")) { if (this.screen_visible) { $(".loadingscreen").addClass("done").removeLater(2000); @@ -759,6 +761,12 @@ $.extend( $.easing, return line; }; + Loading.prototype.log = function() { + var args; + args = 1 <= arguments.length ? slice.call(arguments, 0) : []; + return console.log.apply(console, ["[Loading]"].concat(slice.call(args))); + }; + return Loading; })(); @@ -901,7 +909,6 @@ $.extend( $.easing, }).call(this); - /* ---- Wrapper.coffee ---- */ @@ -1550,6 +1557,7 @@ $.extend( $.easing, if (url_post == null) { url_post = ""; } + this.log("Reload"); current_url = window.location.toString().replace(/#.*/g, ""); if (url_post) { if (current_url.indexOf("?") > 0) { @@ -1665,6 +1673,7 @@ $.extend( $.easing, Wrapper.prototype.onPageLoad = function(e) { var ref; + this.log("onPageLoad"); this.inner_loaded = true; if (!this.inner_ready) { this.sendInner({ @@ -1706,18 +1715,14 @@ $.extend( $.easing, var ref; _this.address = site_info.address; _this.setSiteInfo(site_info); - if (site_info.settings.size > site_info.size_limit * 1024 * 1024) { - if (_this.loading.screen_visible) { - _this.loading.showTooLarge(site_info); - } else { - _this.displayConfirm("Site is larger than allowed: " + ((site_info.settings.size / 1024 / 1024).toFixed(1)) + "MB/" + site_info.size_limit + "MB", "Set limit to " + site_info.next_size_limit + "MB", function() { - return _this.ws.cmd("siteSetLimit", [site_info.next_size_limit], function(res) { - if (res === "ok") { - return _this.notifications.add("size_limit", "done", "Site storage limit modified!", 5000); - } - }); + if (site_info.settings.size > site_info.size_limit * 1024 * 1024 && !_this.loading.screen_visible) { + _this.displayConfirm("Site is larger than allowed: " + ((site_info.settings.size / 1024 / 1024).toFixed(1)) + "MB/" + site_info.size_limit + "MB", "Set limit to " + site_info.next_size_limit + "MB", function() { + return _this.ws.cmd("siteSetLimit", [site_info.next_size_limit], function(res) { + if (res === "ok") { + return _this.notifications.add("size_limit", "done", "Site storage limit modified!", 5000); + } }); - } + }); } if (((ref = site_info.content) != null ? ref.title : void 0) != null) { window.document.title = site_info.content.title + " - ZeroNet"; @@ -1741,7 +1746,7 @@ $.extend( $.easing, } if (site_info.content) { window.document.title = site_info.content.title + " - ZeroNet"; - this.log("Required file done, setting title to", window.document.title); + this.log("Required file " + window.file_inner_path + " done, setting title to", window.document.title); } if (!window.show_loadingscreen) { this.notifications.add("modified", "info", "New version of this page has just released.
Reload to see the modified content."); @@ -1781,11 +1786,16 @@ $.extend( $.easing, } } if (this.loading.screen_visible && this.inner_loaded && site_info.settings.size < site_info.size_limit * 1024 * 1024 && site_info.settings.size > 0) { + this.log("Loading screen visible, but inner loaded"); this.loading.hideScreen(); } if ((site_info != null ? (ref = site_info.settings) != null ? ref.own : void 0 : void 0) && (site_info != null ? (ref1 = site_info.settings) != null ? ref1.modified : void 0 : void 0) !== ((ref2 = this.site_info) != null ? (ref3 = ref2.settings) != null ? ref3.modified : void 0 : void 0)) { this.updateModifiedPanel(); } + if (this.loading.screen_visible && site_info.settings.size > site_info.size_limit * 1024 * 1024) { + this.log("Site too large"); + this.loading.showTooLarge(site_info); + } this.site_info = site_info; return this.event_site_info.resolve(); }; @@ -1927,6 +1937,8 @@ $.extend( $.easing, if (reload == null) { reload = true; } + this.log("setSizeLimit: " + size_limit + ", reload: " + reload); + this.inner_loaded = false; this.ws.cmd("siteSetLimit", [size_limit], (function(_this) { return function(res) { if (res !== "ok") { @@ -1984,6 +1996,7 @@ $.extend( $.easing, }).call(this); + /* ---- WrapperZeroFrame.coffee ---- */ From c5d51c9cab17d82049c5f59bdeb8797644e8cc01 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:54:34 +0100 Subject: [PATCH 410/483] Verify cert in separate function --- src/Content/ContentManager.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Content/ContentManager.py b/src/Content/ContentManager.py index 81b5c0e1..6256a8cd 100644 --- a/src/Content/ContentManager.py +++ b/src/Content/ContentManager.py @@ -802,9 +802,12 @@ class ContentManager(object): def getSignsRequired(self, inner_path, content=None): return 1 # Todo: Multisig - def verifyCert(self, inner_path, content): + def verifyCertSign(self, user_address, user_auth_type, user_name, issuer_address, sign): from Crypt import CryptBitcoin + cert_subject = "%s#%s/%s" % (user_address, user_auth_type, user_name) + return CryptBitcoin.verify(cert_subject, issuer_address, sign) + def verifyCert(self, inner_path, content): rules = self.getRules(inner_path, content) if not rules: @@ -827,12 +830,7 @@ class ContentManager(object): else: raise VerifyError("Invalid cert signer: %s" % domain) - try: - cert_subject = "%s#%s/%s" % (rules["user_address"], content["cert_auth_type"], name) - result = CryptBitcoin.verify(cert_subject, cert_address, content["cert_sign"]) - except Exception as err: - raise VerifyError("Certificate verify error: %s" % err) - return result + return self.verifyCertSign(rules["user_address"], content["cert_auth_type"], name, cert_address, content["cert_sign"]) # Checks if the content.json content is valid # Return: True or False From 0af90aad37df47ac620542a0fca33d241888914f Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:55:08 +0100 Subject: [PATCH 411/483] Maxmind db as download source no longer works --- plugins/Sidebar/SidebarPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index c77805c5..ac9bd476 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -561,7 +561,7 @@ class UiWebsocketPlugin(object): self.log.info("Downloading GeoLite2 City database...") self.cmd("progress", ["geolite-info", _["Downloading GeoLite2 City database (one time only, ~20MB)..."], 0]) db_urls = [ - "https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz", + "https://raw.githubusercontent.com/aemr3/GeoLite2-Database/master/GeoLite2-City.mmdb.gz", "https://raw.githubusercontent.com/texnikru/GeoLite2-Database/master/GeoLite2-City.mmdb.gz" ] for db_url in db_urls: From 39442977dbd800c709663e2ee4c076cf17bafffc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:55:40 +0100 Subject: [PATCH 412/483] Thread safe access and request log updating in optionalmanager --- .../OptionalManager/OptionalManagerPlugin.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/plugins/OptionalManager/OptionalManagerPlugin.py b/plugins/OptionalManager/OptionalManagerPlugin.py index fbb8158c..f01fab65 100644 --- a/plugins/OptionalManager/OptionalManagerPlugin.py +++ b/plugins/OptionalManager/OptionalManagerPlugin.py @@ -17,36 +17,46 @@ def importPluginnedClasses(): def processAccessLog(): + global access_log if access_log: content_db = ContentDbPlugin.content_db if not content_db.conn: return False + + s = time.time() + access_log_prev = access_log + access_log = collections.defaultdict(dict) now = int(time.time()) num = 0 - for site_id in access_log: + for site_id in access_log_prev: content_db.execute( "UPDATE file_optional SET time_accessed = %s WHERE ?" % now, - {"site_id": site_id, "inner_path": list(access_log[site_id].keys())} + {"site_id": site_id, "inner_path": list(access_log_prev[site_id].keys())} ) - num += len(access_log[site_id]) - access_log.clear() + num += len(access_log_prev[site_id]) + + content_db.log.debug("Inserted %s web request stat in %.3fs" % (num, time.time() - s)) def processRequestLog(): + global request_log if request_log: content_db = ContentDbPlugin.content_db if not content_db.conn: return False - cur = content_db.getCursor() + + s = time.time() + request_log_prev = request_log + request_log = collections.defaultdict(lambda: collections.defaultdict(int)) # {site_id: {inner_path1: 1, inner_path2: 1...}} num = 0 - for site_id in request_log: - for inner_path, uploaded in request_log[site_id].items(): + for site_id in request_log_prev: + for inner_path, uploaded in request_log_prev[site_id].items(): content_db.execute( "UPDATE file_optional SET uploaded = uploaded + %s WHERE ?" % uploaded, {"site_id": site_id, "inner_path": inner_path} ) num += 1 - request_log.clear() + content_db.log.debug("Inserted %s file request stat in %.3fs" % (num, time.time() - s)) if "access_log" not in locals().keys(): # To keep between module reloads From 2b5e57e840e49d8e7a6f2fb78dba44a468682dfb Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:55:56 +0100 Subject: [PATCH 413/483] Fix updateing deleted site in contentdb --- plugins/OptionalManager/ContentDbPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index 21193fa0..a77895e8 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -91,7 +91,7 @@ class ContentDbPlugin(object): site_ids_reverse = {val: key for key, val in self.site_ids.items()} for site_id, stats in site_sizes.items(): site_address = site_ids_reverse.get(site_id) - if not site_address: + if not site_address or site_address not in self.sites: self.log.error("Not found site_id: %s" % site_id) continue site = self.sites[site_address] From 03350d7454558439b40cb8fa05d5ed025253be1e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Sat, 4 Jan 2020 16:56:42 +0100 Subject: [PATCH 414/483] Rev4394 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 244b7f80..4f598d09 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4382 + self.rev = 4394 self.argv = argv self.action = None self.test_parser = None From 77c3e43978396a0888ddd54e726c8ef4ce1a7f60 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Tue, 7 Jan 2020 12:34:15 +0300 Subject: [PATCH 415/483] Detect content encoding based on query string (#2385) --- plugins/FilePack/FilePackPlugin.py | 13 +++++-------- src/Ui/UiRequest.py | 7 +++++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/FilePack/FilePackPlugin.py b/plugins/FilePack/FilePackPlugin.py index e4a2dd8e..a095c6d4 100644 --- a/plugins/FilePack/FilePackPlugin.py +++ b/plugins/FilePack/FilePackPlugin.py @@ -21,9 +21,6 @@ def openArchive(archive_path, file_obj=None): if archive_path.endswith("tar.gz"): import tarfile archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:gz") - elif archive_path.endswith("tar.bz2"): - import tarfile - archive_cache[archive_path] = tarfile.open(archive_path, fileobj=file_obj, mode="r:bz2") else: import zipfile archive_cache[archive_path] = zipfile.ZipFile(file_obj or archive_path) @@ -48,7 +45,7 @@ class UiRequestPlugin(object): file_obj = None path_parts = self.parsePath(path) file_path = "%s/%s/%s" % (config.data_dir, path_parts["address"], path_parts["inner_path"]) - match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", file_path) + match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", file_path) archive_path, path_within = match.groups() if archive_path not in archive_cache: site = self.server.site_manager.get(path_parts["address"]) @@ -102,7 +99,7 @@ class UiRequestPlugin(object): class SiteStoragePlugin(object): def isFile(self, inner_path): if ".zip/" in inner_path or ".tar.gz/" in inner_path: - match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))/(.*)", inner_path) + match = re.match("^(.*\.(?:tar.gz|zip))/(.*)", inner_path) archive_inner_path, path_within = match.groups() return super(SiteStoragePlugin, self).isFile(archive_inner_path) else: @@ -130,7 +127,7 @@ class SiteStoragePlugin(object): def walk(self, inner_path, *args, **kwags): if ".zip" in inner_path or ".tar.gz" in inner_path: - match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path) + match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) archive_inner_path, path_within = match.groups() archive = self.openArchive(archive_inner_path) path_within = path_within.lstrip("/") @@ -154,7 +151,7 @@ class SiteStoragePlugin(object): def list(self, inner_path, *args, **kwags): if ".zip" in inner_path or ".tar.gz" in inner_path: - match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path) + match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) archive_inner_path, path_within = match.groups() archive = self.openArchive(archive_inner_path) path_within = path_within.lstrip("/") @@ -181,7 +178,7 @@ class SiteStoragePlugin(object): def read(self, inner_path, mode="rb", **kwargs): if ".zip/" in inner_path or ".tar.gz/" in inner_path: - match = re.match("^(.*\.(?:tar.gz|tar.bz2|zip))(.*)", inner_path) + match = re.match("^(.*\.(?:tar.gz|zip))(.*)", inner_path) archive_inner_path, path_within = match.groups() archive = self.openArchive(archive_inner_path) path_within = path_within.lstrip("/") diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 2d8ebdee..a549d42a 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -707,7 +707,7 @@ class UiRequest(object): return block # Stream a file to client - def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False, header_allow_ajax=False, file_size=None, file_obj=None, path_parts=None): + def actionFile(self, file_path, block_size=64 * 1024, send_header=True, header_length=True, header_noscript=False, header_allow_ajax=False, extra_headers={}, file_size=None, file_obj=None, path_parts=None): file_name = os.path.basename(file_path) if file_size is None: @@ -725,7 +725,10 @@ class UiRequest(object): header_length = False if send_header: - extra_headers = {} + extra_headers = extra_headers.copy() + content_encoding = self.get.get("zeronet_content_encoding", "") + if all(part.strip() in ("gzip", "compress", "deflate", "identity", "br") for part in content_encoding.split(",")): + extra_headers["Content-Encoding"] = content_encoding extra_headers["Accept-Ranges"] = "bytes" if header_length: extra_headers["Content-Length"] = str(file_size) From 224093b3dd9b7ee4f332811cddccbaf123090fa3 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 9 Jan 2020 16:35:05 +0100 Subject: [PATCH 416/483] Rev4397, Fix big file invalid path errors --- plugins/Bigfile/BigfilePlugin.py | 2 +- src/Config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 13254df9..e4b53bdd 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -406,7 +406,7 @@ class SiteStoragePlugin(object): def createSparseFile(self, inner_path, size, sha512=None): file_path = self.getPath(inner_path) - self.ensureDir(os.path.dirname(file_path)) + self.ensureDir(os.path.dirname(inner_path)) f = open(file_path, 'wb') f.truncate(min(1024 * 1024 * 5, size)) # Only pre-allocate up to 5MB diff --git a/src/Config.py b/src/Config.py index 4f598d09..9f6254cd 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4394 + self.rev = 4397 self.argv = argv self.action = None self.test_parser = None From 3edb34ec5601308996304ae77783fc5bfcde66fa Mon Sep 17 00:00:00 2001 From: Eduaddad Date: Wed, 15 Jan 2020 00:53:19 -0300 Subject: [PATCH 417/483] Update pt-br.json more translations --- plugins/UiConfig/languages/pt-br.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/UiConfig/languages/pt-br.json b/plugins/UiConfig/languages/pt-br.json index b95758f2..dc1c7cf3 100644 --- a/plugins/UiConfig/languages/pt-br.json +++ b/plugins/UiConfig/languages/pt-br.json @@ -48,5 +48,9 @@ "Threads for database operations": "Threads para operações de banco de dados", "Sync execution": "Execução de sincronização", "Custom": "Personalizado", - "Custom socks proxy address for trackers": "Endereço de proxy de meias personalizadas para trackers" + "Custom socks proxy address for trackers": "Endereço de proxy de meias personalizadas para trackers", + "Your file server is accessible on these ips. (default: detect automatically)": "Seu servidor de arquivos está acessível nesses ips. (padrão: detectar automaticamente)", + "Detect automatically": "Detectar automaticamente", + "1 CONFIGURATION ITEM VALUE CHANGED": "1 VALOR DO ITEM DE CONFIGURAÇÃO ALTERADO" + } From 914576b9db6a12aeeb6a2e0f4236fefed2b7526e Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Sun, 19 Jan 2020 13:30:22 +0100 Subject: [PATCH 418/483] Change to full PGP fingerprint --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2df14e71..16d36b55 100644 --- a/README.md +++ b/README.md @@ -123,4 +123,4 @@ Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_devel * More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/ * Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet) -* Email: hello@zeronet.io (PGP: CB9613AE) +* Email: hello@zeronet.io (PGP: 960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE) From a16d55c86394a514c4115c9f16d8145f3f4c1d98 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:31:09 +0100 Subject: [PATCH 419/483] Fix compatibility with Snap package --- plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py b/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py index defa9266..fab7bb1f 100644 --- a/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py +++ b/plugins/AnnounceBitTorrent/AnnounceBitTorrentPlugin.py @@ -3,7 +3,7 @@ import urllib.request import struct import socket -import bencode_open +import lib.bencode_open as bencode_open from lib.subtl.subtl import UdpTrackerClient import socks import sockshandler From 3e08eabc86b9c4def0ff6a803d07772c99a75a14 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:31:35 +0100 Subject: [PATCH 420/483] Proper error when piecemap download fails --- plugins/Bigfile/BigfilePlugin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index e4b53bdd..053098a8 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -33,6 +33,7 @@ def importPluginnedClasses(): from Content.ContentManager import VerifyError from Config import config + if "upload_nonces" not in locals(): upload_nonces = {} @@ -340,7 +341,11 @@ class ContentManagerPlugin(object): return piecemap def verifyPiece(self, inner_path, pos, piece): - piecemap = self.getPiecemap(inner_path) + try: + piecemap = self.getPiecemap(inner_path) + except OSError as err: + raise VerifyError("Unable to download piecemap: %s" % err) + piece_i = int(pos / piecemap["piece_size"]) if CryptHash.sha512sum(piece, format="digest") != piecemap["sha512_pieces"][piece_i]: raise VerifyError("Invalid hash") From 2b7aebd89ddcbcf03c28c0cce8a82712157eef3a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:33:30 +0100 Subject: [PATCH 421/483] Fix optional file loading when sites.json load takes more than 1 sec --- plugins/OptionalManager/ContentDbPlugin.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/OptionalManager/ContentDbPlugin.py b/plugins/OptionalManager/ContentDbPlugin.py index a77895e8..f0f8a877 100644 --- a/plugins/OptionalManager/ContentDbPlugin.py +++ b/plugins/OptionalManager/ContentDbPlugin.py @@ -24,7 +24,7 @@ class ContentDbPlugin(object): self.time_peer_numbers_updated = 0 self.my_optional_files = {} # Last 50 site_address/inner_path called by fileWrite (auto-pinning these files) self.optional_files = collections.defaultdict(dict) - self.optional_files_loading = False + self.optional_files_loaded = False self.timer_check_optional = helper.timer(60 * 5, self.checkOptionalLimit) super(ContentDbPlugin, self).__init__(*args, **kwargs) @@ -60,9 +60,6 @@ class ContentDbPlugin(object): super(ContentDbPlugin, self).initSite(site) if self.need_filling: self.fillTableFileOptional(site) - if not self.optional_files_loading: - site.greenlet_manager.spawnLater(1, self.loadFilesOptional) - self.optional_files_loading = True def checkTables(self): changed_tables = super(ContentDbPlugin, self).checkTables() @@ -100,7 +97,7 @@ class ContentDbPlugin(object): total += stats["size_optional"] total_downloaded += stats["optional_downloaded"] - self.log.debug( + self.log.info( "Loaded %s optional files: %.2fMB, downloaded: %.2fMB in %.3fs" % (num, float(total) / 1024 / 1024, float(total_downloaded) / 1024 / 1024, time.time() - s) ) @@ -108,7 +105,7 @@ class ContentDbPlugin(object): if self.need_filling and self.getOptionalLimitBytes() >= 0 and self.getOptionalLimitBytes() < total_downloaded: limit_bytes = self.getOptionalLimitBytes() limit_new = round((float(total_downloaded) / 1024 / 1024 / 1024) * 1.1, 2) # Current limit + 10% - self.log.debug( + self.log.info( "First startup after update and limit is smaller than downloaded files size (%.2fGB), increasing it from %.2fGB to %.2fGB" % (float(total_downloaded) / 1024 / 1024 / 1024, float(limit_bytes) / 1024 / 1024 / 1024, limit_new) ) @@ -405,3 +402,13 @@ class ContentDbPlugin(object): for file_id in deleted_file_ids: cur.execute("UPDATE file_optional SET is_downloaded = 0, is_pinned = 0, peer = peer - 1 WHERE ?", {"file_id": file_id}) cur.close() + + +@PluginManager.registerTo("SiteManager") +class SiteManagerPlugin(object): + def load(self, *args, **kwargs): + back = super(SiteManagerPlugin, self).load(*args, **kwargs) + if self.sites and not content_db.optional_files_loaded and content_db.conn: + content_db.optional_files_loaded = True + content_db.loadFilesOptional() + return back \ No newline at end of file From e75e199334571d32111a35359993f3e70da8d077 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:33:54 +0100 Subject: [PATCH 422/483] Fix multi-line log events display in web console --- plugins/Sidebar/ConsolePlugin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/ConsolePlugin.py b/plugins/Sidebar/ConsolePlugin.py index 30d00fee..15f6a1ba 100644 --- a/plugins/Sidebar/ConsolePlugin.py +++ b/plugins/Sidebar/ConsolePlugin.py @@ -58,9 +58,16 @@ class UiWebsocketPlugin(object): assert SafeRe.isSafePattern(filter) filter_re = re.compile(".*" + filter) + last_match = False for line in log_file: - if filter and not filter_re.match(line): + if not line.startswith("[") and last_match: # Multi-line log entry + lines.append(line.replace(" ", " ")) continue + + if filter and not filter_re.match(line): + last_match = False + continue + last_match = True lines.append(line) num_found = len(lines) From a9368bb3c8dbbccfcb95e2d335ef3427f2955196 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:35:40 +0100 Subject: [PATCH 423/483] Don't allow parallel sites.json loading --- src/Site/SiteManager.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index f9477b69..a6ff4fee 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -7,6 +7,7 @@ import atexit import gevent +import util from Plugin import PluginManager from Content import ContentDb from Config import config @@ -27,12 +28,15 @@ class SiteManager(object): atexit.register(lambda: self.save(recalculate_size=True)) # Load all sites from data/sites.json + @util.Noparallel() def load(self, cleanup=True, startup=False): - self.log.debug("Loading sites...") + from Debug import Debug + self.log.info("Loading sites... (cleanup: %s, startup: %s)" % (cleanup, startup)) self.loaded = False from .Site import Site address_found = [] added = 0 + load_s = time.time() # Load new adresses try: json_path = "%s/sites.json" % config.data_dir @@ -87,7 +91,7 @@ class SiteManager(object): del content_db.sites[address] if added: - self.log.debug("SiteManager added %s sites" % added) + self.log.info("Added %s sites in %.3fs" % (added, time.time() - load_s)) self.loaded = True def saveDelayed(self): @@ -196,7 +200,7 @@ class SiteManager(object): def delete(self, address): self.sites_changed = int(time.time()) - self.log.debug("SiteManager deleted site: %s" % address) + self.log.debug("Deleted site: %s" % address) del(self.sites[address]) # Delete from sites.json self.save() From 62a2ec7254172bbfd80a51fa2a6b93a0a49010b1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:36:33 +0100 Subject: [PATCH 424/483] Make sure to commit before vacuum --- src/Db/DbCursor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Db/DbCursor.py b/src/Db/DbCursor.py index 7d0c1626..acb8846d 100644 --- a/src/Db/DbCursor.py +++ b/src/Db/DbCursor.py @@ -83,8 +83,6 @@ class DbCursor: return query, params def execute(self, query, params=None): - if query.upper().strip("; ") == "VACUUM": - self.db.commit("vacuum called") query = query.strip() while self.db.progress_sleeping or self.db.commiting: time.sleep(0.1) @@ -101,6 +99,8 @@ class DbCursor: try: s = time.time() self.db.lock.acquire(True) + if query.upper().strip("; ") == "VACUUM": + self.db.commit("vacuum called") if params: res = cursor.execute(query, params) else: From 835174270ed3906fb01675e1d4d972e4a625a675 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:36:52 +0100 Subject: [PATCH 425/483] Less wait for closing cursors --- src/Db/Db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Db/Db.py b/src/Db/Db.py index b7cfd501..dab87d48 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -221,7 +221,7 @@ class Db(object): self.need_commit = False self.commit("Closing: %s" % reason) self.log.debug("Close called by %s" % Debug.formatStack()) - for i in range(10): + for i in range(5): if len(self.cursors) == 0: break self.log.debug("Pending cursors: %s" % len(self.cursors)) From 238ede94193ddd1a932189a134b344f4b03ce809 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:37:07 +0100 Subject: [PATCH 426/483] Only correct time if we have at least 9 connected peers --- src/Connection/ConnectionServer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index f2765d58..55be636d 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -360,7 +360,7 @@ class ConnectionServer(object): for connection in self.connections if connection.handshake.get("time") and connection.last_ping_delay ]) - if len(corrections) < 6: + if len(corrections) < 9: return 0.0 mid = int(len(corrections) / 2 - 1) median = (corrections[mid - 1] + corrections[mid] + corrections[mid + 1]) / 3 From ac8aaaff75ce784815d0bb004af1d2f6f34e61ab Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 22 Jan 2020 16:37:48 +0100 Subject: [PATCH 427/483] Rev4399 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9f6254cd..9c7bd18c 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4397 + self.rev = 4399 self.argv = argv self.action = None self.test_parser = None From 4d8ee4bafba62025d820603c44c8e95e7f6ea2e9 Mon Sep 17 00:00:00 2001 From: Eduaddad Date: Wed, 22 Jan 2020 13:54:19 -0300 Subject: [PATCH 428/483] CONFIGURATION ITEM VALUE CHANGED - should work now CONFIGURATION ITEM VALUE CHANGED - should work now --- plugins/UiConfig/languages/pt-br.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UiConfig/languages/pt-br.json b/plugins/UiConfig/languages/pt-br.json index dc1c7cf3..03c34505 100644 --- a/plugins/UiConfig/languages/pt-br.json +++ b/plugins/UiConfig/languages/pt-br.json @@ -51,6 +51,6 @@ "Custom socks proxy address for trackers": "Endereço de proxy de meias personalizadas para trackers", "Your file server is accessible on these ips. (default: detect automatically)": "Seu servidor de arquivos está acessível nesses ips. (padrão: detectar automaticamente)", "Detect automatically": "Detectar automaticamente", - "1 CONFIGURATION ITEM VALUE CHANGED": "1 VALOR DO ITEM DE CONFIGURAÇÃO ALTERADO" + "% CONFIGURATION ITEM VALUE CHANGED": "% VALOR DO ITEM DE CONFIGURAÇÃO ALTERADO" } From 849d514f28718ade5c3e8e550b8bbaf8a9345daa Mon Sep 17 00:00:00 2001 From: Eduaddad Date: Wed, 22 Jan 2020 14:00:19 -0300 Subject: [PATCH 429/483] added to translation Save as .Zip added to translation Save as .Zip --- plugins/Sidebar/languages/pt-br.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/languages/pt-br.json b/plugins/Sidebar/languages/pt-br.json index 06687ce2..acc7279a 100644 --- a/plugins/Sidebar/languages/pt-br.json +++ b/plugins/Sidebar/languages/pt-br.json @@ -92,5 +92,6 @@ "Site settings saved!": "Definições do site salvas!", "Enter your private key:": "Digite sua chave privada:", " Signed!": " Assinado!", - "WebGL not supported": "WebGL não é suportado" + "WebGL not supported": "WebGL não é suportado", + "Save as .Zip": "Salvar como .Zip" } From df93fa0ffee32f513e04fd7f0838be5e4cb5fd8a Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Fri, 24 Jan 2020 14:58:37 +0100 Subject: [PATCH 430/483] Add PGP public key link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16d36b55..f48d7c32 100644 --- a/README.md +++ b/README.md @@ -123,4 +123,4 @@ Next steps: [ZeroNet Developer Documentation](https://zeronet.io/docs/site_devel * More info, help, changelog, zeronet sites: https://www.reddit.com/r/zeronet/ * Come, chat with us: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) or on [gitter](https://gitter.im/HelloZeroNet/ZeroNet) -* Email: hello@zeronet.io (PGP: 960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE) +* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc)) From 11415fe0822652eac53e2193a0ba398bef43a31b Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 24 Jan 2020 16:05:19 +0100 Subject: [PATCH 431/483] Log mock ws caller to get more detail on random test fail --- src/Test/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Test/conftest.py b/src/Test/conftest.py index 9f5a1f32..cc4d5faf 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -354,7 +354,7 @@ def ui_websocket(site, user): self.result = gevent.event.AsyncResult() def send(self, data): - logging.debug("WsMock: Set result (data: %s)" % data) + logging.debug("WsMock: Set result (data: %s) called by %s" % (data, Debug.formatStack())) self.result.set(json.loads(data)["result"]) def getResult(self): From 6dae187e226c43f29657ab8ee3c46093b6752d83 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 28 Jan 2020 16:56:35 +0100 Subject: [PATCH 432/483] More detailed logging on write error --- src/Worker/Worker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Worker/Worker.py b/src/Worker/Worker.py index b8eb4471..4cf04d97 100644 --- a/src/Worker/Worker.py +++ b/src/Worker/Worker.py @@ -114,9 +114,9 @@ class Worker(object): task["site"].storage.write(task["inner_path"], buff) except Exception as err: if type(err) == Debug.Notify: - self.manager.log.debug("%s: Write aborted: %s (%s)" % (self.key, task["inner_path"], err)) + self.manager.log.debug("%s: Write aborted: %s (%s: %s)" % (self.key, task["inner_path"], type(err), err)) else: - self.manager.log.error("%s: Error writing: %s (%s)" % (self.key, task["inner_path"], err)) + self.manager.log.error("%s: Error writing: %s (%s: %s)" % (self.key, task["inner_path"], type(err), err)) raise WorkerIOError(str(err)) def onTaskVerifyFail(self, task, error_message): @@ -226,7 +226,7 @@ class Worker(object): def skip(self, reason="Unknown"): self.manager.log.debug("%s: Force skipping (reason: %s)" % (self.key, reason)) if self.thread: - self.thread.kill(exception=Debug.Notify("Worker stopped")) + self.thread.kill(exception=Debug.Notify("Worker skipping (reason: %s)" % reason)) self.start() # Force stop the worker @@ -234,6 +234,6 @@ class Worker(object): self.manager.log.debug("%s: Force stopping (reason: %s)" % (self.key, reason)) self.running = False if self.thread: - self.thread.kill(exception=Debug.Notify("Worker stopped")) + self.thread.kill(exception=Debug.Notify("Worker stopped (reason: %s)" % reason)) del self.thread self.manager.removeWorker(self) From 46210b2f0418f69c6305fb76953bfbf7108a8fdf Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 28 Jan 2020 16:57:20 +0100 Subject: [PATCH 433/483] Use peer ip in peer exchange if no active connection --- src/Peer/Peer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Peer/Peer.py b/src/Peer/Peer.py index b2cef0c1..03cc1f47 100644 --- a/src/Peer/Peer.py +++ b/src/Peer/Peer.py @@ -345,7 +345,10 @@ class Peer(object): back[hash] += list(map(unpacker_func, peers)) for hash in res.get("my", []): - back[hash].append((self.connection.ip, self.connection.port)) + if self.connection: + back[hash].append((self.connection.ip, self.connection.port)) + else: + back[hash].append((self.ip, self.port)) return back From 2e9cff928c68ec69d9d680d259c1dc173b222406 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 28 Jan 2020 16:58:14 +0100 Subject: [PATCH 434/483] Skip commit if already commiting --- src/Db/Db.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Db/Db.py b/src/Db/Db.py index dab87d48..d1d9ce15 100644 --- a/src/Db/Db.py +++ b/src/Db/Db.py @@ -159,6 +159,10 @@ class Db(object): self.log.debug("Commit ignored: No connection") return False + if self.commiting: + self.log.debug("Commit ignored: Already commiting") + return False + try: s = time.time() self.commiting = True From a0f5e1bde8054ec4aa990eca0c79dc8cd5f2fc7e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 28 Jan 2020 16:58:46 +0100 Subject: [PATCH 435/483] Fix translations --- plugins/Sidebar/languages/pt-br.json | 2 +- plugins/UiConfig/languages/pt-br.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Sidebar/languages/pt-br.json b/plugins/Sidebar/languages/pt-br.json index acc7279a..d5659171 100644 --- a/plugins/Sidebar/languages/pt-br.json +++ b/plugins/Sidebar/languages/pt-br.json @@ -93,5 +93,5 @@ "Enter your private key:": "Digite sua chave privada:", " Signed!": " Assinado!", "WebGL not supported": "WebGL não é suportado", - "Save as .Zip": "Salvar como .Zip" + "Save as .zip": "Salvar como .zip" } diff --git a/plugins/UiConfig/languages/pt-br.json b/plugins/UiConfig/languages/pt-br.json index 03c34505..6235606e 100644 --- a/plugins/UiConfig/languages/pt-br.json +++ b/plugins/UiConfig/languages/pt-br.json @@ -51,6 +51,6 @@ "Custom socks proxy address for trackers": "Endereço de proxy de meias personalizadas para trackers", "Your file server is accessible on these ips. (default: detect automatically)": "Seu servidor de arquivos está acessível nesses ips. (padrão: detectar automaticamente)", "Detect automatically": "Detectar automaticamente", - "% CONFIGURATION ITEM VALUE CHANGED": "% VALOR DO ITEM DE CONFIGURAÇÃO ALTERADO" + " configuration item value changed": " valor do item de configuração alterado" } From 10c02c31c24b12df5314818c5cc2bba11c749074 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 28 Jan 2020 16:59:03 +0100 Subject: [PATCH 436/483] Rev4401 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 9c7bd18c..3bc695b5 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4399 + self.rev = 4401 self.argv = argv self.action = None self.test_parser = None From 8e79a7da635e4a9ed028571c3b7e82d3c86fef24 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:37:37 +0100 Subject: [PATCH 437/483] Fix incomplete loading of dbschema.json --- src/Site/SiteStorage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Site/SiteStorage.py b/src/Site/SiteStorage.py index 6e5f3541..c12a80b0 100644 --- a/src/Site/SiteStorage.py +++ b/src/Site/SiteStorage.py @@ -67,6 +67,7 @@ class SiteStorage(object): def getDbSchema(self): try: + self.site.needFile("dbschema.json") schema = self.loadJson("dbschema.json") except Exception as err: raise Exception("dbschema.json is not a valid JSON: %s" % err) From 6d425f30fe30a00ff3e7f681c1b7d8ee6a3e001d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:38:42 +0100 Subject: [PATCH 438/483] Stop checkconnections with connectionserver --- src/Connection/ConnectionServer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Connection/ConnectionServer.py b/src/Connection/ConnectionServer.py index 55be636d..8d377aca 100644 --- a/src/Connection/ConnectionServer.py +++ b/src/Connection/ConnectionServer.py @@ -47,6 +47,7 @@ class ConnectionServer(object): self.stream_server_proxy = None self.running = False self.stopping = False + self.thread_checker = None self.stat_recv = defaultdict(lambda: defaultdict(int)) self.stat_sent = defaultdict(lambda: defaultdict(int)) @@ -111,11 +112,14 @@ class ConnectionServer(object): except Exception as err: self.log.info("StreamServer listen error: %s" % err) return False + self.log.debug("Stopped.") def stop(self): - self.log.debug("Stopping") + self.log.debug("Stopping %s" % self.stream_server) self.stopping = True self.running = False + if self.thread_checker: + gevent.kill(self.thread_checker) if self.stream_server: self.stream_server.stop() @@ -244,9 +248,9 @@ class ConnectionServer(object): def checkConnections(self): run_i = 0 + time.sleep(15) while self.running: run_i += 1 - time.sleep(15) # Check every minute self.ip_incoming = {} # Reset connected ips counter last_message_time = 0 s = time.time() @@ -323,6 +327,8 @@ class ConnectionServer(object): if time.time() - s > 0.01: self.log.debug("Connection cleanup in %.3fs" % (time.time() - s)) + + time.sleep(15) self.log.debug("Checkconnections ended") @util.Noparallel(blocking=False) From c91f2f0a09111c8dd2da68d6ae637a90bf6dc93d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:40:04 +0100 Subject: [PATCH 439/483] Move all optional file download to separate button on sidebar --- plugins/Sidebar/SidebarPlugin.py | 5 ++--- plugins/Sidebar/media/Sidebar.coffee | 8 ++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/Sidebar/SidebarPlugin.py b/plugins/Sidebar/SidebarPlugin.py index ac9bd476..e6087adf 100644 --- a/plugins/Sidebar/SidebarPlugin.py +++ b/plugins/Sidebar/SidebarPlugin.py @@ -315,7 +315,7 @@ class UiWebsocketPlugin(object): body.append(_("""
  • - +
    """)) @@ -326,6 +326,7 @@ class UiWebsocketPlugin(object): MB {_[Set]} + {_[Download previous files]}
  • """)) body.append("") @@ -754,8 +755,6 @@ class UiWebsocketPlugin(object): @flag.no_multiuser def actionSiteSetAutodownloadoptional(self, to, owned): self.site.settings["autodownloadoptional"] = bool(owned) - self.site.bad_files = {} - gevent.spawn(self.site.update, check_files=True) self.site.worker_manager.removeSolvedFileTasks() @flag.no_multiuser diff --git a/plugins/Sidebar/media/Sidebar.coffee b/plugins/Sidebar/media/Sidebar.coffee index b65912d4..47c6e7f8 100644 --- a/plugins/Sidebar/media/Sidebar.coffee +++ b/plugins/Sidebar/media/Sidebar.coffee @@ -372,6 +372,14 @@ class Sidebar extends Class @updateHtmlTag() return false + # Site start download optional files + @tag.find("#button-autodownload_previous").off("click touchend").on "click touchend", => + @wrapper.ws.cmd "siteUpdate", {"address": @wrapper.site_info.address, "check_files": true}, => + @wrapper.notifications.add "done-download_optional", "done", "Optional files downloaded", 5000 + + @wrapper.notifications.add "start-download_optional", "info", "Optional files download started", 5000 + return false + # Database reload @tag.find("#button-dbreload").off("click touchend").on "click touchend", => @wrapper.ws.cmd "dbReload", [], => From 95bf4ecb429889e3ef52c1f204c74c4741f1bf53 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:40:27 +0100 Subject: [PATCH 440/483] Read 5MB of logs for non-default console tabs --- plugins/Sidebar/media/Console.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Sidebar/media/Console.coffee b/plugins/Sidebar/media/Console.coffee index 4ef9a4fd..724ebb94 100644 --- a/plugins/Sidebar/media/Console.coffee +++ b/plugins/Sidebar/media/Console.coffee @@ -183,7 +183,7 @@ class Console extends Class if @filter == "" @read_size = 32 * 1024 else - @read_size = 1024 * 1024 + @read_size = 5 * 1024 * 1024 @loadConsoleText() handleTabClick: (e) => From a3546d56b0ec56d43d8168fbc703e1d8ae286e31 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:42:26 +0100 Subject: [PATCH 441/483] Merge js --- plugins/Sidebar/media/all.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/plugins/Sidebar/media/all.js b/plugins/Sidebar/media/all.js index 3480ecfe..f83a5c5c 100644 --- a/plugins/Sidebar/media/all.js +++ b/plugins/Sidebar/media/all.js @@ -55,6 +55,7 @@ }).call(this); + /* ---- Console.coffee ---- */ @@ -330,7 +331,7 @@ if (this.filter === "") { this.read_size = 32 * 1024; } else { - this.read_size = 1024 * 1024; + this.read_size = 5 * 1024 * 1024; } return this.loadConsoleText(); }; @@ -436,6 +437,7 @@ }).call(this); + /* ---- RateLimit.coffee ---- */ @@ -464,6 +466,7 @@ }).call(this); + /* ---- Scrollable.js ---- */ @@ -601,9 +604,9 @@ window.initScrollable = function () { this.globe = null; this.preload_html = null; this.original_set_site_info = this.wrapper.setSiteInfo; - if (false) { + if (window.top.location.hash === "#ZeroNet:OpenSidebar") { this.startDrag(); - this.moved(); + this.moved("x"); this.fixbutton_targetx = this.fixbutton_initx - this.width; this.stopDrag(); } @@ -811,9 +814,9 @@ window.initScrollable = function () { return false; }; })(this)); - return this.tag.find("#privatekey-forgot").off("click, touchend").on("click touchend", (function(_this) { + return this.tag.find("#privatekey-forget").off("click, touchend").on("click touchend", (function(_this) { return function(e) { - _this.wrapper.displayConfirm("Remove saved private key for this site?", "Forgot", function(res) { + _this.wrapper.displayConfirm("Remove saved private key for this site?", "Forget", function(res) { if (!res) { return false; } @@ -1013,6 +1016,18 @@ window.initScrollable = function () { return false; }; })(this)); + this.tag.find("#button-autodownload_previous").off("click touchend").on("click touchend", (function(_this) { + return function() { + _this.wrapper.ws.cmd("siteUpdate", { + "address": _this.wrapper.site_info.address, + "check_files": true + }, function() { + return _this.wrapper.notifications.add("done-download_optional", "done", "Optional files downloaded", 5000); + }); + _this.wrapper.notifications.add("start-download_optional", "info", "Optional files download started", 5000); + return false; + }; + })(this)); this.tag.find("#button-dbreload").off("click touchend").on("click touchend", (function(_this) { return function() { _this.wrapper.ws.cmd("dbReload", [], function() { @@ -1345,6 +1360,7 @@ window.initScrollable = function () { }).call(this); + /* ---- morphdom.js ---- */ From 037f0a3ff43cafb39270c45e0773068a45eca63a Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 7 Feb 2020 16:43:23 +0100 Subject: [PATCH 442/483] Rev4404 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 3bc695b5..7623e575 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4401 + self.rev = 4404 self.argv = argv self.action = None self.test_parser = None From 28ce08de8ef1455f7f4d23382d74fe5204e0900d Mon Sep 17 00:00:00 2001 From: tangdou1 <35254744+tangdou1@users.noreply.github.com> Date: Tue, 11 Feb 2020 23:12:06 +0800 Subject: [PATCH 443/483] Update zh.json (#2413) * Update zh.json * Update zh.json * Update zh.json --- plugins/Sidebar/languages/zh.json | 5 ++++- plugins/UiConfig/languages/zh.json | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/plugins/Sidebar/languages/zh.json b/plugins/Sidebar/languages/zh.json index 696084cf..639ac7f6 100644 --- a/plugins/Sidebar/languages/zh.json +++ b/plugins/Sidebar/languages/zh.json @@ -27,8 +27,11 @@ "Optional files": "可选文件", "Downloaded": "已下载", - "Download and help distribute all files": "下载并帮助分发所有文件", + "Help distribute added optional files": "帮助分发新的可选文件", "Auto download big file size limit": "自动下载大文件大小限制", + "Download previous files": "下载之前的文件", + "Optional files download started": "可选文件下载启动", + "Optional files downloaded": "可选文件下载完成", "Total size": "总大小", "Downloaded files": "已下载文件", diff --git a/plugins/UiConfig/languages/zh.json b/plugins/UiConfig/languages/zh.json index 6d6e9f80..9240b249 100644 --- a/plugins/UiConfig/languages/zh.json +++ b/plugins/UiConfig/languages/zh.json @@ -41,6 +41,19 @@ "Everything": "记录全部", "Only important messages": "仅记录重要信息", "Only errors": "仅记录错误", + "Threads for async file system reads": "文件系统异步读取线程", + "Threads for async file system writes": "文件系统异步写入线程", + "Threads for cryptographic functions": "密码函数线程", + "Threads for database operations": "数据库操作线程", + "Sync read": "同步读取", + "Sync write": "同步写入", + "Sync execution": "同步执行", + "1 thread": "1个线程", + "2 threads": "2个线程", + "3 threads": "3个线程", + "4 threads": "4个线程", + "5 threads": "5个线程", + "10 threads": "10个线程", " configuration item value changed": " 个配置值已经改变", "Save settings": "保存配置", From fefd2474b108eb4b8a38f5c4a58777501552067e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:22:09 +0100 Subject: [PATCH 444/483] Don't reload sites on listing --- src/Ui/UiWebsocket.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index d8441af6..e4819644 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -877,7 +877,6 @@ class UiWebsocket(object): @flag.admin def actionSiteList(self, to, connecting_sites=False): ret = [] - SiteManager.site_manager.load() # Reload sites for site in list(self.server.sites.values()): if not site.content_manager.contents.get("content.json") and not connecting_sites: continue # Incomplete site From 113b57415f2dee9227c50811e70e52671d215c11 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:22:37 +0100 Subject: [PATCH 445/483] More detailed info on origin error --- src/Ui/UiRequest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index a549d42a..7c019a64 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -779,8 +779,9 @@ class UiRequest(object): if origin: origin_host = origin.split("://", 1)[-1] if origin_host != host and origin_host not in self.server.allowed_ws_origins: - ws.send(json.dumps({"error": "Invalid origin: %s" % origin})) - return self.error403("Invalid origin: %s" % origin) + error_message = "Invalid origin: %s (host: %s, allowed: %s)" % (origin, host, self.server.allowed_ws_origins) + ws.send(json.dumps({"error": error_message})) + return self.error403(error_message) # Find site by wrapper_key wrapper_key = self.get["wrapper_key"] From d36324e0d357f5286eaff78136ef11a5991fff5e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:23:00 +0100 Subject: [PATCH 446/483] More detailed info on http host error --- src/Ui/UiRequest.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 7c019a64..27fbfd72 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -112,8 +112,14 @@ class UiRequest(object): self_host = self.env["HTTP_HOST"].split(":")[0] self_ip = self.env["HTTP_HOST"].replace(self_host, socket.gethostbyname(self_host)) link = "http://{0}{1}".format(self_ip, http_get) - ret_link = """

    Access via ip: {0}""".format(html.escape(link)).encode("utf8") - return iter([ret_error, ret_link]) + ret_body = """ +

    Start the client with --ui_host "{host}" argument

    +

    or access via ip: {link}

    + """.format( + host=html.escape(self.env["HTTP_HOST"]), + link=html.escape(link) + ).encode("utf8") + return iter([ret_error, ret_body]) # Prepend .bit host for transparent proxy if self.isDomain(self.env.get("HTTP_HOST")): @@ -907,7 +913,8 @@ class UiRequest(object): else: return """

    %s

    %s

    From d2627f36d5cce66f6bfdcf22843554b50b9a68fa Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:23:37 +0100 Subject: [PATCH 447/483] Pass all arguments on site need --- src/Site/SiteManager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index a6ff4fee..73696688 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -165,7 +165,7 @@ class SiteManager(object): return site - def add(self, address, all_file=False, settings=None): + def add(self, address, all_file=False, settings=None, **kwargs): from .Site import Site self.sites_changed = int(time.time()) # Try to find site with differect case @@ -187,7 +187,7 @@ class SiteManager(object): return site # Return or create site and start download site files - def need(self, address, all_file=True, settings=None): + def need(self, address, *args, **kwargs): if self.isDomainCached(address): address_resolved = self.resolveDomainCached(address) if address_resolved: @@ -195,7 +195,7 @@ class SiteManager(object): site = self.get(address) if not site: # Site not exist yet - site = self.add(address, all_file=all_file, settings=settings) + site = self.add(address, *args, **kwargs) return site def delete(self, address): From 61ac6a30d306efeaa99659398e3832a2c1160ffe Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:24:22 +0100 Subject: [PATCH 448/483] Fix loading blocked raw sites --- plugins/TranslateSite/TranslateSitePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/TranslateSite/TranslateSitePlugin.py b/plugins/TranslateSite/TranslateSitePlugin.py index 01521921..d82d0ebe 100644 --- a/plugins/TranslateSite/TranslateSitePlugin.py +++ b/plugins/TranslateSite/TranslateSitePlugin.py @@ -25,6 +25,8 @@ class UiRequestPlugin(object): file_generator = super(UiRequestPlugin, self).actionSiteMedia(path, **kwargs) if "__next__" in dir(file_generator): # File found and generator returned site = self.server.sites.get(path_parts["address"]) + if not site or not site.content_manager.contents.get("content.json"): + return file_generator return self.actionPatchFile(site, path_parts["inner_path"], file_generator) else: return file_generator @@ -45,8 +47,6 @@ class UiRequestPlugin(object): def actionPatchFile(self, site, inner_path, file_generator): content_json = site.content_manager.contents.get("content.json") - if not content_json: - return file_generator lang_file = "languages/%s.json" % translate.lang lang_file_exist = False if site.settings.get("own"): # My site, check if the file is exist (allow to add new lang without signing) From 70cc982e2e925c356111eb7dbbe1b086a9ec2852 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:24:59 +0100 Subject: [PATCH 449/483] Log actual disabled function for multiuser plugin --- plugins/disabled-Multiuser/MultiuserPlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/disabled-Multiuser/MultiuserPlugin.py b/plugins/disabled-Multiuser/MultiuserPlugin.py index e7eabdf1..799c3337 100644 --- a/plugins/disabled-Multiuser/MultiuserPlugin.py +++ b/plugins/disabled-Multiuser/MultiuserPlugin.py @@ -7,6 +7,7 @@ from Plugin import PluginManager from Crypt import CryptBitcoin from . import UserPlugin from util.Flag import flag +from Translate import translate as _ # We can only import plugin host clases after the plugins are loaded @PluginManager.afterLoad @@ -212,7 +213,7 @@ class UiWebsocketPlugin(object): flags = flag.db.get(self.getCmdFuncName(cmd), ()) is_public_proxy_user = not config.multiuser_local and self.user.master_address not in local_master_addresses if is_public_proxy_user and "no_multiuser" in flags: - self.cmd("notification", ["info", "This function is disabled on this proxy!"]) + self.cmd("notification", ["info", _("This function ({cmd}) is disabled on this proxy!")]) return False else: return super(UiWebsocketPlugin, self).hasCmdPermission(cmd) From bc76bf291a48411bdaa1071883d216bbf02b10cc Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:26:15 +0100 Subject: [PATCH 450/483] Fix site blocklist with address hash based blocking and move checking to server-side --- plugins/ContentFilter/ContentFilterPlugin.py | 47 ++++++++++++++++--- plugins/ContentFilter/ContentFilterStorage.py | 24 ++++++++-- plugins/ContentFilter/media/blocklisted.html | 22 +-------- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/plugins/ContentFilter/ContentFilterPlugin.py b/plugins/ContentFilter/ContentFilterPlugin.py index 4d5d1b4c..f2f84b49 100644 --- a/plugins/ContentFilter/ContentFilterPlugin.py +++ b/plugins/ContentFilter/ContentFilterPlugin.py @@ -1,7 +1,6 @@ import time import re import html -import hashlib import os from Plugin import PluginManager @@ -26,9 +25,20 @@ class SiteManagerPlugin(object): filter_storage = ContentFilterStorage(site_manager=self) def add(self, address, *args, **kwargs): - if filter_storage.isSiteblocked(address): - details = filter_storage.getSiteblockDetails(address) - raise Exception("Site blocked: %s" % html.escape(details.get("reason", "unknown reason"))) + should_ignore_block = kwargs.get("ignore_block") or kwargs.get("settings") + if should_ignore_block: + block_details = None + elif filter_storage.isSiteblocked(address): + block_details = filter_storage.getSiteblockDetails(address) + else: + address_hashed = filter_storage.getSiteAddressHashed(address) + if filter_storage.isSiteblocked(address_hashed): + block_details = filter_storage.getSiteblockDetails(address_hashed) + else: + block_details = None + + if block_details: + raise Exception("Site blocked: %s" % html.escape(block_details.get("reason", "unknown reason"))) else: return super(SiteManagerPlugin, self).add(address, *args, **kwargs) @@ -79,6 +89,17 @@ class UiWebsocketPlugin(object): self.response(to, filter_storage.file_content["mutes"]) # Siteblock + @flag.no_multiuser + @flag.admin + def actionSiteblockIgnoreAddSite(self, to, site_address): + if site_address in filter_storage.site_manager.sites: + return {"error": "Site already added"} + else: + if filter_storage.site_manager.need(site_address, ignore_block=True): + return "ok" + else: + return {"error": "Invalid address"} + @flag.no_multiuser @flag.admin def actionSiteblockAdd(self, to, site_address, reason=None): @@ -97,6 +118,18 @@ class UiWebsocketPlugin(object): def actionSiteblockList(self, to): self.response(to, filter_storage.file_content["siteblocks"]) + @flag.admin + def actionSiteblockGet(self, to, site_address): + if filter_storage.isSiteblocked(site_address): + res = filter_storage.getSiteblockDetails(site_address) + else: + site_address_hashed = filter_storage.getSiteAddressHashed(site_address) + if filter_storage.isSiteblocked(site_address_hashed): + res = filter_storage.getSiteblockDetails(site_address_hashed) + else: + res = {"error": "Site block not found"} + self.response(to, res) + # Include @flag.no_multiuser def actionFilterIncludeAdd(self, to, inner_path, description=None, address=None): @@ -202,11 +235,11 @@ class UiRequestPlugin(object): address = self.resolveDomain(address) if address: - address_sha256 = "0x" + hashlib.sha256(address.encode("utf8")).hexdigest() + address_hashed = filter_storage.getSiteAddressHashed(address) else: - address_sha256 = None + address_hashed = None - if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_sha256): + if filter_storage.isSiteblocked(address) or filter_storage.isSiteblocked(address_hashed): site = self.server.site_manager.get(config.homepage) if not extra_headers: extra_headers = {} diff --git a/plugins/ContentFilter/ContentFilterStorage.py b/plugins/ContentFilter/ContentFilterStorage.py index 2215ccca..70409d6b 100644 --- a/plugins/ContentFilter/ContentFilterStorage.py +++ b/plugins/ContentFilter/ContentFilterStorage.py @@ -3,12 +3,14 @@ import json import logging import collections import time +import hashlib from Debug import Debug from Plugin import PluginManager from Config import config from util import helper + class ContentFilterStorage(object): def __init__(self, site_manager): self.log = logging.getLogger("ContentFilterStorage") @@ -114,16 +116,32 @@ class ContentFilterStorage(object): else: return False + def getSiteAddressHashed(self, address): + return "0x" + hashlib.sha256(address.encode("ascii")).hexdigest() + def isSiteblocked(self, address): if address in self.file_content["siteblocks"] or address in self.include_filters["siteblocks"]: return True - else: - return False + return False def getSiteblockDetails(self, address): details = self.file_content["siteblocks"].get(address) if not details: - details = self.include_filters["siteblocks"].get(address) + address_sha256 = self.getSiteAddressHashed(address) + details = self.file_content["siteblocks"].get(address_sha256) + + if not details: + includes = self.file_content.get("includes", {}).values() + for include in includes: + include_site = self.site_manager.get(include["address"]) + if not include_site: + continue + content = include_site.storage.loadJson(include["inner_path"]) + details = content.get("siteblocks").get(address) + if details: + details["include"] = include + break + return details # Search and remove or readd files of an user diff --git a/plugins/ContentFilter/media/blocklisted.html b/plugins/ContentFilter/media/blocklisted.html index 9a287b72..c9d201a9 100644 --- a/plugins/ContentFilter/media/blocklisted.html +++ b/plugins/ContentFilter/media/blocklisted.html @@ -62,25 +62,7 @@ class Page extends ZeroFrame { } async updateSiteblockDetails(address) { - var address_sha256 = await sha256hex(address) - var blocks = await this.cmdp("siteblockList") - if (blocks[address] || blocks[address_sha256]) { - block = blocks[address] - } else { - var includes = await this.cmdp("filterIncludeList", {all_sites: true, filters: true}) - for (let include of includes) { - if (include["siteblocks"][address]) { - var block = include["siteblocks"][address] - block["include"] = include - } - if (include["siteblocks"][address_sha256]) { - var block = include["siteblocks"][address_sha256] - block["include"] = include - } - } - } - - this.blocks = blocks + var block = await this.cmdp("siteblockGet", address) var reason = block["reason"] if (!reason) reason = "Unknown reason" var date = new Date(block["date_added"] * 1000) @@ -95,7 +77,7 @@ class Page extends ZeroFrame { document.getElementById("visit").style.opacity = "1" document.getElementById("visit").onclick = () => { if (block["include"]) - this.cmd("siteAdd", address, () => { this.cmd("wrapperReload") }) + this.cmd("siteblockIgnoreAddSite", address, () => { this.cmd("wrapperReload") }) else this.cmd("siteblockRemove", address, () => { this.cmd("wrapperReload") }) } From 8aa4e27938a08c2ac66d659f56835f81590a194e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 13 Feb 2020 17:26:29 +0100 Subject: [PATCH 451/483] Rev4411 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 7623e575..aaaae028 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4404 + self.rev = 4411 self.argv = argv self.action = None self.test_parser = None From 64e5e0c80ebf38ec4971271a2db0ee986445d5b4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 18 Feb 2020 15:28:14 +0100 Subject: [PATCH 452/483] Rev445, Fix and test random fail in CryptMessage decrypt --- plugins/CryptMessage/CryptMessage.py | 25 +++++++++++++++++++++---- plugins/CryptMessage/Test/TestCrypt.py | 19 +++++++++++++++++-- src/Config.py | 2 +- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index b6c65673..38abbe5d 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -1,13 +1,13 @@ import hashlib import base64 -import binascii +import struct import lib.pybitcointools as btctools -from util import ThreadPool from Crypt import Crypt ecc_cache = {} + def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): from lib import pyelliptic pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey)) @@ -32,7 +32,7 @@ def eciesDecryptMulti(encrypted_datas, privatekey): try: text = eciesDecrypt(encrypted_data, privatekey).decode("utf8") texts.append(text) - except: + except Exception: texts.append(None) return texts @@ -41,9 +41,26 @@ def eciesDecrypt(encrypted_data, privatekey): ecc_key = getEcc(privatekey) return ecc_key.decrypt(base64.b64decode(encrypted_data)) + +def decodePubkey(pubkey): + i = 0 + curve = struct.unpack('!H', pubkey[i:i + 2])[0] + i += 2 + tmplen = struct.unpack('!H', pubkey[i:i + 2])[0] + i += 2 + pubkey_x = pubkey[i:i + tmplen] + i += tmplen + tmplen = struct.unpack('!H', pubkey[i:i + 2])[0] + i += 2 + pubkey_y = pubkey[i:i + tmplen] + i += tmplen + return curve, pubkey_x, pubkey_y, i + + def split(encrypted): iv = encrypted[0:16] - ciphertext = encrypted[16 + 70:-32] + curve, pubkey_x, pubkey_y, i = decodePubkey(encrypted[16:]) + ciphertext = encrypted[16 + i:-32] return iv, ciphertext diff --git a/plugins/CryptMessage/Test/TestCrypt.py b/plugins/CryptMessage/Test/TestCrypt.py index 05cc6e44..96f73761 100644 --- a/plugins/CryptMessage/Test/TestCrypt.py +++ b/plugins/CryptMessage/Test/TestCrypt.py @@ -57,12 +57,12 @@ class TestCrypt: assert decrypted != "hello" # Decrypt using correct privatekey - decrypted = ui_websocket.testAction("EciesDecrypt", encrypted) + decrypted = ui_websocket.testAction("EciesDecrypt", encrypted) assert decrypted == "hello" # Decrypt incorrect text decrypted = ui_websocket.testAction("EciesDecrypt", "baad") - assert decrypted == None + assert decrypted is None # Decrypt batch decrypted = ui_websocket.testAction("EciesDecrypt", [encrypted, "baad", encrypted]) @@ -90,6 +90,21 @@ class TestCrypt: ui_websocket.actionAesDecrypt(0, base64.b64encode(aes_iv), base64.b64encode(aes_encrypted), aes_key) assert ui_websocket.ws.getResult() == "hello" + def testEciesAesLongpubkey(self, ui_websocket): + privatekey = "5HwVS1bTFnveNk9EeGaRenWS1QFzLFb5kuncNbiY3RiHZrVR6ok" + + ecies_encrypted, aes_key = ["lWiXfEikIjw1ac3J/RaY/gLKACALRUfksc9rXYRFyKDSaxhwcSFBYCgAdIyYlY294g/6VgAf/68PYBVMD3xKH1n7Zbo+ge8b4i/XTKmCZRJvy0eutMKWckYCMVcxgIYNa/ZL1BY1kvvH7omgzg1wBraoLfdbNmVtQgdAZ9XS8PwRy6OB2Q==", "Rvlf7zsMuBFHZIGHcbT1rb4If+YTmsWDv6kGwcvSeMM="] + + # Decrypt using Ecies + ui_websocket.actionEciesDecrypt(0, ecies_encrypted, privatekey) + assert ui_websocket.ws.getResult() == "hello" + + # Decrypt using AES + aes_iv, aes_encrypted = CryptMessage.split(base64.b64decode(ecies_encrypted)) + + ui_websocket.actionAesDecrypt(0, base64.b64encode(aes_iv), base64.b64encode(aes_encrypted), aes_key) + assert ui_websocket.ws.getResult() == "hello" + def testAes(self, ui_websocket): ui_websocket.actionAesEncrypt(0, "hello") key, iv, encrypted = ui_websocket.ws.getResult() diff --git a/src/Config.py b/src/Config.py index aaaae028..eddfefb2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4411 + self.rev = 4445 self.argv = argv self.action = None self.test_parser = None From 8facd9ff843d98e6a000876db4073c38ecfeb8e9 Mon Sep 17 00:00:00 2001 From: canewsin Date: Tue, 18 Feb 2020 23:09:16 +0530 Subject: [PATCH 453/483] Added Custom Openssl Path for Native Clients and start_dir config This Parameter helpful where openssl path is not fixed always, we can also use this to reduce code verbosity by providing other like these and provide them as parameter if sys.platform.startswith("win"): self.openssl_bin = "tools\\openssl\\openssl.exe" elif config.dist_type.startswith("bundle_linux"): self.openssl_bin = "../runtime/bin/openssl" else: self.openssl_bin = "openssl" Also Added Custom start_dir config option since android path issue of not valid "./" path, where files via provided path are not loading on some systems like Android client. for more detailed conversation see pull request [#2422](https://github.com/HelloZeroNet/ZeroNet/pull/2422) --- src/Config.py | 9 +++++++-- src/Crypt/CryptConnection.py | 13 ++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Config.py b/src/Config.py index eddfefb2..d482d892 100644 --- a/src/Config.py +++ b/src/Config.py @@ -29,6 +29,7 @@ class Config(object): ]) self.start_dir = self.getStartDir() + self.openssl_path = "default" self.config_file = self.start_dir + "/zeronet.conf" self.data_dir = self.start_dir + "/data" self.log_dir = self.start_dir + "/log" @@ -106,6 +107,8 @@ class Config(object): else: fix_float_decimals = False + openssl_path = "default" + start_dir = self.start_dir config_file = self.start_dir + "/zeronet.conf" data_dir = self.start_dir + "/data" log_dir = self.start_dir + "/log" @@ -220,6 +223,7 @@ class Config(object): self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true') + self.parser.add_argument('--start_dir', help='Start Directory of ZeroNet(Usually dir where zeronet.py Exists)',default=start_dir, metavar="path") self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path") self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path") @@ -259,6 +263,7 @@ class Config(object): self.parser.add_argument('--ip_external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*') self.parser.add_argument('--offline', help='Disable network communication', action='store_true') + self.parser.add_argument('--openssl_path', help='Custom Path to OpenSSL Binary', default=openssl_path, metavar="path") self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true') self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port') self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip') @@ -479,7 +484,7 @@ class Config(object): for key, val in args.items(): if type(val) is list: val = val[:] - if key in ("data_dir", "log_dir"): + if key in ("start_dir", "data_dir", "log_dir", "openssl_path"): val = val.replace("\\", "/") setattr(self, key, val) @@ -560,7 +565,7 @@ class Config(object): "language": self.language, "debug": self.debug, "plugins": PluginManager.plugin_manager.plugin_names, - + "openssl_path": os.path.abspath(self.openssl_path), "log_dir": os.path.abspath(self.log_dir), "data_dir": os.path.abspath(self.data_dir), "src_dir": os.path.dirname(os.path.abspath(__file__)) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 689357fa..3f6279fb 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -11,12 +11,15 @@ from util import helper class CryptConnectionManager: def __init__(self): - if sys.platform.startswith("win"): - self.openssl_bin = "tools\\openssl\\openssl.exe" - elif config.dist_type.startswith("bundle_linux"): - self.openssl_bin = "../runtime/bin/openssl" + if config.openssl_path != "default": + self.openssl_bin = config.openssl_path else: - self.openssl_bin = "openssl" + if sys.platform.startswith("win"): + self.openssl_bin = "tools\\openssl\\openssl.exe" + elif config.dist_type.startswith("bundle_linux"): + self.openssl_bin = "../runtime/bin/openssl" + else: + self.openssl_bin = "openssl" self.context_client = None self.context_server = None From 2c826eba2d3aaaeaba397de9f90ce42e1decef07 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 19 Feb 2020 16:48:14 +0100 Subject: [PATCH 454/483] Rev4447, Fix Msgpack 1.0.0 compatibility --- src/Config.py | 2 +- src/util/Msgpack.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Config.py b/src/Config.py index eddfefb2..caac7f2d 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4445 + self.rev = 4447 self.argv = argv self.action = None self.test_parser = None diff --git a/src/util/Msgpack.py b/src/util/Msgpack.py index f87b95b0..1033f92e 100644 --- a/src/util/Msgpack.py +++ b/src/util/Msgpack.py @@ -78,10 +78,14 @@ def getUnpacker(fallback=False, decode=True): else: unpacker = msgpack.Unpacker + extra_kwargs = {"max_buffer_size": 5 * 1024 * 1024} + if msgpack.version[0] >= 1: + extra_kwargs["strict_map_key"] = False + if decode: # Workaround for backward compatibility: Try to decode bin to str - unpacker = unpacker(raw=True, object_pairs_hook=objectDecoderHook, max_buffer_size=5 * 1024 * 1024) + unpacker = unpacker(raw=True, object_pairs_hook=objectDecoderHook, **extra_kwargs) else: - unpacker = unpacker(raw=False, max_buffer_size=5 * 1024 * 1024) + unpacker = unpacker(raw=False, **extra_kwargs) return unpacker From fca1033f83e9b82e94733e1e7452bf88f61cc377 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:18:59 +0100 Subject: [PATCH 455/483] Fix trayicon auto start script write/read with utf8 path --- plugins/Trayicon/TrayiconPlugin.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index e9b12a26..622f742a 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -140,17 +140,17 @@ class ActionsPlugin(object): cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip() cmd += ' --open_browser ""' - return """ - @echo off - chcp 65001 > nul - set PYTHONIOENCODING=utf-8 - cd /D \"%s\" - start "" %s - """ % (cwd, cmd) + return "\r\n".join([ + '@echo off', + 'chcp 65001 > nul', + 'set PYTHONIOENCODING=utf-8', + 'cd /D \"%s\"' % cwd, + 'start "" %s' % cmd + ]) def isAutorunEnabled(self): path = self.getAutorunPath() - return os.path.isfile(path) and open(path).read() == self.formatAutorun() + return os.path.isfile(path) and open(path, "rb").read().decode("utf8") == self.formatAutorun() def titleAutorun(self): translate = _["Start ZeroNet when Windows starts"] @@ -163,4 +163,4 @@ class ActionsPlugin(object): if self.isAutorunEnabled(): os.unlink(self.getAutorunPath()) else: - open(self.getAutorunPath(), "w").write(self.formatAutorun()) + open(self.getAutorunPath(), "wb").write(self.formatAutorun().encode("utf8")) From b1819ff71d2b66b25db095d10284b8ac2b5f3724 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:19:16 +0100 Subject: [PATCH 456/483] Fix trayicon autostart script duplicated arguments --- plugins/Trayicon/TrayiconPlugin.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/Trayicon/TrayiconPlugin.py b/plugins/Trayicon/TrayiconPlugin.py index 622f742a..2dc3a5c6 100644 --- a/plugins/Trayicon/TrayiconPlugin.py +++ b/plugins/Trayicon/TrayiconPlugin.py @@ -132,12 +132,17 @@ class ActionsPlugin(object): else: cwd = os.path.dirname(sys.executable) + ignored_args = [ + "--open_browser", "default_browser", + "--dist_type", "bundle_win64" + ] + if sys.platform == 'win32': - args = ['"%s"' % arg for arg in args if arg] + args = ['"%s"' % arg for arg in args if arg and arg not in ignored_args] cmd = " ".join(args) # Dont open browser on autorun - cmd = cmd.replace("start.py", "zeronet.py").replace('"--open_browser"', "").replace('"default_browser"', "").strip() + cmd = cmd.replace("start.py", "zeronet.py").strip() cmd += ' --open_browser ""' return "\r\n".join([ From 1cc0ec3f31b3b8db4a534ec64ebe82629b81fa2e Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:23:00 +0100 Subject: [PATCH 457/483] Indepently configurable OpenSSL lib/bin file --- src/Config.py | 13 ++++++++----- src/Crypt/CryptConnection.py | 15 +++++++-------- src/util/OpensslFindPatch.py | 4 ++++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Config.py b/src/Config.py index 6645baea..ba9139cc 100644 --- a/src/Config.py +++ b/src/Config.py @@ -33,6 +33,8 @@ class Config(object): self.config_file = self.start_dir + "/zeronet.conf" self.data_dir = self.start_dir + "/data" self.log_dir = self.start_dir + "/log" + self.openssl_lib_file = None + self.openssl_bin_file = None self.trackers_file = False self.createParser() @@ -107,7 +109,6 @@ class Config(object): else: fix_float_decimals = False - openssl_path = "default" start_dir = self.start_dir config_file = self.start_dir + "/zeronet.conf" data_dir = self.start_dir + "/data" @@ -263,7 +264,6 @@ class Config(object): self.parser.add_argument('--ip_external', help='Set reported external ip (tested on start if None)', metavar='ip', nargs='*') self.parser.add_argument('--offline', help='Disable network communication', action='store_true') - self.parser.add_argument('--openssl_path', help='Custom Path to OpenSSL Binary', default=openssl_path, metavar="path") self.parser.add_argument('--disable_udp', help='Disable UDP connections', action='store_true') self.parser.add_argument('--proxy', help='Socks proxy address', metavar='ip:port') self.parser.add_argument('--bind', help='Bind outgoing sockets to this address', metavar='ip') @@ -272,6 +272,8 @@ class Config(object): self.parser.add_argument('--trackers_proxy', help='Force use proxy to connect to trackers (disable, tor, ip:port)', default="disable") self.parser.add_argument('--use_libsecp256k1', help='Use Libsecp256k1 liblary for speedup', type='bool', choices=[True, False], default=True) self.parser.add_argument('--use_openssl', help='Use OpenSSL liblary for speedup', type='bool', choices=[True, False], default=True) + self.parser.add_argument('--openssl_lib_file', help='Path for OpenSSL library file (default: detect)', default=argparse.SUPPRESS, metavar="path") + self.parser.add_argument('--openssl_bin_file', help='Path for OpenSSL binary file (default: detect)', default=argparse.SUPPRESS, metavar="path") self.parser.add_argument('--disable_db', help='Disable database updating', action='store_true') self.parser.add_argument('--disable_encryption', help='Disable connection encryption', action='store_true') self.parser.add_argument('--force_encryption', help="Enforce encryption to all peer connections", action='store_true') @@ -484,8 +486,9 @@ class Config(object): for key, val in args.items(): if type(val) is list: val = val[:] - if key in ("start_dir", "data_dir", "log_dir", "openssl_path"): - val = val.replace("\\", "/") + if key in ("data_dir", "log_dir", "start_dir", "openssl_bin_file", "openssl_lib_file"): + if val: + val = val.replace("\\", "/") setattr(self, key, val) def loadPlugins(self): @@ -565,7 +568,7 @@ class Config(object): "language": self.language, "debug": self.debug, "plugins": PluginManager.plugin_manager.plugin_names, - "openssl_path": os.path.abspath(self.openssl_path), + "log_dir": os.path.abspath(self.log_dir), "data_dir": os.path.abspath(self.data_dir), "src_dir": os.path.dirname(os.path.abspath(__file__)) diff --git a/src/Crypt/CryptConnection.py b/src/Crypt/CryptConnection.py index 3f6279fb..ebbc6295 100644 --- a/src/Crypt/CryptConnection.py +++ b/src/Crypt/CryptConnection.py @@ -11,15 +11,14 @@ from util import helper class CryptConnectionManager: def __init__(self): - if config.openssl_path != "default": - self.openssl_bin = config.openssl_path + if config.openssl_bin_file: + self.openssl_bin = config.openssl_bin_file + elif sys.platform.startswith("win"): + self.openssl_bin = "tools\\openssl\\openssl.exe" + elif config.dist_type.startswith("bundle_linux"): + self.openssl_bin = "../runtime/bin/openssl" else: - if sys.platform.startswith("win"): - self.openssl_bin = "tools\\openssl\\openssl.exe" - elif config.dist_type.startswith("bundle_linux"): - self.openssl_bin = "../runtime/bin/openssl" - else: - self.openssl_bin = "openssl" + self.openssl_bin = "openssl" self.context_client = None self.context_server = None diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index d6851e32..fd09ec6b 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -4,11 +4,15 @@ import sys import ctypes import ctypes.util +from Config import config find_library_original = ctypes.util.find_library def getOpensslPath(): + if config.openssl_lib_file: + return config.openssl_lib_file + if sys.platform.startswith("win"): lib_paths = [ os.path.join(os.getcwd(), "tools/openssl/libeay32.dll"), # ZeroBundle Windows From a9c75a3146c05dce260165a190b6bc5b7dd3edb4 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:25:06 +0100 Subject: [PATCH 458/483] Fix start dir parsing for command line and better description --- src/Config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index ba9139cc..d0e4cd27 100644 --- a/src/Config.py +++ b/src/Config.py @@ -29,7 +29,6 @@ class Config(object): ]) self.start_dir = self.getStartDir() - self.openssl_path = "default" self.config_file = self.start_dir + "/zeronet.conf" self.data_dir = self.start_dir + "/data" self.log_dir = self.start_dir + "/log" @@ -56,7 +55,9 @@ class Config(object): def getStartDir(self): this_file = os.path.abspath(__file__).replace("\\", "/").rstrip("cd") - if this_file.endswith("/Contents/Resources/core/src/Config.py"): + if "--start_dir" in self.argv: + start_dir = self.argv[self.argv.index("--start_dir") + 1] + elif this_file.endswith("/Contents/Resources/core/src/Config.py"): # Running as ZeroNet.app if this_file.startswith("/Application") or this_file.startswith("/private") or this_file.startswith(os.path.expanduser("~/Library")): # Runnig from non-writeable directory, put data to Application Support @@ -109,7 +110,6 @@ class Config(object): else: fix_float_decimals = False - start_dir = self.start_dir config_file = self.start_dir + "/zeronet.conf" data_dir = self.start_dir + "/data" log_dir = self.start_dir + "/log" @@ -224,7 +224,7 @@ class Config(object): self.parser.add_argument('--batch', help="Batch mode (No interactive input for commands)", action='store_true') - self.parser.add_argument('--start_dir', help='Start Directory of ZeroNet(Usually dir where zeronet.py Exists)',default=start_dir, metavar="path") + self.parser.add_argument('--start_dir', help='Path of working dir for variable content (data, log, .conf)', default=self.start_dir, metavar="path") self.parser.add_argument('--config_file', help='Path of config file', default=config_file, metavar="path") self.parser.add_argument('--data_dir', help='Path of data directory', default=data_dir, metavar="path") From 9b85d8638d30205c72ca8c8c162bcd01022f5405 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:25:56 +0100 Subject: [PATCH 459/483] Don't allow run site api calls when site is deleting --- src/Site/Site.py | 1 + src/Ui/UiWebsocket.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/Site/Site.py b/src/Site/Site.py index 09ff03c9..32f10abe 100644 --- a/src/Site/Site.py +++ b/src/Site/Site.py @@ -1059,6 +1059,7 @@ class Site(object): self.log.info("Deleting site...") s = time.time() self.settings["serving"] = False + self.settings["deleting"] = True self.saveSettings() num_greenlets = self.greenlet_manager.stopGreenlets("Site %s deleted" % self.address) self.worker_manager.running = False diff --git a/src/Ui/UiWebsocket.py b/src/Ui/UiWebsocket.py index e4819644..7fce398b 100644 --- a/src/Ui/UiWebsocket.py +++ b/src/Ui/UiWebsocket.py @@ -216,6 +216,9 @@ class UiWebsocket(object): else: # Normal command func_name = self.getCmdFuncName(cmd) func = getattr(self, func_name, None) + if self.site.settings.get("deleting"): + return self.response(req["id"], {"error": "Site is deleting"}) + if not func: # Unknown command return self.response(req["id"], {"error": "Unknown command: %s" % cmd}) From ae9a76a6c93d9bea6fb9e6561161b21014b60ad6 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:27:31 +0100 Subject: [PATCH 460/483] Fix double sites.json loading on startup when adding missing sites --- src/Site/SiteManager.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index 73696688..e1a7c7c3 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -44,6 +44,8 @@ class SiteManager(object): except Exception as err: raise Exception("Unable to load %s: %s" % (json_path, err)) + sites_need = [] + for address, settings in data.items(): if address not in self.sites: if os.path.isfile("%s/%s/content.json" % (config.data_dir, address)): @@ -61,7 +63,7 @@ class SiteManager(object): elif startup: # No site directory, start download self.log.debug("Found new site in sites.json: %s" % address) - gevent.spawn(self.need, address, settings=settings) + sites_need.append([address, settings]) added += 1 address_found.append(address) @@ -90,9 +92,11 @@ class SiteManager(object): if address in content_db.sites: del content_db.sites[address] + self.loaded = True + for address, settings in sites_need: + gevent.spawn(self.need, address, settings=settings) if added: self.log.info("Added %s sites in %.3fs" % (added, time.time() - load_s)) - self.loaded = True def saveDelayed(self): RateLimit.callAsync("Save sites.json", allowed_again=5, func=self.save) From 8b994e42c23a49225833357240c73d51896da6b2 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Thu, 20 Feb 2020 17:27:50 +0100 Subject: [PATCH 461/483] Rev4452 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index d0e4cd27..c2e6985a 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4447 + self.rev = 4452 self.argv = argv self.action = None self.test_parser = None From f0a706f6ab847ba22798a8107b4b1f15d79de6b7 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Fri, 21 Feb 2020 13:58:11 +0100 Subject: [PATCH 462/483] Rev4455, Fix new sites file downloading --- src/Config.py | 2 +- src/Site/SiteManager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index c2e6985a..facf575e 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4452 + self.rev = 4455 self.argv = argv self.action = None self.test_parser = None diff --git a/src/Site/SiteManager.py b/src/Site/SiteManager.py index e1a7c7c3..44773b0d 100644 --- a/src/Site/SiteManager.py +++ b/src/Site/SiteManager.py @@ -169,7 +169,7 @@ class SiteManager(object): return site - def add(self, address, all_file=False, settings=None, **kwargs): + def add(self, address, all_file=True, settings=None, **kwargs): from .Site import Site self.sites_changed = int(time.time()) # Try to find site with differect case From f8e2cbe4295101605bead86f5479f2e617002fd6 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 24 Feb 2020 15:46:01 +0300 Subject: [PATCH 463/483] Allow uploading files via websocket (#2437) * Allow uploading files via websocket * Fix --- plugins/Bigfile/BigfilePlugin.py | 85 +++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 053098a8..6bffb69e 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -61,13 +61,44 @@ class UiRequestPlugin(object): }) self.readMultipartHeaders(self.env['wsgi.input']) # Skip http headers + result = self.handleBigfileUpload(upload_info, self.env['wsgi.input'].read) + return json.dumps(result) + def actionBigfileUploadWebsocket(self): + ws = self.env.get("wsgi.websocket") + + if not ws: + self.start_response("400 Bad Request", []) + return [b"Not a websocket request!"] + + nonce = self.get.get("upload_nonce") + if nonce not in upload_nonces: + return self.error403("Upload nonce error.") + + upload_info = upload_nonces[nonce] + del upload_nonces[nonce] + + ws.send("poll") + + buffer = b"" + def read(size): + nonlocal buffer + while len(buffer) < size: + buffer += ws.receive() + ws.send("poll") + part, buffer = buffer[:size], buffer[size:] + return part + + result = self.handleBigfileUpload(upload_info, read) + ws.send(json.dumps(result)) + + def handleBigfileUpload(self, upload_info, read): site = upload_info["site"] inner_path = upload_info["inner_path"] with site.storage.open(inner_path, "wb", create_dirs=True) as out_file: merkle_root, piece_size, piecemap_info = site.content_manager.hashBigfile( - self.env['wsgi.input'], upload_info["size"], upload_info["piece_size"], out_file + read, upload_info["size"], upload_info["piece_size"], out_file ) if len(piecemap_info["sha512_pieces"]) == 1: # Small file, don't split @@ -106,12 +137,12 @@ class UiRequestPlugin(object): site.content_manager.contents.loadItem(file_info["content_inner_path"]) # reload cache - return json.dumps({ + return { "merkle_root": merkle_root, "piece_num": len(piecemap_info["sha512_pieces"]), "piece_size": piece_size, "inner_path": inner_path - }) + } def readMultipartHeaders(self, wsgi_input): found = False @@ -169,6 +200,44 @@ class UiWebsocketPlugin(object): "file_relative_path": file_relative_path } + def actionBigfileUploadInitWebsocket(self, to, inner_path, size): + valid_signers = self.site.content_manager.getValidSigners(inner_path) + auth_address = self.user.getAuthAddress(self.site.address) + if not self.site.settings["own"] and auth_address not in valid_signers: + self.log.error("FileWrite forbidden %s not in valid_signers %s" % (auth_address, valid_signers)) + return self.response(to, {"error": "Forbidden, you can only modify your own files"}) + + nonce = CryptHash.random() + piece_size = 1024 * 1024 + inner_path = self.site.content_manager.sanitizePath(inner_path) + file_info = self.site.content_manager.getFileInfo(inner_path, new_file=True) + + content_inner_path_dir = helper.getDirname(file_info["content_inner_path"]) + file_relative_path = inner_path[len(content_inner_path_dir):] + + upload_nonces[nonce] = { + "added": time.time(), + "site": self.site, + "inner_path": inner_path, + "websocket_client": self, + "size": size, + "piece_size": piece_size, + "piecemap": inner_path + ".piecemap.msgpack" + } + + server_url = self.request.getWsServerUrl() + if server_url: + proto, host = server_url.split("://") + origin = proto.replace("http", "ws") + "://" + host + else: + origin = "{origin}" + return { + "url": origin + "/ZeroNet-Internal/BigfileUploadWebsocket?upload_nonce=" + nonce, + "piece_size": piece_size, + "inner_path": inner_path, + "file_relative_path": file_relative_path + } + @flag.no_multiuser def actionSiteSetAutodownloadBigfileLimit(self, to, limit): permissions = self.getPermissions(to) @@ -210,14 +279,14 @@ class ContentManagerPlugin(object): file_info = super(ContentManagerPlugin, self).getFileInfo(inner_path, *args, **kwargs) return file_info - def readFile(self, file_in, size, buff_size=1024 * 64): + def readFile(self, read_func, size, buff_size=1024 * 64): part_num = 0 recv_left = size while 1: part_num += 1 read_size = min(buff_size, recv_left) - part = file_in.read(read_size) + part = read_func(read_size) if not part: break @@ -230,7 +299,7 @@ class ContentManagerPlugin(object): if recv_left <= 0: break - def hashBigfile(self, file_in, size, piece_size=1024 * 1024, file_out=None): + def hashBigfile(self, read_func, size, piece_size=1024 * 1024, file_out=None): self.site.settings["has_bigfile"] = True recv = 0 @@ -243,7 +312,7 @@ class ContentManagerPlugin(object): mt.hash_function = CryptHash.sha512t part = "" - for part in self.readFile(file_in, size): + for part in self.readFile(read_func, size): if file_out: file_out.write(part) @@ -309,7 +378,7 @@ class ContentManagerPlugin(object): return super(ContentManagerPlugin, self).hashFile(dir_inner_path, file_relative_path, optional) self.log.info("- [HASHING] %s" % file_relative_path) - merkle_root, piece_size, piecemap_info = self.hashBigfile(self.site.storage.open(inner_path, "rb"), file_size) + merkle_root, piece_size, piecemap_info = self.hashBigfile(self.site.storage.open(inner_path, "rb").read, file_size) if not hash: hash = merkle_root From 17f65a51796023f5ecacadb79596636521518dba Mon Sep 17 00:00:00 2001 From: Ivanq Date: Mon, 24 Feb 2020 19:19:35 +0300 Subject: [PATCH 464/483] Avoid code duplication --- plugins/Bigfile/BigfilePlugin.py | 65 +++++++++++--------------------- 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/plugins/Bigfile/BigfilePlugin.py b/plugins/Bigfile/BigfilePlugin.py index 6bffb69e..41506f13 100644 --- a/plugins/Bigfile/BigfilePlugin.py +++ b/plugins/Bigfile/BigfilePlugin.py @@ -169,38 +169,7 @@ class UiRequestPlugin(object): @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): - def actionBigfileUploadInit(self, to, inner_path, size): - valid_signers = self.site.content_manager.getValidSigners(inner_path) - auth_address = self.user.getAuthAddress(self.site.address) - if not self.site.settings["own"] and auth_address not in valid_signers: - self.log.error("FileWrite forbidden %s not in valid_signers %s" % (auth_address, valid_signers)) - return self.response(to, {"error": "Forbidden, you can only modify your own files"}) - - nonce = CryptHash.random() - piece_size = 1024 * 1024 - inner_path = self.site.content_manager.sanitizePath(inner_path) - file_info = self.site.content_manager.getFileInfo(inner_path, new_file=True) - - content_inner_path_dir = helper.getDirname(file_info["content_inner_path"]) - file_relative_path = inner_path[len(content_inner_path_dir):] - - upload_nonces[nonce] = { - "added": time.time(), - "site": self.site, - "inner_path": inner_path, - "websocket_client": self, - "size": size, - "piece_size": piece_size, - "piecemap": inner_path + ".piecemap.msgpack" - } - return { - "url": "/ZeroNet-Internal/BigfileUpload?upload_nonce=" + nonce, - "piece_size": piece_size, - "inner_path": inner_path, - "file_relative_path": file_relative_path - } - - def actionBigfileUploadInitWebsocket(self, to, inner_path, size): + def actionBigfileUploadInit(self, to, inner_path, size, protocol="xhr"): valid_signers = self.site.content_manager.getValidSigners(inner_path) auth_address = self.user.getAuthAddress(self.site.address) if not self.site.settings["own"] and auth_address not in valid_signers: @@ -225,18 +194,28 @@ class UiWebsocketPlugin(object): "piecemap": inner_path + ".piecemap.msgpack" } - server_url = self.request.getWsServerUrl() - if server_url: - proto, host = server_url.split("://") - origin = proto.replace("http", "ws") + "://" + host + if protocol == "xhr": + return { + "url": "/ZeroNet-Internal/BigfileUpload?upload_nonce=" + nonce, + "piece_size": piece_size, + "inner_path": inner_path, + "file_relative_path": file_relative_path + } + elif protocol == "websocket": + server_url = self.request.getWsServerUrl() + if server_url: + proto, host = server_url.split("://") + origin = proto.replace("http", "ws") + "://" + host + else: + origin = "{origin}" + return { + "url": origin + "/ZeroNet-Internal/BigfileUploadWebsocket?upload_nonce=" + nonce, + "piece_size": piece_size, + "inner_path": inner_path, + "file_relative_path": file_relative_path + } else: - origin = "{origin}" - return { - "url": origin + "/ZeroNet-Internal/BigfileUploadWebsocket?upload_nonce=" + nonce, - "piece_size": piece_size, - "inner_path": inner_path, - "file_relative_path": file_relative_path - } + return {"error": "Unknown protocol"} @flag.no_multiuser def actionSiteSetAutodownloadBigfileLimit(self, to, limit): From 6a1235bd456e69ed44359af62ba20acd6971a1e8 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 25 Feb 2020 16:45:55 +0100 Subject: [PATCH 465/483] Remove old Gevent RLock support --- src/Tor/TorManager.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 7c3d7277..48db2752 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -15,11 +15,7 @@ from Config import config from Crypt import CryptRsa from Site import SiteManager import socks -try: - from gevent.coros import RLock -except: - from gevent.lock import RLock -from util import helper +from gevent.lock import RLock from Debug import Debug from Plugin import PluginManager From b85477787dddaf1e485ac7511059b578ae155b32 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 25 Feb 2020 16:46:21 +0100 Subject: [PATCH 466/483] Workaround for Tor utf8 cookie file path encoding bug on Windows --- src/Tor/TorManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Tor/TorManager.py b/src/Tor/TorManager.py index 48db2752..7e5c8bb0 100644 --- a/src/Tor/TorManager.py +++ b/src/Tor/TorManager.py @@ -152,6 +152,9 @@ class TorManager(object): res_auth = self.send('AUTHENTICATE "%s"' % config.tor_password, conn) elif cookie_match: cookie_file = cookie_match.group(1).encode("ascii").decode("unicode_escape") + if not os.path.isfile(cookie_file) and self.tor_process: + # Workaround for tor client cookie auth file utf8 encoding bug (https://github.com/torproject/stem/issues/57) + cookie_file = os.path.dirname(self.tor_exe) + "\\data\\control_auth_cookie" auth_hex = binascii.b2a_hex(open(cookie_file, "rb").read()) res_auth = self.send("AUTHENTICATE %s" % auth_hex.decode("utf8"), conn) else: From 58f03e21ef381701a6115cce50a721d5d940378c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 25 Feb 2020 16:47:04 +0100 Subject: [PATCH 467/483] Change unreliable trackers --- src/Config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config.py b/src/Config.py index facf575e..4b6832c2 100644 --- a/src/Config.py +++ b/src/Config.py @@ -82,9 +82,9 @@ class Config(object): "zero://boot3rdez4rzn36x.onion:15441", "zero://zero.booth.moe#f36ca555bee6ba216b14d10f38c16f7769ff064e0e37d887603548cc2e64191d:443", # US/NY "udp://tracker.coppersurfer.tk:6969", # DE - "udp://tracker.zum.bi:6969", # US/NY "udp://104.238.198.186:8000", # US/LA - "http://tracker01.loveapp.com:6789/announce", # Google + "udp://retracker.akado-ural.ru:80", # RU + "http://h4.trakx.nibba.trade:80/announce", # US/VA "http://open.acgnxtracker.com:80/announce", # DE "http://tracker.bt4g.com:2095/announce", # Cloudflare "zero://2602:ffc5::c5b2:5360:26312" # US/ATL From 6218a92895bc4c16829ac3e347fb7bb300a4311d Mon Sep 17 00:00:00 2001 From: shortcutme Date: Tue, 25 Feb 2020 16:47:28 +0100 Subject: [PATCH 468/483] Rev4458 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 4b6832c2..6dfd25b3 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4455 + self.rev = 4458 self.argv = argv self.action = None self.test_parser = None From 2862587c152bb02c8671409eae0a5f307c279228 Mon Sep 17 00:00:00 2001 From: krzotr Date: Thu, 27 Feb 2020 00:48:26 +0100 Subject: [PATCH 469/483] Fixed "LookupError: 'hex' is not a text encoding" on /StatsBootstrapper page (#2442) * Fixed "LookupError: 'hex' is not a text encoding" * Fixed KeyError: 'ip4' --- plugins/disabled-Bootstrapper/BootstrapperPlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py index 474f79c1..59e7af7b 100644 --- a/plugins/disabled-Bootstrapper/BootstrapperPlugin.py +++ b/plugins/disabled-Bootstrapper/BootstrapperPlugin.py @@ -150,7 +150,7 @@ class UiRequestPlugin(object): ).fetchall() yield "
    %s (added: %s, peers: %s)
    " % ( - str(hash_row["hash"]).encode("hex"), hash_row["date_added"], len(peer_rows) + str(hash_row["hash"]).encode().hex(), hash_row["date_added"], len(peer_rows) ) for peer_row in peer_rows: - yield " - {ip4: <30} {onion: <30} added: {date_added}, announced: {date_announced}
    ".format(**dict(peer_row)) + yield " - {type} {address}:{port} added: {date_added}, announced: {date_announced}
    ".format(**dict(peer_row)) From 219b90668f673ee0c9cf54b633397641b2a985f6 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Fri, 28 Feb 2020 03:20:04 +0300 Subject: [PATCH 470/483] Switch from gevent-websocket to gevent-ws (#2439) * Switch from gevent-websocket to gevent-ws * Return error handling, add gevent_ws source to lib --- requirements.txt | 2 +- src/Config.py | 1 - src/Ui/UiRequest.py | 2 +- src/Ui/UiServer.py | 31 ++-- src/lib/gevent_ws/__init__.py | 256 ++++++++++++++++++++++++++++++++++ 5 files changed, 268 insertions(+), 24 deletions(-) create mode 100644 src/lib/gevent_ws/__init__.py diff --git a/requirements.txt b/requirements.txt index 3a542131..c2893480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ rsa PySocks>=1.6.8 pyasn1 websocket_client -gevent-websocket +gevent-ws coincurve python-bitcoinlib maxminddb diff --git a/src/Config.py b/src/Config.py index 6dfd25b3..29e05739 100644 --- a/src/Config.py +++ b/src/Config.py @@ -646,7 +646,6 @@ class Config(object): logging.addLevelName(15, "WARNING") logging.getLogger('').name = "-" # Remove root prefix - logging.getLogger("geventwebsocket.handler").setLevel(logging.WARNING) # Don't log ws debug messages if console_logging: self.initConsoleLogger() diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 27fbfd72..075462e7 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -814,7 +814,7 @@ class UiRequest(object): # Remove websocket from every site (admin sites allowed to join other sites event channels) if ui_websocket in site_check.websockets: site_check.websockets.remove(ui_websocket) - return "Bye." + return [b"Bye."] else: # No site found by wrapper key ws.send(json.dumps({"error": "Wrapper key not found: %s" % wrapper_key})) return self.error403("Wrapper key not found: %s" % wrapper_key) diff --git a/src/Ui/UiServer.py b/src/Ui/UiServer.py index 7f6f35b7..188ff811 100644 --- a/src/Ui/UiServer.py +++ b/src/Ui/UiServer.py @@ -5,8 +5,7 @@ import socket import gevent from gevent.pywsgi import WSGIServer -from gevent.pywsgi import WSGIHandler -from geventwebsocket.handler import WebSocketHandler +from lib.gevent_ws import WebSocketHandler from .UiRequest import UiRequest from Site import SiteManager @@ -27,7 +26,7 @@ class LogDb(logging.StreamHandler): # Skip websocket handler if not necessary -class UiWSGIHandler(WSGIHandler): +class UiWSGIHandler(WebSocketHandler): def __init__(self, *args, **kwargs): self.server = args[2] @@ -46,24 +45,14 @@ class UiWSGIHandler(WSGIHandler): self.write(block) def run_application(self): - if "HTTP_UPGRADE" in self.environ: # Websocket request - try: - ws_handler = WebSocketHandler(*self.args, **self.kwargs) - ws_handler.__dict__ = self.__dict__ # Match class variables - ws_handler.run_application() - except (ConnectionAbortedError, ConnectionResetError) as err: - logging.warning("UiWSGIHandler websocket connection error: %s" % err) - except Exception as err: - logging.error("UiWSGIHandler websocket error: %s" % Debug.formatException(err)) - self.handleError(err) - else: # Standard HTTP request - try: - super(UiWSGIHandler, self).run_application() - except (ConnectionAbortedError, ConnectionResetError) as err: - logging.warning("UiWSGIHandler connection error: %s" % err) - except Exception as err: - logging.error("UiWSGIHandler error: %s" % Debug.formatException(err)) - self.handleError(err) + err_name = "UiWSGIHandler websocket" if "HTTP_UPGRADE" in self.environ else "UiWSGIHandler" + try: + super(UiWSGIHandler, self).run_application() + except (ConnectionAbortedError, ConnectionResetError) as err: + logging.warning("%s connection error: %s" % (err_name, err)) + except Exception as err: + logging.warning("%s error: %s" % (err_name, Debug.formatException(err))) + self.handleError(err) def handle(self): # Save socket to be able to close them properly on exit diff --git a/src/lib/gevent_ws/__init__.py b/src/lib/gevent_ws/__init__.py new file mode 100644 index 00000000..8ad74155 --- /dev/null +++ b/src/lib/gevent_ws/__init__.py @@ -0,0 +1,256 @@ +from gevent.pywsgi import WSGIHandler, _InvalidClientInput +from gevent.queue import Queue +import gevent +import hashlib +import base64 +import struct +import socket +import time +import sys + + +SEND_PACKET_SIZE = 1300 +OPCODE_TEXT = 1 +OPCODE_BINARY = 2 +OPCODE_CLOSE = 8 +OPCODE_PING = 9 +OPCODE_PONG = 10 +STATUS_OK = 1000 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_DATA_ERROR = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_TOO_LONG = 1009 + + +class WebSocket: + def __init__(self, socket): + self.socket = socket + self.closed = False + self.status = None + self._receive_error = None + self._queue = Queue() + self.max_length = 10 * 1024 * 1024 + gevent.spawn(self._listen) + + + def set_max_message_length(self, length): + self.max_length = length + + + def _listen(self): + try: + while True: + fin = False + message = bytearray() + is_first_message = True + start_opcode = None + while not fin: + payload, opcode, fin = self._get_frame(max_length=self.max_length - len(message)) + # Make sure continuation frames have correct information + if not is_first_message and opcode != 0: + self._error(STATUS_PROTOCOL_ERROR) + if is_first_message: + if opcode not in (OPCODE_TEXT, OPCODE_BINARY): + self._error(STATUS_PROTOCOL_ERROR) + # Save opcode + start_opcode = opcode + message += payload + is_first_message = False + message = bytes(message) + if start_opcode == OPCODE_TEXT: # UTF-8 text + try: + message = message.decode() + except UnicodeDecodeError: + self._error(STATUS_DATA_ERROR) + self._queue.put(message) + except Exception as e: + self.closed = True + self._receive_error = e + self._queue.put(None) # To make sure the error is read + + + def receive(self): + if not self._queue.empty(): + return self.receive_nowait() + if isinstance(self._receive_error, EOFError): + return None + if self._receive_error: + raise self._receive_error + self._queue.peek() + return self.receive_nowait() + + + def receive_nowait(self): + ret = self._queue.get_nowait() + if self._receive_error and not isinstance(self._receive_error, EOFError): + raise self._receive_error + return ret + + + def send(self, data): + if self.closed: + raise EOFError() + if isinstance(data, str): + self._send_frame(OPCODE_TEXT, data.encode()) + elif isinstance(data, bytes): + self._send_frame(OPCODE_BINARY, data) + else: + raise TypeError("Expected str or bytes, got " + repr(type(data))) + + + # Reads a frame from the socket. Pings, pongs and close packets are handled + # automatically + def _get_frame(self, max_length): + while True: + payload, opcode, fin = self._read_frame(max_length=max_length) + if opcode == OPCODE_PING: + self._send_frame(OPCODE_PONG, payload) + elif opcode == OPCODE_PONG: + pass + elif opcode == OPCODE_CLOSE: + if len(payload) >= 2: + self.status = struct.unpack("!H", payload[:2])[0] + was_closed = self.closed + self.closed = True + if not was_closed: + # Send a close frame in response + self.close(STATUS_OK) + raise EOFError() + else: + return payload, opcode, fin + + + # Low-level function, use _get_frame instead + def _read_frame(self, max_length): + header = self._recv_exactly(2) + + if not (header[1] & 0x80): + self._error(STATUS_POLICY_VIOLATION) + + opcode = header[0] & 0xf + fin = bool(header[0] & 0x80) + + payload_length = header[1] & 0x7f + if payload_length == 126: + payload_length = struct.unpack("!H", self._recv_exactly(2))[0] + elif payload_length == 127: + payload_length = struct.unpack("!Q", self._recv_exactly(8))[0] + + # Control frames are handled in a special way + if opcode in (OPCODE_PING, OPCODE_PONG): + max_length = 125 + + if payload_length > max_length: + self._error(STATUS_TOO_LONG) + + mask = self._recv_exactly(4) + payload = self._recv_exactly(payload_length) + payload = self._unmask(payload, mask) + + return payload, opcode, fin + + + def _recv_exactly(self, length): + buf = bytearray() + while len(buf) < length: + block = self.socket.recv(min(4096, length - len(buf))) + if block == b"": + raise EOFError() + buf += block + return bytes(buf) + + + def _unmask(self, payload, mask): + def gen(c): + return bytes([x ^ c for x in range(256)]) + + + payload = bytearray(payload) + payload[0::4] = payload[0::4].translate(gen(mask[0])) + payload[1::4] = payload[1::4].translate(gen(mask[1])) + payload[2::4] = payload[2::4].translate(gen(mask[2])) + payload[3::4] = payload[3::4].translate(gen(mask[3])) + return bytes(payload) + + + def _send_frame(self, opcode, data): + for i in range(0, len(data), SEND_PACKET_SIZE): + part = data[i:i + SEND_PACKET_SIZE] + fin = int(i == (len(data) - 1) // SEND_PACKET_SIZE * SEND_PACKET_SIZE) + header = bytes( + [ + (opcode if i == 0 else 0) | (fin << 7), + min(len(part), 126) + ] + ) + if len(part) >= 126: + header += struct.pack("!H", len(part)) + self.socket.sendall(header + part) + + + def _error(self, status): + self.close(status) + raise EOFError() + + + def close(self, status=STATUS_OK): + self.closed = True + self._send_frame(OPCODE_CLOSE, struct.pack("!H", status)) + self.socket.close() + + +class WebSocketHandler(WSGIHandler): + def handle_one_response(self): + self.time_start = time.time() + self.status = None + self.headers_sent = False + + self.result = None + self.response_use_chunked = False + self.response_length = 0 + + + http_connection = [s.strip() for s in self.environ.get("HTTP_CONNECTION", "").split(",")] + if "Upgrade" not in http_connection or self.environ.get("HTTP_UPGRADE", "") != "websocket": + # Not my problem + return super(WebSocketHandler, self).handle_one_response() + + if "HTTP_SEC_WEBSOCKET_KEY" not in self.environ: + self.start_response("400 Bad Request", []) + return + + # Generate Sec-Websocket-Accept header + accept = self.environ["HTTP_SEC_WEBSOCKET_KEY"].encode() + accept += b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + accept = base64.b64encode(hashlib.sha1(accept).digest()).decode() + + # Accept + self.start_response("101 Switching Protocols", [ + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-Websocket-Accept", accept) + ])(b"") + + self.environ["wsgi.websocket"] = WebSocket(self.socket) + + # Can't call super because it sets invalid flags like "status" + try: + try: + self.run_application() + finally: + try: + self.wsgi_input._discard() + except (socket.error, IOError): + pass + except _InvalidClientInput: + self._send_error_response_if_possible(400) + except socket.error as ex: + if ex.args[0] in self.ignored_socket_errors: + self.close_connection = True + else: + self.handle_error(*sys.exc_info()) + except: # pylint:disable=bare-except + self.handle_error(*sys.exc_info()) + finally: + self.time_finish = time.time() + self.log_request() From b790bcac9b1fdb7fc4289164d6e6491449bf7957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Otr=C4=99ba?= Date: Fri, 28 Feb 2020 01:24:44 +0100 Subject: [PATCH 471/483] Polish translation --- plugins/Trayicon/languages/pl.json | 14 +++++++ plugins/UiConfig/languages/pl.json | 62 ++++++++++++++++++++++++++++++ src/Translate/languages/pl.json | 13 ++++--- 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 plugins/Trayicon/languages/pl.json create mode 100644 plugins/UiConfig/languages/pl.json diff --git a/plugins/Trayicon/languages/pl.json b/plugins/Trayicon/languages/pl.json new file mode 100644 index 00000000..84c14796 --- /dev/null +++ b/plugins/Trayicon/languages/pl.json @@ -0,0 +1,14 @@ +{ + "ZeroNet Twitter": "ZeroNet Twitter", + "ZeroNet Reddit": "ZeroNet Reddit", + "ZeroNet Github": "ZeroNet Github", + "Report bug/request feature": "Zgłoś błąd / propozycję", + "!Open ZeroNet": "!Otwórz ZeroNet", + "Quit": "Zamknij", + "(active)": "(aktywny)", + "(passive)": "(pasywny)", + "Connections: %s": "Połączenia: %s", + "Received: %.2f MB | Sent: %.2f MB": "Odebrano: %.2f MB | Wysłano: %.2f MB", + "Show console window": "Pokaż okno konsoli", + "Start ZeroNet when Windows starts": "Uruchom ZeroNet podczas startu Windows" +} diff --git a/plugins/UiConfig/languages/pl.json b/plugins/UiConfig/languages/pl.json new file mode 100644 index 00000000..daeba3d8 --- /dev/null +++ b/plugins/UiConfig/languages/pl.json @@ -0,0 +1,62 @@ +{ + "ZeroNet config": "Konfiguracja ZeroNet", + "Web Interface": "Interfejs webowy", + "Open web browser on ZeroNet startup": "Otwórz przeglądarkę podczas uruchomienia ZeroNet", + + "Network": "Sieć", + "Offline mode": "Tryb offline", + "Disable network communication.": "Wyłącz komunikacje sieciową", + "File server network": "Sieć serwera plików", + "Accept incoming peers using IPv4 or IPv6 address. (default: dual)": "Akceptuj połączenia przychodzące używając IPv4 i IPv6. (domyślnie: oba)", + "Dual (IPv4 & IPv6)": "Oba (IPv4 i IPv6)", + "File server port": "Port serwera plików", + "Other peers will use this port to reach your served sites. (default: 15441)": "Inni użytkownicy będą używać tego portu do połączenia się z Tobą. (domyślnie 15441)", + "File server external ip": "Zewnętrzny adres IP serwera plików", + "Detect automatically": "Wykryj automatycznie", + "Your file server is accessible on these ips. (default: detect automatically)": "Twój serwer plików będzie dostępny na tych adresach IP. (domyślnie: wykryj automatycznie)", + + "Disable: Don't connect to peers on Tor network": "Wyłącz: Nie łącz się do użytkowników sieci Tor", + "Enable: Only use Tor for Tor network peers": "Włącz: Łącz się do użytkowników sieci Tor", + "Always: Use Tor for every connections to hide your IP address (slower)": "Zawsze Tor: Użyj Tor dla wszystkich połączeń w celu ukrycia Twojego adresu IP (wolne działanie)", + + "Disable": "Wyłącz", + "Enable": "Włącz", + "Always": "Zawsze Tor", + + "Use Tor bridges": "Użyj Tor bridges", + "Use obfuscated bridge relays to avoid network level Tor block (even slower)": "Użyj obfuskacji, aby uniknąć blokowania Tor na poziomie sieci (jeszcze wolniejsze działanie)", + "Trackers": "Trackery", + "Discover new peers using these adresses": "Wykryj użytkowników korzystając z tych adresów trackerów", + + "Trackers files": "Pliki trackerów", + "Load additional list of torrent trackers dynamically, from a file": "Dynamicznie wczytaj dodatkową listę trackerów z pliku .json", + "Eg.: data/trackers.json": "Np.: data/trackers.json", + + "Proxy for tracker connections": "Serwer proxy dla trackerów", + "Custom": "Własny", + "Custom socks proxy address for trackers": "Adres serwera proxy do łączenia z trackerami", + "Eg.: 127.0.0.1:1080": "Np.: 127.0.0.1:1080", + "Performance": "Wydajność", + "Level of logging to file": "Poziom logowania do pliku", + "Everything": "Wszystko", + "Only important messages": "Tylko ważne wiadomości", + "Only errors": "Tylko błędy", + "Threads for async file system reads": "Wątki asynchroniczne dla odczytu", + "Threads for async file system writes": "Wątki asynchroniczne dla zapisu", + "Threads for cryptographic functions": "Wątki dla funkcji kryptograficznych", + "Threads for database operations": "Wątki dla operacji na bazie danych", + "Sync read": "Synchroniczny odczyt", + "Sync write": "Synchroniczny zapis", + "Sync execution": "Synchroniczne wykonanie", + "1 thread": "1 wątek", + "2 threads": "2 wątki", + "3 threads": "3 wątki", + "4 threads": "4 wątki", + "5 threads": "5 wątków", + "10 threads": "10 wątków", + + " configuration item value changed": " obiekt konfiguracji zmieniony", + "Save settings": "Zapisz ustawienia", + "Some changed settings requires restart": "Niektóre zmiany ustawień wymagają ponownego uruchomienia ZeroNet", + "Restart ZeroNet client": "Uruchom ponownie ZeroNet" +} diff --git a/src/Translate/languages/pl.json b/src/Translate/languages/pl.json index 75caeceb..679e909d 100644 --- a/src/Translate/languages/pl.json +++ b/src/Translate/languages/pl.json @@ -13,8 +13,8 @@ "Content signing failed": "Podpisanie treści zawiodło", "Content publish queued for {0:.0f} seconds.": "Publikacja treści wstrzymana na {0:.0f} sekund(y).", - "Content published to {0} peers.": "Treść opublikowana do {0} uzytkowników równorzednych.", - "No peers found, but your content is ready to access.": "Nie odnaleziono użytkowników równorzędnych, ale twoja treść jest dostępna.", + "Content published to {0} peers.": "Treść opublikowana do {0} uzytkowników.", + "No peers found, but your content is ready to access.": "Nie odnaleziono użytkowników, ale twoja treść jest dostępna.", "Your network connection is restricted. Please, open {0} port": "Twoje połączenie sieciowe jest ograniczone. Proszę, otwórz port {0}", "on your router to make your site accessible for everyone.": "w swoim routerze, by twoja strona mogłabyć dostępna dla wszystkich.", "Content publish failed.": "Publikacja treści zawiodła.", @@ -39,13 +39,16 @@ " files needs to be downloaded": " pliki muszą zostać ściągnięte", " downloaded": " ściągnięte", " download failed": " ściąganie nie powiodło się", - "Peers found: ": "Odnaleziono użytkowników równorzednych: ", - "No peers found": "Nie odnaleziono użytkowników równorzędnych", + "Peers found: ": "Odnaleziono użytkowników: ", + "No peers found": "Nie odnaleziono użytkowników", "Running out of size limit (": "Limit rozmiaru na wyczerpaniu (", "Set limit to \" + site_info.next_size_limit + \"MB": "Ustaw limit na \" + site_info.next_size_limit + \"MBów", "Site size limit changed to {0}MB": "Rozmiar limitu strony zmieniony na {0}MBów", " New version of this page has just released.
    Reload to see the modified content.": "Nowa wersja tej strony właśnie została wydana.
    Odśwież by zobaczyć nową, zmodyfikowaną treść strony.", "This site requests permission:": "Ta strona wymaga uprawnień:", - "_(Accept)": "Przyznaj uprawnienia" + "_(Accept)": "Przyznaj uprawnienia", + "Sign and publish": "Podpisz i opublikuj", + "Restart ZeroNet client?": "Uruchomić ponownie klienta ZeroNet?", + "Restart": "Uruchom ponownie" } From 5baacf963d71889e07a2e0aba70094b7cbc35510 Mon Sep 17 00:00:00 2001 From: krzotr Date: Sat, 29 Feb 2020 00:51:41 +0100 Subject: [PATCH 472/483] Fixed `Cache-Control` for .js and .css files --- src/Ui/UiRequest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Ui/UiRequest.py b/src/Ui/UiRequest.py index 075462e7..51730954 100644 --- a/src/Ui/UiRequest.py +++ b/src/Ui/UiRequest.py @@ -299,9 +299,6 @@ class UiRequest(object): headers["Access-Control-Allow-Headers"] = "Origin, X-Requested-With, Content-Type, Accept, Cookie, Range" headers["Access-Control-Allow-Credentials"] = "true" - if content_type in ("text/plain", "text/html", "text/css", "application/javascript", "application/json", "application/manifest+json"): - content_type += "; charset=utf-8" - # Download instead of display file types that can be dangerous if re.findall("/svg|/xml|/x-shockwave-flash|/pdf", content_type): headers["Content-Disposition"] = "attachment" @@ -312,6 +309,9 @@ class UiRequest(object): content_type in ("application/javascript", "text/css") ) + if content_type in ("text/plain", "text/html", "text/css", "application/javascript", "application/json", "application/manifest+json"): + content_type += "; charset=utf-8" + if status in (200, 206) and cacheable_type: # Cache Css, Js, Image files for 10min headers["Cache-Control"] = "public, max-age=600" # Cache 10 min else: From 1fc67a3d716871b2963fe6a9589854a6c2d5209c Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Mar 2020 16:44:34 +0100 Subject: [PATCH 473/483] Rev4460, Fix mergersite update on slow storage --- plugins/MergerSite/MergerSitePlugin.py | 24 +++++++++++++++--------- src/Config.py | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index ca2ba31e..6ea94be4 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -345,9 +345,9 @@ class SiteManagerPlugin(object): def updateMergerSites(self): global merger_db, merged_db, merged_to_merger, site_manager s = time.time() - merger_db = {} - merged_db = {} - merged_to_merger = {} + merger_db_new = {} + merged_db_new = {} + merged_to_merger_new = {} site_manager = self if not self.sites: return @@ -359,7 +359,7 @@ class SiteManagerPlugin(object): self.log.error("Error loading site %s: %s" % (site.address, Debug.formatException(err))) continue if merged_type: - merged_db[site.address] = merged_type + merged_db_new[site.address] = merged_type # Update merger sites for permission in site.settings["permissions"]: @@ -373,9 +373,9 @@ class SiteManagerPlugin(object): site.settings["permissions"].remove(permission) continue merger_type = permission.replace("Merger:", "") - if site.address not in merger_db: - merger_db[site.address] = [] - merger_db[site.address].append(merger_type) + if site.address not in merger_db_new: + merger_db_new[site.address] = [] + merger_db_new[site.address].append(merger_type) site_manager.sites[site.address] = site # Update merged to merger @@ -383,8 +383,14 @@ class SiteManagerPlugin(object): for merger_site in self.sites.values(): if "Merger:" + merged_type in merger_site.settings["permissions"]: if site.address not in merged_to_merger: - merged_to_merger[site.address] = [] - merged_to_merger[site.address].append(merger_site) + merged_to_merger_new[site.address] = [] + merged_to_merger_new[site.address].append(merger_site) + + # Update globals + merger_db = merger_db_new + merged_db = merged_db_new + merged_to_merger = merged_to_merger_new + self.log.debug("Updated merger sites in %.3fs" % (time.time() - s)) def load(self, *args, **kwags): diff --git a/src/Config.py b/src/Config.py index 29e05739..8b555658 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4458 + self.rev = 4460 self.argv = argv self.action = None self.test_parser = None From e0bf4dc9ecc52252102e330d0e86f63dde2e3c91 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Mar 2020 17:08:43 +0100 Subject: [PATCH 474/483] Skip announcing to trackers with unsupported address --- src/Site/SiteAnnouncer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Site/SiteAnnouncer.py b/src/Site/SiteAnnouncer.py index cfa16ab2..2fd63e82 100644 --- a/src/Site/SiteAnnouncer.py +++ b/src/Site/SiteAnnouncer.py @@ -39,6 +39,8 @@ class SiteAnnouncer(object): if not self.site.connection_server.tor_manager.enabled: trackers = [tracker for tracker in trackers if ".onion" not in tracker] + trackers = [tracker for tracker in trackers if self.getAddressParts(tracker)] # Remove trackers with unknown address + if "ipv6" not in self.site.connection_server.supported_ip_types: trackers = [tracker for tracker in trackers if helper.getIpType(self.getAddressParts(tracker)["ip"]) != "ipv6"] From 27761c5045116c546cf1157c7bc1ff2902e812c1 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Mar 2020 17:09:13 +0100 Subject: [PATCH 475/483] Fix merger site updating --- plugins/MergerSite/MergerSitePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/MergerSite/MergerSitePlugin.py b/plugins/MergerSite/MergerSitePlugin.py index 6ea94be4..d2c24398 100644 --- a/plugins/MergerSite/MergerSitePlugin.py +++ b/plugins/MergerSite/MergerSitePlugin.py @@ -382,7 +382,7 @@ class SiteManagerPlugin(object): if merged_type: for merger_site in self.sites.values(): if "Merger:" + merged_type in merger_site.settings["permissions"]: - if site.address not in merged_to_merger: + if site.address not in merged_to_merger_new: merged_to_merger_new[site.address] = [] merged_to_merger_new[site.address].append(merger_site) From f46b945cdce30ac2e8a00296c3536bcc45e74a55 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Mon, 2 Mar 2020 17:09:21 +0100 Subject: [PATCH 476/483] Rev4461 --- src/Config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config.py b/src/Config.py index 8b555658..d14f2f43 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4460 + self.rev = 4461 self.argv = argv self.action = None self.test_parser = None From aaabcb6b1a45e917d4b95fe1a242e3050144aef8 Mon Sep 17 00:00:00 2001 From: zyw271828 Date: Tue, 3 Mar 2020 12:48:18 +0800 Subject: [PATCH 477/483] Update "How to join" section of README-zh-cn.md --- README-zh-cn.md | 81 +++++++++++++++++-------------------------------- 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/README-zh-cn.md b/README-zh-cn.md index 103194ea..f5e0ebf6 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -56,65 +56,40 @@ #### [在 ZeroNet 文档里查看更多的屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/) -## 如何加入 ? +## 如何加入 -* 下载 ZeroBundle 文件包: - * [Microsoft Windows](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist/ZeroNet-win.zip) - * [Apple macOS](https://github.com/HelloZeroNet/ZeroNet-mac/archive/dist/ZeroNet-mac.zip) - * [Linux 64bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz) - * [Linux 32bit](https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux32.tar.gz) -* 解压缩 -* 运行 `ZeroNet.exe` (win), `ZeroNet(.app)` (osx), `ZeroNet.sh` (linux) +### Windows -### Linux 命令行 + - 下载 [ZeroNet-py3-win64.zip](https://github.com/HelloZeroNet/ZeroNet-win/archive/dist-win64/ZeroNet-py3-win64.zip) (18MB) + - 在任意位置解压缩 + - 运行 `ZeroNet.exe` + +### macOS -* `wget https://github.com/HelloZeroNet/ZeroBundle/raw/master/dist/ZeroBundle-linux64.tar.gz` -* `tar xvpfz ZeroBundle-linux64.tar.gz` -* `cd ZeroBundle` -* 执行 `./ZeroNet.sh` 来启动 + - 下载 [ZeroNet-dist-mac.zip](https://github.com/HelloZeroNet/ZeroNet-dist/archive/mac/ZeroNet-dist-mac.zip) (13.2MB) + - 在任意位置解压缩 + - 运行 `ZeroNet.app` + +### Linux (x86-64bit) -在你打开时他将会自动下载最新版本的 ZeroNet 。 + - `wget https://github.com/HelloZeroNet/ZeroNet-linux/archive/dist-linux64/ZeroNet-py3-linux64.tar.gz` + - `tar xvpfz ZeroNet-py3-linux64.tar.gz` + - `cd ZeroNet-linux-dist-linux64/` + - 使用以下命令启动 `./ZeroNet.sh` + - 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面 + + __提示:__ 若要允许在 Web 界面上的远程连接,使用以下命令启动 `./ZeroNet.sh --ui_ip '*' --ui_restrict your.ip.address` -#### 在 Debian Linux 中手动安装 +### 从源代码安装 -* `sudo apt-get update` -* `sudo apt-get install msgpack-python python-gevent` -* `wget https://github.com/HelloZeroNet/ZeroNet/archive/master.tar.gz` -* `tar xvpfz master.tar.gz` -* `cd ZeroNet-master` -* 执行 `python2 zeronet.py` 来启动 -* 在你的浏览器中打开 http://127.0.0.1:43110/ - -### [FreeBSD](https://www.freebsd.org/) - -* `pkg install zeronet` 或者 `cd /usr/ports/security/zeronet/ && make install clean` -* `sysrc zeronet_enable="YES"` -* `service zeronet start` -* 在你的浏览器中打开 http://127.0.0.1:43110/ - -### [Vagrant](https://www.vagrantup.com/) - -* `vagrant up` -* 通过 `vagrant ssh` 连接到 VM -* `cd /vagrant` -* 运行 `python2 zeronet.py --ui_ip 0.0.0.0` -* 在你的浏览器中打开 http://127.0.0.1:43110/ - -### [Docker](https://www.docker.com/) -* `docker run -d -v :/root/data -p 26552:26552 -p 43110:43110 nofish/zeronet` -* 这个 Docker 镜像包含了 Tor ,但默认是禁用的,因为一些托管商不允许你在他们的服务器上运行 Tor。如果你希望启用它, -设置 `ENABLE_TOR` 环境变量为 `true` (默认: `false`). E.g.: - - `docker run -d -e "ENABLE_TOR=true" -v :/root/data -p 26552:26552 -p 43110:43110 nofish/zeronet` -* 在你的浏览器中打开 http://127.0.0.1:43110/ - -### [Virtualenv](https://virtualenv.readthedocs.org/en/latest/) - -* `virtualenv env` -* `source env/bin/activate` -* `pip install msgpack gevent` -* `python2 zeronet.py` -* 在你的浏览器中打开 http://127.0.0.1:43110/ + - `wget https://github.com/HelloZeroNet/ZeroNet/archive/py3/ZeroNet-py3.tar.gz` + - `tar xvpfz ZeroNet-py3.tar.gz` + - `cd ZeroNet-py3` + - `sudo apt-get update` + - `sudo apt-get install python3-pip` + - `sudo python3 -m pip install -r requirements.txt` + - 使用以下命令启动 `python3 zeronet.py` + - 在浏览器打开 http://127.0.0.1:43110/ 即可访问 ZeroHello 页面 ## 现有限制 From e2a582d8929815ffee94233c8102a392e9cf67bf Mon Sep 17 00:00:00 2001 From: zyw271828 Date: Tue, 3 Mar 2020 12:54:41 +0800 Subject: [PATCH 478/483] Update "How can I create a ZeroNet site" section of README-zh-cn.md --- README-zh-cn.md | 50 ++++++------------------------------------------- 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/README-zh-cn.md b/README-zh-cn.md index f5e0ebf6..939eca86 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -99,52 +99,14 @@ * 不支持私有站点 -## 如何创建一个 ZeroNet 站点? +## 如何创建一个 ZeroNet 站点? + * 点击 [ZeroHello](http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D) 站点的 **⋮** > **「新建空站点」** 菜单项 + * 您将被**重定向**到一个全新的站点,该站点只能由您修改 + * 您可以在 **data/[您的站点地址]** 目录中找到并修改网站的内容 + * 修改后打开您的网站,将右上角的「0」按钮拖到左侧,然后点击底部的**签名**并**发布**按钮 -如果 zeronet 在运行,把它关掉 -执行: -```bash -$ zeronet.py siteCreate -... -- Site private key: 23DKQpzxhbVBrAtvLEc2uvk7DZweh4qL3fn3jpM3LgHDczMK2TtYUq -- Site address: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -... -- Site created! -$ zeronet.py -... -``` - -你已经完成了! 现在任何人都可以通过 -`http://localhost:43110/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2` -来访问你的站点 - -下一步: [ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/) - - -## 我要如何修改 ZeroNet 站点? - -* 修改位于 data/13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 的目录. - 在你改好之后: - -```bash -$ zeronet.py siteSign 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -- Signing site: 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2... -Private key (input hidden): -``` - -* 输入你在创建站点时获得的私钥 - -```bash -$ zeronet.py sitePublish 13DNDkMUExRf9Xa9ogwPKqp7zyHFEqbhC2 -... -Site:13DNDk..bhC2 Publishing to 3/10 peers... -Site:13DNDk..bhC2 Successfuly published to 3 peers -- Serving files.... -``` - -* 就是这样! 你现在已经成功的签名并推送了你的更改。 - +接下来的步骤:[ZeroNet 开发者文档](https://zeronet.io/docs/site_development/getting_started/) ## 帮助这个项目 From 6df3036f11fd188aaaa1c25a8c9c8cdc9156409c Mon Sep 17 00:00:00 2001 From: zyw271828 Date: Tue, 3 Mar 2020 13:12:54 +0800 Subject: [PATCH 479/483] Improve README-zh-cn.md according to latest README.md --- README-zh-cn.md | 66 ++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/README-zh-cn.md b/README-zh-cn.md index 939eca86..fabdb0e5 100644 --- a/README-zh-cn.md +++ b/README-zh-cn.md @@ -1,51 +1,49 @@ -# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=master)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) +# ZeroNet [![Build Status](https://travis-ci.org/HelloZeroNet/ZeroNet.svg?branch=py3)](https://travis-ci.org/HelloZeroNet/ZeroNet) [![Documentation](https://img.shields.io/badge/docs-faq-brightgreen.svg)](https://zeronet.io/docs/faq/) [![Help](https://img.shields.io/badge/keep_this_project_alive-donate-yellow.svg)](https://zeronet.io/docs/help_zeronet/donate/) [English](./README.md) 使用 Bitcoin 加密和 BitTorrent 网络的去中心化网络 - https://zeronet.io -## 为什么? +## 为什么? -* 我们相信开放,自由,无审查的网络 +* 我们相信开放,自由,无审查的网络和通讯 * 不会受单点故障影响:只要有在线的节点,站点就会保持在线 -* 无托管费用: 站点由访问者托管 -* 无法关闭: 因为节点无处不在 -* 快速并可离线运行: 即使没有互联网连接也可以使用 +* 无托管费用:站点由访问者托管 +* 无法关闭:因为节点无处不在 +* 快速并可离线运行:即使没有互联网连接也可以使用 ## 功能 * 实时站点更新 * 支持 Namecoin 的 .bit 域名 - * 安装方便: 只需解压并运行 + * 安装方便:只需解压并运行 * 一键克隆存在的站点 - * 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) 的认证:用与比特币钱包相同的加密方法用来保护你的账户 -你的账户被使用和比特币钱包相同的加密方法 - * 内建 SQL 服务器和 P2P 数据同步: 让开发更简单并提升加载速度 - * 匿名性: 完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过IPv4地址连接 + * 无需密码、基于 [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) + 的认证:您的账户被与比特币钱包相同的加密方法保护 + * 内建 SQL 服务器和 P2P 数据同步:让开发更简单并提升加载速度 + * 匿名性:完整的 Tor 网络支持,支持通过 .onion 隐藏服务相互连接而不是通过 IPv4 地址连接 * TLS 加密连接 * 自动打开 uPnP 端口 - * 插件和多用户 (开放式代理) 支持 - * 全平台兼容 + * 多用户(openproxy)支持的插件 + * 适用于任何浏览器 / 操作系统 ## 原理 -* 在你运行`zeronet.py`后你将可以通过`http://127.0.0.1:43110/{zeronet_address}` (比如. -`http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)。访问 zeronet 中的站点。 +* 在运行 `zeronet.py` 后,您将可以通过 + `http://127.0.0.1:43110/{zeronet_address}`(例如: + `http://127.0.0.1:43110/1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D`)访问 zeronet 中的站点 +* 在您浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件(html,css,js...) +* 您将会储存每一个浏览过的站点 +* 每个站点都包含一个名为 `content.json` 的文件,它储存了其他所有文件的 sha512 散列值以及一个通过站点私钥生成的签名 +* 如果站点的所有者(拥有站点地址的私钥)修改了站点,并且他 / 她签名了新的 `content.json` 然后推送至其他节点, + 那么这些节点将会在使用签名验证 `content.json` 的真实性后,下载修改后的文件并将新内容推送至另外的节点 -* 在你浏览 zeronet 站点时,客户端会尝试通过 BitTorrent 网络来寻找可用的节点,从而下载需要的文件 (html, css, js...) - -* 你将会储存每一个浏览过的站点 -* 每个站点都包含一个名为 `content.json` ,它储存了其他所有文件的 sha512 hash 值 - 和一个通过站点私钥建立的签名 -* 如果站点的所有者 (拥有私钥的那个人) 修改了站点, 并且他/她签名了新的 `content.json` 然后推送至其他节点, -那么所有节点将会在验证 `content.json` 的真实性 (使用签名)后, 下载修改后的文件并推送至其他节点。 - -#### [有关于 ZeroNet 加密, 站点更新, 多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1qBxkroB_iiX2zHEn0dt-N-qRZgyEzui46XS2hEa3AA4/pub?start=false&loop=false&delayms=3000) +#### [关于 ZeroNet 加密,站点更新,多用户站点的幻灯片 »](https://docs.google.com/presentation/d/1_2qK1IuOKJ51pgBvllZ9Yu7Au2l551t3XBgyTSvilew/pub?start=false&loop=false&delayms=3000) #### [常见问题 »](https://zeronet.io/docs/faq/) -#### [ZeroNet开发者文档 »](https://zeronet.io/docs/site_development/getting_started/) +#### [ZeroNet 开发者文档 »](https://zeronet.io/docs/site_development/getting_started/) ## 屏幕截图 @@ -53,7 +51,7 @@ ![Screenshot](https://i.imgur.com/H60OAHY.png) ![ZeroTalk](https://zeronet.io/docs/img/zerotalk.png) -#### [在 ZeroNet 文档里查看更多的屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/) +#### [ZeroNet 文档中的更多屏幕截图 »](https://zeronet.io/docs/using_zeronet/sample_sites/) ## 如何加入 @@ -93,9 +91,9 @@ ## 现有限制 -* ~~没有类似于 BitTorrent 的文件拆分来支持大文件~~ (已添加大文件支持) -* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持) -* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持) +* ~~没有类似于 torrent 的文件拆分来支持大文件~~ (已添加大文件支持) +* ~~没有比 BitTorrent 更好的匿名性~~ (已添加内置的完整 Tor 支持) +* 传输文件时没有压缩~~和加密~~ (已添加 TLS 支持) * 不支持私有站点 @@ -115,11 +113,11 @@ ### 赞助商 -* 在 OSX/Safari 下 [BrowserStack.com](https://www.browserstack.com) 带来更好的兼容性 +* [BrowserStack.com](https://www.browserstack.com) 使更好的 macOS/Safari 兼容性成为可能 -#### 感谢! +#### 感谢您! -* 更多信息, 帮助, 变更记录和 zeronet 站点: https://www.reddit.com/r/zeronet/ -* 在: [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 和我们聊天,或者使用 [gitter](https://gitter.im/HelloZeroNet/ZeroNet) +* 更多信息,帮助,变更记录和 zeronet 站点:https://www.reddit.com/r/zeronet/ +* 前往 [#zeronet @ FreeNode](https://kiwiirc.com/client/irc.freenode.net/zeronet) 或 [gitter](https://gitter.im/HelloZeroNet/ZeroNet) 和我们聊天 * [这里](https://gitter.im/ZeroNet-zh/Lobby)是一个 gitter 上的中文聊天室 -* Email: hello@noloop.me +* Email: hello@zeronet.io (PGP: [960F FF2D 6C14 5AA6 13E8 491B 5B63 BAE6 CB96 13AE](https://zeronet.io/files/tamas@zeronet.io_pub.asc)) From c4f65a5d7b48ad43646c9a57b3c1161042b3dd52 Mon Sep 17 00:00:00 2001 From: shortcutme Date: Wed, 4 Mar 2020 21:50:28 +0100 Subject: [PATCH 480/483] Rev4462, Experimental fix for segfault on shutdown --- src/Config.py | 2 +- src/util/ThreadPool.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Config.py b/src/Config.py index d14f2f43..101f1277 100644 --- a/src/Config.py +++ b/src/Config.py @@ -13,7 +13,7 @@ class Config(object): def __init__(self, argv): self.version = "0.7.1" - self.rev = 4461 + self.rev = 4462 self.argv = argv self.action = None self.test_parser = None diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 2c78a2ad..5bb3c0d6 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -55,9 +55,6 @@ class ThreadPool: del self.pool self.pool = None - def __del__(self): - self.kill() - lock_pool = gevent.threadpool.ThreadPool(50) main_thread_id = threading.current_thread().ident From 09e65e1d95ebb61f43261dc01697747a48e9eee5 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Wed, 4 Mar 2020 22:11:59 +0300 Subject: [PATCH 481/483] Make ThreadPool a context manager to prevent memory leaks --- src/Test/TestNoparallel.py | 32 ++++---- src/Test/TestThreadPool.py | 160 ++++++++++++++++++------------------- src/util/ThreadPool.py | 6 ++ 3 files changed, 99 insertions(+), 99 deletions(-) diff --git a/src/Test/TestNoparallel.py b/src/Test/TestNoparallel.py index d80cc5fb..6fc4f57d 100644 --- a/src/Test/TestNoparallel.py +++ b/src/Test/TestNoparallel.py @@ -149,21 +149,19 @@ class TestNoparallel: def testMultithreadMix(self, queue_spawn): obj1 = ExampleClass() - thread_pool = ThreadPool.ThreadPool(10) + with ThreadPool.ThreadPool(10) as thread_pool: + s = time.time() + t1 = queue_spawn(obj1.countBlocking, 5) + time.sleep(0.01) + t2 = thread_pool.spawn(obj1.countBlocking, 5) + time.sleep(0.01) + t3 = thread_pool.spawn(obj1.countBlocking, 5) + time.sleep(0.3) + t4 = gevent.spawn(obj1.countBlocking, 5) + threads = [t1, t2, t3, t4] + for thread in threads: + assert thread.get() == "counted:5" - s = time.time() - t1 = queue_spawn(obj1.countBlocking, 5) - time.sleep(0.01) - t2 = thread_pool.spawn(obj1.countBlocking, 5) - time.sleep(0.01) - t3 = thread_pool.spawn(obj1.countBlocking, 5) - time.sleep(0.3) - t4 = gevent.spawn(obj1.countBlocking, 5) - threads = [t1, t2, t3, t4] - for thread in threads: - assert thread.get() == "counted:5" - - time_taken = time.time() - s - assert obj1.counted == 5 - assert 0.5 < time_taken < 0.7 - thread_pool.kill() + time_taken = time.time() - s + assert obj1.counted == 5 + assert 0.5 < time_taken < 0.7 diff --git a/src/Test/TestThreadPool.py b/src/Test/TestThreadPool.py index 6c7f35e7..5e95005e 100644 --- a/src/Test/TestThreadPool.py +++ b/src/Test/TestThreadPool.py @@ -9,31 +9,29 @@ from util import ThreadPool class TestThreadPool: def testExecutionOrder(self): - pool = ThreadPool.ThreadPool(4) + with ThreadPool.ThreadPool(4) as pool: + events = [] - events = [] + @pool.wrap + def blocker(): + events.append("S") + out = 0 + for i in range(10000000): + if i == 3000000: + events.append("M") + out += 1 + events.append("D") + return out - @pool.wrap - def blocker(): - events.append("S") - out = 0 - for i in range(10000000): - if i == 3000000: - events.append("M") - out += 1 - events.append("D") - return out + threads = [] + for i in range(3): + threads.append(gevent.spawn(blocker)) + gevent.joinall(threads) - threads = [] - for i in range(3): - threads.append(gevent.spawn(blocker)) - gevent.joinall(threads) + assert events == ["S"] * 3 + ["M"] * 3 + ["D"] * 3 - assert events == ["S"] * 3 + ["M"] * 3 + ["D"] * 3 - - res = blocker() - assert res == 10000000 - pool.kill() + res = blocker() + assert res == 10000000 def testLockBlockingSameThread(self): lock = ThreadPool.Lock() @@ -60,89 +58,88 @@ class TestThreadPool: time.sleep(0.5) lock.release() - pool = ThreadPool.ThreadPool(10) - threads = [ - pool.spawn(locker), - pool.spawn(locker), - gevent.spawn(locker), - pool.spawn(locker) - ] - time.sleep(0.1) + with ThreadPool.ThreadPool(10) as pool: + threads = [ + pool.spawn(locker), + pool.spawn(locker), + gevent.spawn(locker), + pool.spawn(locker) + ] + time.sleep(0.1) - s = time.time() + s = time.time() - lock.acquire(True, 5.0) + lock.acquire(True, 5.0) - unlock_taken = time.time() - s + unlock_taken = time.time() - s - assert 1.8 < unlock_taken < 2.2 + assert 1.8 < unlock_taken < 2.2 - gevent.joinall(threads) + gevent.joinall(threads) def testMainLoopCallerThreadId(self): main_thread_id = threading.current_thread().ident - pool = ThreadPool.ThreadPool(5) + with ThreadPool.ThreadPool(5) as pool: + def getThreadId(*args, **kwargs): + return threading.current_thread().ident - def getThreadId(*args, **kwargs): - return threading.current_thread().ident + t = pool.spawn(getThreadId) + assert t.get() != main_thread_id - t = pool.spawn(getThreadId) - assert t.get() != main_thread_id - - t = pool.spawn(lambda: ThreadPool.main_loop.call(getThreadId)) - assert t.get() == main_thread_id + t = pool.spawn(lambda: ThreadPool.main_loop.call(getThreadId)) + assert t.get() == main_thread_id def testMainLoopCallerGeventSpawn(self): main_thread_id = threading.current_thread().ident - pool = ThreadPool.ThreadPool(5) - def waiter(): - time.sleep(1) - return threading.current_thread().ident + with ThreadPool.ThreadPool(5) as pool: + def waiter(): + time.sleep(1) + return threading.current_thread().ident - def geventSpawner(): - event = ThreadPool.main_loop.call(gevent.spawn, waiter) + def geventSpawner(): + event = ThreadPool.main_loop.call(gevent.spawn, waiter) - with pytest.raises(Exception) as greenlet_err: - event.get() - assert str(greenlet_err.value) == "cannot switch to a different thread" + with pytest.raises(Exception) as greenlet_err: + event.get() + assert str(greenlet_err.value) == "cannot switch to a different thread" - waiter_thread_id = ThreadPool.main_loop.call(event.get) - return waiter_thread_id + waiter_thread_id = ThreadPool.main_loop.call(event.get) + return waiter_thread_id - s = time.time() - waiter_thread_id = pool.apply(geventSpawner) - assert main_thread_id == waiter_thread_id - time_taken = time.time() - s - assert 0.9 < time_taken < 1.2 + s = time.time() + waiter_thread_id = pool.apply(geventSpawner) + assert main_thread_id == waiter_thread_id + time_taken = time.time() - s + assert 0.9 < time_taken < 1.2 def testEvent(self): - pool = ThreadPool.ThreadPool(5) - event = ThreadPool.Event() + with ThreadPool.ThreadPool(5) as pool: + event = ThreadPool.Event() - def setter(): - time.sleep(1) - event.set("done!") + def setter(): + time.sleep(1) + event.set("done!") - def getter(): - return event.get() + def getter(): + return event.get() - pool.spawn(setter) - t_gevent = gevent.spawn(getter) - t_pool = pool.spawn(getter) - s = time.time() - assert event.get() == "done!" - time_taken = time.time() - s - gevent.joinall([t_gevent, t_pool]) + pool.spawn(setter) + t_gevent = gevent.spawn(getter) + t_pool = pool.spawn(getter) + s = time.time() + assert event.get() == "done!" + time_taken = time.time() - s + gevent.joinall([t_gevent, t_pool]) - assert t_gevent.get() == "done!" - assert t_pool.get() == "done!" + assert t_gevent.get() == "done!" + assert t_pool.get() == "done!" - assert 0.9 < time_taken < 1.2 + assert 0.9 < time_taken < 1.2 - with pytest.raises(Exception) as err: - event.set("another result") + with pytest.raises(Exception) as err: + event.set("another result") - assert "Event already has value" in str(err.value) + assert "Event already has value" in str(err.value) def testMemoryLeak(self): import gc @@ -153,10 +150,9 @@ class TestThreadPool: return "ok" def poolTest(): - pool = ThreadPool.ThreadPool(5) - for i in range(20): - pool.spawn(worker) - pool.kill() + with ThreadPool.ThreadPool(5) as pool: + for i in range(20): + pool.spawn(worker) for i in range(5): poolTest() diff --git a/src/util/ThreadPool.py b/src/util/ThreadPool.py index 5bb3c0d6..8c759039 100644 --- a/src/util/ThreadPool.py +++ b/src/util/ThreadPool.py @@ -55,6 +55,12 @@ class ThreadPool: del self.pool self.pool = None + def __enter__(self): + return self + + def __exit__(self, *args): + self.kill() + lock_pool = gevent.threadpool.ThreadPool(50) main_thread_id = threading.current_thread().ident From 296e4aab57e0226f9875ba4e40269864641a168e Mon Sep 17 00:00:00 2001 From: ZeroNet Date: Thu, 5 Mar 2020 17:54:46 +0100 Subject: [PATCH 482/483] Fix sslcrypto thread safety (#2454) * Use sslcrypto instead of pyelliptic and pybitcointools * Fix CryptMessage * Support Python 3.4 * Fix user creation * Get rid of pyelliptic and pybitcointools * Fix typo * Delete test file * Add sslcrypto to tree * Update sslcrypto * Add pyaes to src/lib * Fix typo in tests * Update sslcrypto version * Use privatekey_bin instead of privatekey for bytes objects * Fix sslcrypto * Fix Benchmark plugin * Don't calculate the same thing twice * Only import sslcrypto once * Handle fallback sslcrypto implementation during tests * Fix sslcrypto fallback implementation selection * Fix thread safety * Add derivation * Bring split back * Fix typo * v3.3 * Fix custom OpenSSL discovery --- .gitlab-ci.yml | 2 +- .travis.yml | 5 +- plugins/Benchmark/BenchmarkPlugin.py | 4 +- plugins/CryptMessage/CryptMessage.py | 61 +- plugins/CryptMessage/CryptMessagePlugin.py | 56 +- plugins/CryptMessage/Test/TestCrypt.py | 7 +- requirements.txt | 1 - src/Crypt/CryptBitcoin.py | 87 +- src/Test/conftest.py | 2 +- .../libsecp256k1message.py | 43 +- .../LICENSE => pyaes/LICENSE.txt} | 9 +- src/lib/pyaes/README.md | 363 +++ src/lib/pyaes/__init__.py | 53 + src/lib/pyaes/aes.py | 589 +++++ src/lib/pyaes/blockfeeder.py | 227 ++ src/lib/pyaes/util.py | 60 + src/lib/pybitcointools/__init__.py | 10 - src/lib/pybitcointools/bci.py | 528 ----- src/lib/pybitcointools/blocks.py | 50 - src/lib/pybitcointools/composite.py | 128 -- src/lib/pybitcointools/deterministic.py | 199 -- src/lib/pybitcointools/english.txt | 2048 ----------------- src/lib/pybitcointools/main.py | 581 ----- src/lib/pybitcointools/mnemonic.py | 127 - src/lib/pybitcointools/py2specials.py | 98 - src/lib/pybitcointools/py3specials.py | 123 - src/lib/pybitcointools/ripemd.py | 414 ---- src/lib/pybitcointools/stealth.py | 100 - src/lib/pybitcointools/transaction.py | 514 ----- src/lib/pyelliptic/LICENSE | 674 ------ src/lib/pyelliptic/README.md | 96 - src/lib/pyelliptic/__init__.py | 19 - src/lib/pyelliptic/arithmetic.py | 144 -- src/lib/pyelliptic/cipher.py | 84 - src/lib/pyelliptic/ecc.py | 505 ---- src/lib/pyelliptic/hash.py | 69 - src/lib/pyelliptic/openssl.py | 553 ----- src/lib/pyelliptic/setup.py | 23 - src/lib/sslcrypto/LICENSE | 27 + src/lib/sslcrypto/__init__.py | 6 + src/lib/sslcrypto/_aes.py | 53 + src/lib/sslcrypto/_ecc.py | 334 +++ src/lib/sslcrypto/_ripemd.py | 375 +++ src/lib/sslcrypto/fallback/__init__.py | 3 + src/lib/sslcrypto/fallback/_jacobian.py | 159 ++ src/lib/sslcrypto/fallback/_util.py | 79 + src/lib/sslcrypto/fallback/aes.py | 101 + src/lib/sslcrypto/fallback/ecc.py | 371 +++ src/lib/sslcrypto/fallback/rsa.py | 8 + src/lib/sslcrypto/openssl/__init__.py | 3 + src/lib/sslcrypto/openssl/aes.py | 156 ++ src/lib/sslcrypto/openssl/discovery.py | 3 + src/lib/sslcrypto/openssl/ecc.py | 575 +++++ src/lib/sslcrypto/openssl/library.py | 98 + src/lib/sslcrypto/openssl/rsa.py | 11 + src/util/Electrum.py | 39 + src/util/OpensslFindPatch.py | 30 +- 57 files changed, 3781 insertions(+), 7306 deletions(-) rename src/lib/{pybitcointools/LICENSE => pyaes/LICENSE.txt} (80%) create mode 100644 src/lib/pyaes/README.md create mode 100644 src/lib/pyaes/__init__.py create mode 100644 src/lib/pyaes/aes.py create mode 100644 src/lib/pyaes/blockfeeder.py create mode 100644 src/lib/pyaes/util.py delete mode 100644 src/lib/pybitcointools/__init__.py delete mode 100644 src/lib/pybitcointools/bci.py delete mode 100644 src/lib/pybitcointools/blocks.py delete mode 100644 src/lib/pybitcointools/composite.py delete mode 100644 src/lib/pybitcointools/deterministic.py delete mode 100644 src/lib/pybitcointools/english.txt delete mode 100644 src/lib/pybitcointools/main.py delete mode 100644 src/lib/pybitcointools/mnemonic.py delete mode 100644 src/lib/pybitcointools/py2specials.py delete mode 100644 src/lib/pybitcointools/py3specials.py delete mode 100644 src/lib/pybitcointools/ripemd.py delete mode 100644 src/lib/pybitcointools/stealth.py delete mode 100644 src/lib/pybitcointools/transaction.py delete mode 100644 src/lib/pyelliptic/LICENSE delete mode 100644 src/lib/pyelliptic/README.md delete mode 100644 src/lib/pyelliptic/__init__.py delete mode 100644 src/lib/pyelliptic/arithmetic.py delete mode 100644 src/lib/pyelliptic/cipher.py delete mode 100644 src/lib/pyelliptic/ecc.py delete mode 100644 src/lib/pyelliptic/hash.py delete mode 100644 src/lib/pyelliptic/openssl.py delete mode 100644 src/lib/pyelliptic/setup.py create mode 100644 src/lib/sslcrypto/LICENSE create mode 100644 src/lib/sslcrypto/__init__.py create mode 100644 src/lib/sslcrypto/_aes.py create mode 100644 src/lib/sslcrypto/_ecc.py create mode 100644 src/lib/sslcrypto/_ripemd.py create mode 100644 src/lib/sslcrypto/fallback/__init__.py create mode 100644 src/lib/sslcrypto/fallback/_jacobian.py create mode 100644 src/lib/sslcrypto/fallback/_util.py create mode 100644 src/lib/sslcrypto/fallback/aes.py create mode 100644 src/lib/sslcrypto/fallback/ecc.py create mode 100644 src/lib/sslcrypto/fallback/rsa.py create mode 100644 src/lib/sslcrypto/openssl/__init__.py create mode 100644 src/lib/sslcrypto/openssl/aes.py create mode 100644 src/lib/sslcrypto/openssl/discovery.py create mode 100644 src/lib/sslcrypto/openssl/ecc.py create mode 100644 src/lib/sslcrypto/openssl/library.py create mode 100644 src/lib/sslcrypto/openssl/rsa.py create mode 100644 src/util/Electrum.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b62c7b0c..f3e1ed29 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ stages: - python -m pytest -x plugins/Multiuser/Test --color=yes - mv plugins/disabled-Bootstrapper plugins/Bootstrapper - python -m pytest -x plugins/Bootstrapper/Test --color=yes - - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ test:py3.4: image: python:3.4.3 diff --git a/.travis.yml b/.travis.yml index 148bd402..bdaafa22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,10 +33,13 @@ script: - export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test - find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" - find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" - - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pybitcointools/ + - flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ after_failure: - zip -r log.zip log/ - curl --upload-file ./log.zip https://transfer.sh/log.zip +after_success: + - codecov + - coveralls --rcfile=src/Test/coverage.ini notifications: email: recipients: diff --git a/plugins/Benchmark/BenchmarkPlugin.py b/plugins/Benchmark/BenchmarkPlugin.py index f22e6a26..8af140d8 100644 --- a/plugins/Benchmark/BenchmarkPlugin.py +++ b/plugins/Benchmark/BenchmarkPlugin.py @@ -104,8 +104,8 @@ class ActionsPlugin: tests.extend([ {"func": self.testHdPrivatekey, "num": 50, "time_standard": 0.57}, {"func": self.testSign, "num": 20, "time_standard": 0.46}, - {"func": self.testVerify, "kwargs": {"lib_verify": "btctools"}, "num": 20, "time_standard": 0.38}, - {"func": self.testVerify, "kwargs": {"lib_verify": "openssl"}, "num": 200, "time_standard": 0.30}, + {"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto_fallback"}, "num": 20, "time_standard": 0.38}, + {"func": self.testVerify, "kwargs": {"lib_verify": "sslcrypto"}, "num": 200, "time_standard": 0.30}, {"func": self.testVerify, "kwargs": {"lib_verify": "libsecp256k1"}, "num": 200, "time_standard": 0.10}, {"func": self.testPackMsgpack, "num": 100, "time_standard": 0.35}, diff --git a/plugins/CryptMessage/CryptMessage.py b/plugins/CryptMessage/CryptMessage.py index 38abbe5d..74659404 100644 --- a/plugins/CryptMessage/CryptMessage.py +++ b/plugins/CryptMessage/CryptMessage.py @@ -1,28 +1,22 @@ import hashlib import base64 import struct - -import lib.pybitcointools as btctools +from lib import sslcrypto from Crypt import Crypt -ecc_cache = {} + +curve = sslcrypto.ecc.get_curve("secp256k1") -def eciesEncrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): - from lib import pyelliptic - pubkey_openssl = toOpensslPublickey(base64.b64decode(pubkey)) - curve, pubkey_x, pubkey_y, i = pyelliptic.ECC._decode_pubkey(pubkey_openssl) - if ephemcurve is None: - ephemcurve = curve - ephem = pyelliptic.ECC(curve=ephemcurve) - key = hashlib.sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - pubkey = ephem.get_pubkey() - iv = pyelliptic.OpenSSL.rand(pyelliptic.OpenSSL.get_cipher(ciphername).get_blocksize()) - ctx = pyelliptic.Cipher(key_e, iv, 1, ciphername) - ciphertext = iv + pubkey + ctx.ciphering(data) - mac = pyelliptic.hmac_sha256(key_m, ciphertext) - return key_e, ciphertext + mac +def eciesEncrypt(data, pubkey, ciphername="aes-256-cbc"): + ciphertext, key_e = curve.encrypt( + data, + base64.b64decode(pubkey), + algo=ciphername, + derivation="sha512", + return_aes_key=True + ) + return key_e, ciphertext @Crypt.thread_pool_crypt.wrap @@ -37,9 +31,8 @@ def eciesDecryptMulti(encrypted_datas, privatekey): return texts -def eciesDecrypt(encrypted_data, privatekey): - ecc_key = getEcc(privatekey) - return ecc_key.decrypt(base64.b64decode(encrypted_data)) +def eciesDecrypt(ciphertext, privatekey): + return curve.decrypt(base64.b64decode(ciphertext), curve.wif_to_private(privatekey), derivation="sha512") def decodePubkey(pubkey): @@ -63,29 +56,3 @@ def split(encrypted): ciphertext = encrypted[16 + i:-32] return iv, ciphertext - - -def getEcc(privatekey=None): - from lib import pyelliptic - global ecc_cache - if privatekey not in ecc_cache: - if privatekey: - publickey_bin = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin") - publickey_openssl = toOpensslPublickey(publickey_bin) - privatekey_openssl = toOpensslPrivatekey(privatekey) - ecc_cache[privatekey] = pyelliptic.ECC(curve='secp256k1', privkey=privatekey_openssl, pubkey=publickey_openssl) - else: - ecc_cache[None] = pyelliptic.ECC() - return ecc_cache[privatekey] - - -def toOpensslPrivatekey(privatekey): - privatekey_bin = btctools.encode_privkey(privatekey, "bin") - return b'\x02\xca\x00\x20' + privatekey_bin - - -def toOpensslPublickey(publickey): - publickey_bin = btctools.encode_pubkey(publickey, "bin") - publickey_bin = publickey_bin[1:] - publickey_openssl = b'\x02\xca\x00 ' + publickey_bin[:32] + b'\x00 ' + publickey_bin[32:] - return publickey_openssl diff --git a/plugins/CryptMessage/CryptMessagePlugin.py b/plugins/CryptMessage/CryptMessagePlugin.py index 45afe184..150bf8be 100644 --- a/plugins/CryptMessage/CryptMessagePlugin.py +++ b/plugins/CryptMessage/CryptMessagePlugin.py @@ -5,24 +5,22 @@ import gevent from Plugin import PluginManager from Crypt import CryptBitcoin, CryptHash -import lib.pybitcointools as btctools from Config import config +import sslcrypto + from . import CryptMessage +curve = sslcrypto.ecc.get_curve("secp256k1") + @PluginManager.registerTo("UiWebsocket") class UiWebsocketPlugin(object): - def eciesDecrypt(self, encrypted, privatekey): - back = CryptMessage.getEcc(privatekey).decrypt(encrypted) - return back.decode("utf8") - # - Actions - # Returns user's public key unique to site # Return: Public key def actionUserPublickey(self, to, index=0): - publickey = self.user.getEncryptPublickey(self.site.address, index) - self.response(to, publickey) + self.response(to, self.user.getEncryptPublickey(self.site.address, index)) # Encrypt a text using the publickey or user's sites unique publickey # Return: Encrypted text using base64 encoding @@ -55,23 +53,16 @@ class UiWebsocketPlugin(object): # Encrypt a text using AES # Return: Iv, AES key, Encrypted text - def actionAesEncrypt(self, to, text, key=None, iv=None): - from lib import pyelliptic - + def actionAesEncrypt(self, to, text, key=None): if key: key = base64.b64decode(key) else: - key = os.urandom(32) - - if iv: # Generate new AES key if not definied - iv = base64.b64decode(iv) - else: - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') + key = sslcrypto.aes.new_key() if text: - encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(text.encode("utf8")) + encrypted, iv = sslcrypto.aes.encrypt(text.encode("utf8"), key) else: - encrypted = b"" + encrypted, iv = b"", b"" res = [base64.b64encode(item).decode("utf8") for item in [key, iv, encrypted]] self.response(to, res) @@ -79,8 +70,6 @@ class UiWebsocketPlugin(object): # Decrypt a text using AES # Return: Decrypted text def actionAesDecrypt(self, to, *args): - from lib import pyelliptic - if len(args) == 3: # Single decrypt encrypted_texts = [(args[0], args[1])] keys = [args[2]] @@ -93,9 +82,8 @@ class UiWebsocketPlugin(object): iv = base64.b64decode(iv) text = None for key in keys: - ctx = pyelliptic.Cipher(base64.b64decode(key), iv, 0, ciphername='aes-256-cbc') try: - decrypted = ctx.ciphering(encrypted_text) + decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, base64.b64decode(key)) if decrypted and decrypted.decode("utf8"): # Valid text decoded text = decrypted.decode("utf8") except Exception as err: @@ -122,12 +110,11 @@ class UiWebsocketPlugin(object): # Gets the publickey of a given privatekey def actionEccPrivToPub(self, to, privatekey): - self.response(to, btctools.privtopub(privatekey)) + self.response(to, curve.private_to_public(curve.wif_to_private(privatekey))) # Gets the address of a given publickey def actionEccPubToAddr(self, to, publickey): - address = btctools.pubtoaddr(btctools.decode_pubkey(publickey)) - self.response(to, address) + self.response(to, curve.public_to_address(bytes.fromhex(publickey))) @PluginManager.registerTo("User") @@ -163,7 +150,7 @@ class UserPlugin(object): if "encrypt_publickey_%s" % index not in site_data: privatekey = self.getEncryptPrivatekey(address, param_index) - publickey = btctools.encode_pubkey(btctools.privtopub(privatekey), "bin_compressed") + publickey = curve.private_to_public(curve.wif_to_private(privatekey)) site_data["encrypt_publickey_%s" % index] = base64.b64encode(publickey).decode("utf8") return site_data["encrypt_publickey_%s" % index] @@ -200,8 +187,8 @@ class ActionsPlugin: aes_key, encrypted = CryptMessage.eciesEncrypt(self.utf8_text.encode("utf8"), self.publickey) for i in range(num_run): assert len(aes_key) == 32 - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == self.utf8_text.encode("utf8"), "%s != %s" % (ecc.decrypt(encrypted), self.utf8_text.encode("utf8")) + decrypted = CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) + assert decrypted == self.utf8_text.encode("utf8"), "%s != %s" % (decrypted, self.utf8_text.encode("utf8")) yield "." def testCryptEciesDecryptMulti(self, num_run=1): @@ -223,23 +210,16 @@ class ActionsPlugin: gevent.joinall(threads) def testCryptAesEncrypt(self, num_run=1): - from lib import pyelliptic - for i in range(num_run): key = os.urandom(32) - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') - encrypted = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) + encrypted = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) yield "." def testCryptAesDecrypt(self, num_run=1): - from lib import pyelliptic - key = os.urandom(32) - iv = pyelliptic.Cipher.gen_IV('aes-256-cbc') - encrypted_text = pyelliptic.Cipher(key, iv, 1, ciphername='aes-256-cbc').ciphering(self.utf8_text.encode("utf8")) + encrypted_text, iv = sslcrypto.aes.encrypt(self.utf8_text.encode("utf8"), key) for i in range(num_run): - ctx = pyelliptic.Cipher(key, iv, 0, ciphername='aes-256-cbc') - decrypted = ctx.ciphering(encrypted_text).decode("utf8") + decrypted = sslcrypto.aes.decrypt(encrypted_text, iv, key).decode("utf8") assert decrypted == self.utf8_text yield "." diff --git a/plugins/CryptMessage/Test/TestCrypt.py b/plugins/CryptMessage/Test/TestCrypt.py index 96f73761..25a077d8 100644 --- a/plugins/CryptMessage/Test/TestCrypt.py +++ b/plugins/CryptMessage/Test/TestCrypt.py @@ -18,13 +18,10 @@ class TestCrypt: assert len(aes_key) == 32 # assert len(encrypted) == 134 + int(len(text) / 16) * 16 # Not always true - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == text_repeated + assert CryptMessage.eciesDecrypt(base64.b64encode(encrypted), self.privatekey) == text_repeated def testDecryptEcies(self, user): - encrypted = base64.b64decode(self.ecies_encrypted_text) - ecc = CryptMessage.getEcc(self.privatekey) - assert ecc.decrypt(encrypted) == b"hello" + assert CryptMessage.eciesDecrypt(self.ecies_encrypted_text, self.privatekey) == b"hello" def testPublickey(self, ui_websocket): pub = ui_websocket.testAction("UserPublickey", 0) diff --git a/requirements.txt b/requirements.txt index c2893480..83f3c3ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,4 @@ pyasn1 websocket_client gevent-ws coincurve -python-bitcoinlib maxminddb diff --git a/src/Crypt/CryptBitcoin.py b/src/Crypt/CryptBitcoin.py index b6bfaa77..f54015dc 100644 --- a/src/Crypt/CryptBitcoin.py +++ b/src/Crypt/CryptBitcoin.py @@ -1,16 +1,21 @@ import logging import base64 +import binascii import time +import hashlib -from util import OpensslFindPatch -from lib import pybitcointools as btctools +from util.Electrum import dbl_format from Config import config -lib_verify_best = "btctools" +lib_verify_best = "sslcrypto" +import sslcrypto +sslcurve_native = sslcrypto.ecc.get_curve("secp256k1") +sslcurve_fallback = sslcrypto.fallback.ecc.get_curve("secp256k1") +sslcurve = sslcurve_native def loadLib(lib_name, silent=False): - global bitcoin, libsecp256k1message, lib_verify_best + global sslcurve, libsecp256k1message, lib_verify_best if lib_name == "libsecp256k1": s = time.time() from lib import libsecp256k1message @@ -21,24 +26,10 @@ def loadLib(lib_name, silent=False): "Libsecpk256k1 loaded: %s in %.3fs" % (type(coincurve._libsecp256k1.lib).__name__, time.time() - s) ) - elif lib_name == "openssl": - s = time.time() - import bitcoin.signmessage - import bitcoin.core.key - import bitcoin.wallet - - try: - # OpenSSL 1.1.0 - ssl_version = bitcoin.core.key._ssl.SSLeay() - except AttributeError: - # OpenSSL 1.1.1+ - ssl_version = bitcoin.core.key._ssl.OpenSSL_version_num() - - if not silent: - logging.info( - "OpenSSL loaded: %s, version: %.9X in %.3fs" % - (bitcoin.core.key._ssl, ssl_version, time.time() - s) - ) + elif lib_name == "sslcrypto": + sslcurve = sslcurve_native + elif lib_name == "sslcrypto_fallback": + sslcurve = sslcurve_fallback try: if not config.use_libsecp256k1: @@ -46,35 +37,30 @@ try: loadLib("libsecp256k1") lib_verify_best = "libsecp256k1" except Exception as err: - logging.info("Libsecp256k1 load failed: %s, try to load OpenSSL" % err) - try: - if not config.use_openssl: - raise Exception("Disabled by config") - loadLib("openssl") - lib_verify_best = "openssl" - except Exception as err: - logging.info("OpenSSL load failed: %s, falling back to slow bitcoin verify" % err) + logging.info("Libsecp256k1 load failed: %s" % err) -def newPrivatekey(uncompressed=True): # Return new private key - privatekey = btctools.encode_privkey(btctools.random_key(), "wif") - return privatekey +def newPrivatekey(): # Return new private key + return sslcurve.private_to_wif(sslcurve.new_private_key()).decode() def newSeed(): - return btctools.random_key() + return binascii.hexlify(sslcurve.new_private_key()).decode() def hdPrivatekey(seed, child): - masterkey = btctools.bip32_master_key(bytes(seed, "ascii")) - childkey = btctools.bip32_ckd(masterkey, child % 100000000) # Too large child id could cause problems - key = btctools.bip32_extract_key(childkey) - return btctools.encode_privkey(key, "wif") + # Too large child id could cause problems + privatekey_bin = sslcurve.derive_child(seed.encode(), child % 100000000) + return sslcurve.private_to_wif(privatekey_bin).decode() def privatekeyToAddress(privatekey): # Return address from private key try: - return btctools.privkey_to_address(privatekey) + if len(privatekey) == 64: + privatekey_bin = bytes.fromhex(privatekey) + else: + privatekey_bin = sslcurve.wif_to_private(privatekey.encode()) + return sslcurve.private_to_address(privatekey_bin, is_compressed=False).decode() except Exception: # Invalid privatekey return False @@ -82,8 +68,13 @@ def privatekeyToAddress(privatekey): # Return address from private key def sign(data, privatekey): # Return sign to data using private key if privatekey.startswith("23") and len(privatekey) > 52: return None # Old style private key not supported - sign = btctools.ecdsa_sign(data, privatekey) - return sign + return base64.b64encode(sslcurve.sign( + data.encode(), + sslcurve.wif_to_private(privatekey.encode()), + is_compressed=False, + recoverable=True, + hash=dbl_format + )).decode() def verify(data, valid_address, sign, lib_verify=None): # Verify data using address and sign @@ -95,17 +86,9 @@ def verify(data, valid_address, sign, lib_verify=None): # Verify data using add if lib_verify == "libsecp256k1": sign_address = libsecp256k1message.recover_address(data.encode("utf8"), sign).decode("utf8") - elif lib_verify == "openssl": - sig = base64.b64decode(sign) - message = bitcoin.signmessage.BitcoinMessage(data) - hash = message.GetHash() - - pubkey = bitcoin.core.key.CPubKey.recover_compact(hash, sig) - - sign_address = str(bitcoin.wallet.P2PKHBitcoinAddress.from_pubkey(pubkey)) - elif lib_verify == "btctools": # Use pure-python - pub = btctools.ecdsa_recover(data, sign) - sign_address = btctools.pubtoaddr(pub) + elif lib_verify in ("sslcrypto", "sslcrypto_fallback"): + publickey = sslcurve.recover(base64.b64decode(sign), data.encode(), hash=dbl_format) + sign_address = sslcurve.public_to_address(publickey).decode() else: raise Exception("No library enabled for signature verification") diff --git a/src/Test/conftest.py b/src/Test/conftest.py index cc4d5faf..8f9dc3a5 100644 --- a/src/Test/conftest.py +++ b/src/Test/conftest.py @@ -436,7 +436,7 @@ def db(request): return db -@pytest.fixture(params=["btctools", "openssl", "libsecp256k1"]) +@pytest.fixture(params=["sslcrypto", "sslcrypto_fallback", "libsecp256k1"]) def crypt_bitcoin_lib(request, monkeypatch): monkeypatch.setattr(CryptBitcoin, "lib_verify_best", request.param) CryptBitcoin.loadLib(request.param) diff --git a/src/lib/libsecp256k1message/libsecp256k1message.py b/src/lib/libsecp256k1message/libsecp256k1message.py index 92802e1c..59768b88 100644 --- a/src/lib/libsecp256k1message/libsecp256k1message.py +++ b/src/lib/libsecp256k1message/libsecp256k1message.py @@ -1,9 +1,9 @@ import hashlib -import struct import base64 from coincurve import PrivateKey, PublicKey from base58 import b58encode_check, b58decode_check from hmac import compare_digest +from util.Electrum import format as zero_format RECID_MIN = 0 RECID_MAX = 3 @@ -97,7 +97,7 @@ def sign_data(secretkey, byte_string): """Sign [byte_string] with [secretkey]. Return serialized signature compatible with Electrum (ZeroNet).""" # encode the message - encoded = _zero_format(byte_string) + encoded = zero_format(byte_string) # sign the message and get a coincurve signature signature = secretkey.sign_recoverable(encoded) # reserialize signature and return it @@ -110,7 +110,7 @@ def verify_data(key_digest, electrum_signature, byte_string): # reserialize signature signature = coincurve_sig(electrum_signature) # encode the message - encoded = _zero_format(byte_string) + encoded = zero_format(byte_string) # recover full public key from signature # "which guarantees a correct signature" publickey = recover_public_key(signature, encoded) @@ -130,45 +130,10 @@ def verify_sig(publickey, signature, byte_string): def verify_key(publickey, key_digest): return compare_digest(key_digest, public_digest(publickey)) - -# Electrum, the heck?! - -def bchr(i): - return struct.pack('B', i) - -def _zero_encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = b''.join([bchr(x) for x in range(256)]) - result = b'' - while val > 0: - index = val % base - result = code_string[index:index + 1] + result - val //= base - return code_string[0:1] * max(minlen - len(result), 0) + result - -def _zero_insane_int(x): - x = int(x) - if x < 253: - return bchr(x) - elif x < 65536: - return bchr(253) + _zero_encode(x, 256, 2)[::-1] - elif x < 4294967296: - return bchr(254) + _zero_encode(x, 256, 4)[::-1] - else: - return bchr(255) + _zero_encode(x, 256, 8)[::-1] - - -def _zero_magic(message): - return b'\x18Bitcoin Signed Message:\n' + _zero_insane_int(len(message)) + message - -def _zero_format(message): - padded = _zero_magic(message) - return hashlib.sha256(padded).digest() - def recover_address(data, sign): sign_bytes = base64.b64decode(sign) is_compressed = ((sign_bytes[0] - 27) & 4) != 0 - publickey = recover_public_key(coincurve_sig(sign_bytes), _zero_format(data)) + publickey = recover_public_key(coincurve_sig(sign_bytes), zero_format(data)) return compute_public_address(publickey, compressed=is_compressed) __all__ = [ diff --git a/src/lib/pybitcointools/LICENSE b/src/lib/pyaes/LICENSE.txt similarity index 80% rename from src/lib/pybitcointools/LICENSE rename to src/lib/pyaes/LICENSE.txt index c47d4ad0..0417a6c2 100644 --- a/src/lib/pybitcointools/LICENSE +++ b/src/lib/pyaes/LICENSE.txt @@ -1,12 +1,6 @@ -This code is public domain. Everyone has the right to do whatever they want -with it for any purpose. - -In case your jurisdiction does not consider the above disclaimer valid or -enforceable, here's an MIT license for you: - The MIT License (MIT) -Copyright (c) 2013 Vitalik Buterin +Copyright (c) 2014 Richard Moore Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -25,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/lib/pyaes/README.md b/src/lib/pyaes/README.md new file mode 100644 index 00000000..26e3b2ba --- /dev/null +++ b/src/lib/pyaes/README.md @@ -0,0 +1,363 @@ +pyaes +===== + +A pure-Python implementation of the AES block cipher algorithm and the common modes of operation (CBC, CFB, CTR, ECB and OFB). + + +Features +-------- + +* Supports all AES key sizes +* Supports all AES common modes +* Pure-Python (no external dependencies) +* BlockFeeder API allows streams to easily be encrypted and decrypted +* Python 2.x and 3.x support (make sure you pass in bytes(), not strings for Python 3) + + +API +--- + +All keys may be 128 bits (16 bytes), 192 bits (24 bytes) or 256 bits (32 bytes) long. + +To generate a random key use: +```python +import os + +# 128 bit, 192 bit and 256 bit keys +key_128 = os.urandom(16) +key_192 = os.urandom(24) +key_256 = os.urandom(32) +``` + +To generate keys from simple-to-remember passwords, consider using a _password-based key-derivation function_ such as [scrypt](https://github.com/ricmoo/pyscrypt). + + +### Common Modes of Operation + +There are many modes of operations, each with various pros and cons. In general though, the **CBC** and **CTR** modes are recommended. The **ECB is NOT recommended.**, and is included primarily for completeness. + +Each of the following examples assumes the following key: +```python +import pyaes + +# A 256 bit (32 byte) key +key = "This_key_for_demo_purposes_only!" + +# For some modes of operation we need a random initialization vector +# of 16 bytes +iv = "InitializationVe" +``` + + +#### Counter Mode of Operation (recommended) + +```python +aes = pyaes.AESModeOfOperationCTR(key) +plaintext = "Text may be any length you wish, no padding is required" +ciphertext = aes.encrypt(plaintext) + +# '''\xb6\x99\x10=\xa4\x96\x88\xd1\x89\x1co\xe6\x1d\xef;\x11\x03\xe3\xee +# \xa9V?wY\xbfe\xcdO\xe3\xdf\x9dV\x19\xe5\x8dk\x9fh\xb87>\xdb\xa3\xd6 +# \x86\xf4\xbd\xb0\x97\xf1\t\x02\xe9 \xed''' +print repr(ciphertext) + +# The counter mode of operation maintains state, so decryption requires +# a new instance be created +aes = pyaes.AESModeOfOperationCTR(key) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext + +# To use a custom initial value +counter = pyaes.Counter(initial_value = 100) +aes = pyaes.AESModeOfOperationCTR(key, counter = counter) +ciphertext = aes.encrypt(plaintext) + +# '''WZ\x844\x02\xbfoY\x1f\x12\xa6\xce\x03\x82Ei)\xf6\x97mX\x86\xe3\x9d +# _1\xdd\xbd\x87\xb5\xccEM_4\x01$\xa6\x81\x0b\xd5\x04\xd7Al\x07\xe5 +# \xb2\x0e\\\x0f\x00\x13,\x07''' +print repr(ciphertext) +``` + + +#### Cipher-Block Chaining (recommended) + +```python +aes = pyaes.AESModeOfOperationCBC(key, iv = iv) +plaintext = "TextMustBe16Byte" +ciphertext = aes.encrypt(plaintext) + +# '\xd6:\x18\xe6\xb1\xb3\xc3\xdc\x87\xdf\xa7|\x08{k\xb6' +print repr(ciphertext) + + +# The cipher-block chaining mode of operation maintains state, so +# decryption requires a new instance be created +aes = pyaes.AESModeOfOperationCBC(key, iv = iv) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Cipher Feedback + +```python +# Each block into the mode of operation must be a multiple of the segment +# size. For this example we choose 8 bytes. +aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) +plaintext = "TextMustBeAMultipleOfSegmentSize" +ciphertext = aes.encrypt(plaintext) + +# '''v\xa9\xc1w"\x8aL\x93\xcb\xdf\xa0/\xf8Y\x0b\x8d\x88i\xcb\x85rmp +# \x85\xfe\xafM\x0c)\xd5\xeb\xaf''' +print repr(ciphertext) + + +# The cipher-block chaining mode of operation maintains state, so +# decryption requires a new instance be created +aes = pyaes.AESModeOfOperationCFB(key, iv = iv, segment_size = 8) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Output Feedback Mode of Operation + +```python +aes = pyaes.AESModeOfOperationOFB(key, iv = iv) +plaintext = "Text may be any length you wish, no padding is required" +ciphertext = aes.encrypt(plaintext) + +# '''v\xa9\xc1wO\x92^\x9e\rR\x1e\xf7\xb1\xa2\x9d"l1\xc7\xe7\x9d\x87(\xc26s +# \xdd8\xc8@\xb6\xd9!\xf5\x0cM\xaa\x9b\xc4\xedLD\xe4\xb9\xd8\xdf\x9e\xac +# \xa1\xb8\xea\x0f\x8ev\xb5''' +print repr(ciphertext) + +# The counter mode of operation maintains state, so decryption requires +# a new instance be created +aes = pyaes.AESModeOfOperationOFB(key, iv = iv) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +#### Electronic Codebook (NOT recommended) + +```python +aes = pyaes.AESModeOfOperationECB(key) +plaintext = "TextMustBe16Byte" +ciphertext = aes.encrypt(plaintext) + +# 'L6\x95\x85\xe4\xd9\xf1\x8a\xfb\xe5\x94X\x80|\x19\xc3' +print repr(ciphertext) + +# Since there is no state stored in this mode of operation, it +# is not necessary to create a new aes object for decryption. +#aes = pyaes.AESModeOfOperationECB(key) +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext +``` + + +### BlockFeeder + +Since most of the modes of operations require data in specific block-sized or segment-sized blocks, it can be difficult when working with large arbitrary streams or strings of data. + +The BlockFeeder class is meant to make life easier for you, by buffering bytes across multiple calls and returning bytes as they are available, as well as padding or stripping the output when finished, if necessary. + +```python +import pyaes + +# Any mode of operation can be used; for this example CBC +key = "This_key_for_demo_purposes_only!" +iv = "InitializationVe" + +ciphertext = '' + +# We can encrypt one line at a time, regardles of length +encrypter = pyaes.Encrypter(pyaes.AESModeOfOperationCBC(key, iv)) +for line in file('/etc/passwd'): + ciphertext += encrypter.feed(line) + +# Make a final call to flush any remaining bytes and add paddin +ciphertext += encrypter.feed() + +# We can decrypt the cipher text in chunks (here we split it in half) +decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv)) +decrypted = decrypter.feed(ciphertext[:len(ciphertext) / 2]) +decrypted += decrypter.feed(ciphertext[len(ciphertext) / 2:]) + +# Again, make a final call to flush any remaining bytes and strip padding +decrypted += decrypter.feed() + +print file('/etc/passwd').read() == decrypted +``` + +### Stream Feeder + +This is meant to make it even easier to encrypt and decrypt streams and large files. + +```python +import pyaes + +# Any mode of operation can be used; for this example CTR +key = "This_key_for_demo_purposes_only!" + +# Create the mode of operation to encrypt with +mode = pyaes.AESModeOfOperationCTR(key) + +# The input and output files +file_in = file('/etc/passwd') +file_out = file('/tmp/encrypted.bin', 'wb') + +# Encrypt the data as a stream, the file is read in 8kb chunks, be default +pyaes.encrypt_stream(mode, file_in, file_out) + +# Close the files +file_in.close() +file_out.close() +``` + +Decrypting is identical, except you would use `pyaes.decrypt_stream`, and the encrypted file would be the `file_in` and target for decryption the `file_out`. + +### AES block cipher + +Generally you should use one of the modes of operation above. This may however be useful for experimenting with a custom mode of operation or dealing with encrypted blocks. + +The block cipher requires exactly one block of data to encrypt or decrypt, and each block should be an array with each element an integer representation of a byte. + +```python +import pyaes + +# 16 byte block of plain text +plaintext = "Hello World!!!!!" +plaintext_bytes = [ ord(c) for c in plaintext ] + +# 32 byte key (256 bit) +key = "This_key_for_demo_purposes_only!" + +# Our AES instance +aes = pyaes.AES(key) + +# Encrypt! +ciphertext = aes.encrypt(plaintext_bytes) + +# [55, 250, 182, 25, 185, 208, 186, 95, 206, 115, 50, 115, 108, 58, 174, 115] +print repr(ciphertext) + +# Decrypt! +decrypted = aes.decrypt(ciphertext) + +# True +print decrypted == plaintext_bytes +``` + +What is a key? +-------------- + +This seems to be a point of confusion for many people new to using encryption. You can think of the key as the *"password"*. However, these algorithms require the *"password"* to be a specific length. + +With AES, there are three possible key lengths, 16-bytes, 24-bytes or 32-bytes. When you create an AES object, the key size is automatically detected, so it is important to pass in a key of the correct length. + +Often, you wish to provide a password of arbitrary length, for example, something easy to remember or write down. In these cases, you must come up with a way to transform the password into a key, of a specific length. A **Password-Based Key Derivation Function** (PBKDF) is an algorithm designed for this exact purpose. + +Here is an example, using the popular (possibly obsolete?) *crypt* PBKDF: + +``` +# See: https://www.dlitz.net/software/python-pbkdf2/ +import pbkdf2 + +password = "HelloWorld" + +# The crypt PBKDF returns a 48-byte string +key = pbkdf2.crypt(password) + +# A 16-byte, 24-byte and 32-byte key, respectively +key_16 = key[:16] +key_24 = key[:24] +key_32 = key[:32] +``` + +The [scrypt](https://github.com/ricmoo/pyscrypt) PBKDF is intentionally slow, to make it more difficult to brute-force guess a password: + +``` +# See: https://github.com/ricmoo/pyscrypt +import pyscrypt + +password = "HelloWorld" + +# Salt is required, and prevents Rainbow Table attacks +salt = "SeaSalt" + +# N, r, and p are parameters to specify how difficult it should be to +# generate a key; bigger numbers take longer and more memory +N = 1024 +r = 1 +p = 1 + +# A 16-byte, 24-byte and 32-byte key, respectively; the scrypt algorithm takes +# a 6-th parameter, indicating key length +key_16 = pyscrypt.hash(password, salt, N, r, p, 16) +key_24 = pyscrypt.hash(password, salt, N, r, p, 24) +key_32 = pyscrypt.hash(password, salt, N, r, p, 32) +``` + +Another possibility, is to use a hashing function, such as SHA256 to hash the password, but this method may be vulnerable to [Rainbow Attacks](http://en.wikipedia.org/wiki/Rainbow_table), unless you use a [salt](http://en.wikipedia.org/wiki/Salt_(cryptography)). + +```python +import hashlib + +password = "HelloWorld" + +# The SHA256 hash algorithm returns a 32-byte string +hashed = hashlib.sha256(password).digest() + +# A 16-byte, 24-byte and 32-byte key, respectively +key_16 = hashed[:16] +key_24 = hashed[:24] +key_32 = hashed +``` + + + + +Performance +----------- + +There is a test case provided in _/tests/test-aes.py_ which does some basic performance testing (its primary purpose is moreso as a regression test). + +Based on that test, in **CPython**, this library is about 30x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 80x slower for CFB; and 300x slower for CTR. + +Based on that same test, in **Pypy**, this library is about 4x slower than [PyCrypto](https://www.dlitz.net/software/pycrypto/) for CBC, ECB and OFB; about 12x slower for CFB; and 19x slower for CTR. + +The PyCrypto documentation makes reference to the counter call being responsible for the speed problems of the counter (CTR) mode of operation, which is why they use a specially optimized counter. I will investigate this problem further in the future. + + +FAQ +--- + +#### Why do this? + +The short answer, *why not?* + +The longer answer, is for my [pyscrypt](https://github.com/ricmoo/pyscrypt) library. I required a pure-Python AES implementation that supported 256-bit keys with the counter (CTR) mode of operation. After searching, I found several implementations, but all were missing CTR or only supported 128 bit keys. After all the work of learning AES inside and out to implement the library, it was only a marginal amount of extra work to library-ify a more general solution. So, *why not?* + +#### How do I get a question I have added? + +E-mail me at pyaes@ricmoo.com with any questions, suggestions, comments, et cetera. + + +#### Can I give you my money? + +Umm... Ok? :-) + +_Bitcoin_ - `18UDs4qV1shu2CgTS2tKojhCtM69kpnWg9` diff --git a/src/lib/pyaes/__init__.py b/src/lib/pyaes/__init__.py new file mode 100644 index 00000000..5712f794 --- /dev/null +++ b/src/lib/pyaes/__init__.py @@ -0,0 +1,53 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + +# See the README.md for API details and general information. + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +VERSION = [1, 3, 0] + +from .aes import AES, AESModeOfOperationCTR, AESModeOfOperationCBC, AESModeOfOperationCFB, AESModeOfOperationECB, AESModeOfOperationOFB, AESModesOfOperation, Counter +from .blockfeeder import decrypt_stream, Decrypter, encrypt_stream, Encrypter +from .blockfeeder import PADDING_NONE, PADDING_DEFAULT diff --git a/src/lib/pyaes/aes.py b/src/lib/pyaes/aes.py new file mode 100644 index 00000000..c6e8bc02 --- /dev/null +++ b/src/lib/pyaes/aes.py @@ -0,0 +1,589 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# This is a pure-Python implementation of the AES algorithm and AES common +# modes of operation. + +# See: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard + +# Honestly, the best description of the modes of operations are the wonderful +# diagrams on Wikipedia. They explain in moments what my words could never +# achieve. Hence the inline documentation here is sparer than I'd prefer. +# See: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation + +# Also useful, PyCrypto, a crypto library implemented in C with Python bindings: +# https://www.dlitz.net/software/pycrypto/ + + +# Supported key sizes: +# 128-bit +# 192-bit +# 256-bit + + +# Supported modes of operation: +# ECB - Electronic Codebook +# CBC - Cipher-Block Chaining +# CFB - Cipher Feedback +# OFB - Output Feedback +# CTR - Counter + + +# See the README.md for API details and general information. + + +import copy +import struct + +__all__ = ["AES", "AESModeOfOperationCTR", "AESModeOfOperationCBC", "AESModeOfOperationCFB", + "AESModeOfOperationECB", "AESModeOfOperationOFB", "AESModesOfOperation", "Counter"] + + +def _compact_word(word): + return (word[0] << 24) | (word[1] << 16) | (word[2] << 8) | word[3] + +def _string_to_bytes(text): + return list(ord(c) for c in text) + +def _bytes_to_string(binary): + return "".join(chr(b) for b in binary) + +def _concat_list(a, b): + return a + b + + +# Python 3 compatibility +try: + xrange +except Exception: + xrange = range + + # Python 3 supports bytes, which is already an array of integers + def _string_to_bytes(text): + if isinstance(text, bytes): + return text + return [ord(c) for c in text] + + # In Python 3, we return bytes + def _bytes_to_string(binary): + return bytes(binary) + + # Python 3 cannot concatenate a list onto a bytes, so we bytes-ify it first + def _concat_list(a, b): + return a + bytes(b) + + +# Based *largely* on the Rijndael implementation +# See: http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf +class AES(object): + '''Encapsulates the AES block cipher. + + You generally should not need this. Use the AESModeOfOperation classes + below instead.''' + + # Number of rounds by keysize + number_of_rounds = {16: 10, 24: 12, 32: 14} + + # Round constant words + rcon = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 ] + + # S-box and Inverse S-box (S is for Substitution) + S = [ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 ] + Si =[ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d ] + + # Transformations for encryption + T1 = [ 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a ] + T2 = [ 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616 ] + T3 = [ 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16 ] + T4 = [ 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c ] + + # Transformations for decryption + T5 = [ 0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742 ] + T6 = [ 0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857 ] + T7 = [ 0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8 ] + T8 = [ 0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0 ] + + # Transformations for decryption key expansion + U1 = [ 0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3 ] + U2 = [ 0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697 ] + U3 = [ 0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46 ] + U4 = [ 0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d ] + + def __init__(self, key): + + if len(key) not in (16, 24, 32): + raise ValueError('Invalid key size') + + rounds = self.number_of_rounds[len(key)] + + # Encryption round keys + self._Ke = [[0] * 4 for i in xrange(rounds + 1)] + + # Decryption round keys + self._Kd = [[0] * 4 for i in xrange(rounds + 1)] + + round_key_count = (rounds + 1) * 4 + KC = len(key) // 4 + + # Convert the key into ints + tk = [ struct.unpack('>i', key[i:i + 4])[0] for i in xrange(0, len(key), 4) ] + + # Copy values into round key arrays + for i in xrange(0, KC): + self._Ke[i // 4][i % 4] = tk[i] + self._Kd[rounds - (i // 4)][i % 4] = tk[i] + + # Key expansion (fips-197 section 5.2) + rconpointer = 0 + t = KC + while t < round_key_count: + + tt = tk[KC - 1] + tk[0] ^= ((self.S[(tt >> 16) & 0xFF] << 24) ^ + (self.S[(tt >> 8) & 0xFF] << 16) ^ + (self.S[ tt & 0xFF] << 8) ^ + self.S[(tt >> 24) & 0xFF] ^ + (self.rcon[rconpointer] << 24)) + rconpointer += 1 + + if KC != 8: + for i in xrange(1, KC): + tk[i] ^= tk[i - 1] + + # Key expansion for 256-bit keys is "slightly different" (fips-197) + else: + for i in xrange(1, KC // 2): + tk[i] ^= tk[i - 1] + tt = tk[KC // 2 - 1] + + tk[KC // 2] ^= (self.S[ tt & 0xFF] ^ + (self.S[(tt >> 8) & 0xFF] << 8) ^ + (self.S[(tt >> 16) & 0xFF] << 16) ^ + (self.S[(tt >> 24) & 0xFF] << 24)) + + for i in xrange(KC // 2 + 1, KC): + tk[i] ^= tk[i - 1] + + # Copy values into round key arrays + j = 0 + while j < KC and t < round_key_count: + self._Ke[t // 4][t % 4] = tk[j] + self._Kd[rounds - (t // 4)][t % 4] = tk[j] + j += 1 + t += 1 + + # Inverse-Cipher-ify the decryption round key (fips-197 section 5.3) + for r in xrange(1, rounds): + for j in xrange(0, 4): + tt = self._Kd[r][j] + self._Kd[r][j] = (self.U1[(tt >> 24) & 0xFF] ^ + self.U2[(tt >> 16) & 0xFF] ^ + self.U3[(tt >> 8) & 0xFF] ^ + self.U4[ tt & 0xFF]) + + def encrypt(self, plaintext): + 'Encrypt a block of plain text using the AES block cipher.' + + if len(plaintext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Ke) - 1 + (s1, s2, s3) = [1, 2, 3] + a = [0, 0, 0, 0] + + # Convert plaintext to (ints ^ key) + t = [(_compact_word(plaintext[4 * i:4 * i + 4]) ^ self._Ke[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T1[(t[ i ] >> 24) & 0xFF] ^ + self.T2[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T3[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T4[ t[(i + s3) % 4] & 0xFF] ^ + self._Ke[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Ke[rounds][i] + result.append((self.S[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.S[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.S[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.S[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + def decrypt(self, ciphertext): + 'Decrypt a block of cipher text using the AES block cipher.' + + if len(ciphertext) != 16: + raise ValueError('wrong block length') + + rounds = len(self._Kd) - 1 + (s1, s2, s3) = [3, 2, 1] + a = [0, 0, 0, 0] + + # Convert ciphertext to (ints ^ key) + t = [(_compact_word(ciphertext[4 * i:4 * i + 4]) ^ self._Kd[0][i]) for i in xrange(0, 4)] + + # Apply round transforms + for r in xrange(1, rounds): + for i in xrange(0, 4): + a[i] = (self.T5[(t[ i ] >> 24) & 0xFF] ^ + self.T6[(t[(i + s1) % 4] >> 16) & 0xFF] ^ + self.T7[(t[(i + s2) % 4] >> 8) & 0xFF] ^ + self.T8[ t[(i + s3) % 4] & 0xFF] ^ + self._Kd[r][i]) + t = copy.copy(a) + + # The last round is special + result = [ ] + for i in xrange(0, 4): + tt = self._Kd[rounds][i] + result.append((self.Si[(t[ i ] >> 24) & 0xFF] ^ (tt >> 24)) & 0xFF) + result.append((self.Si[(t[(i + s1) % 4] >> 16) & 0xFF] ^ (tt >> 16)) & 0xFF) + result.append((self.Si[(t[(i + s2) % 4] >> 8) & 0xFF] ^ (tt >> 8)) & 0xFF) + result.append((self.Si[ t[(i + s3) % 4] & 0xFF] ^ tt ) & 0xFF) + + return result + + +class Counter(object): + '''A counter object for the Counter (CTR) mode of operation. + + To create a custom counter, you can usually just override the + increment method.''' + + def __init__(self, initial_value = 1): + + # Convert the value into an array of bytes long + self._counter = [ ((initial_value >> i) % 256) for i in xrange(128 - 8, -1, -8) ] + + value = property(lambda s: s._counter) + + def increment(self): + '''Increment the counter (overflow rolls back to 0).''' + + for i in xrange(len(self._counter) - 1, -1, -1): + self._counter[i] += 1 + + if self._counter[i] < 256: break + + # Carry the one + self._counter[i] = 0 + + # Overflow + else: + self._counter = [ 0 ] * len(self._counter) + + +class AESBlockModeOfOperation(object): + '''Super-class for AES modes of operation that require blocks.''' + def __init__(self, key): + self._aes = AES(key) + + def decrypt(self, ciphertext): + raise Exception('not implemented') + + def encrypt(self, plaintext): + raise Exception('not implemented') + + +class AESStreamModeOfOperation(AESBlockModeOfOperation): + '''Super-class for AES modes of operation that are stream-ciphers.''' + +class AESSegmentModeOfOperation(AESStreamModeOfOperation): + '''Super-class for AES modes of operation that segment data.''' + + segment_bytes = 16 + + + +class AESModeOfOperationECB(AESBlockModeOfOperation): + '''AES Electronic Codebook Mode of Operation. + + o Block-cipher, so data must be padded to 16 byte boundaries + + Security Notes: + o This mode is not recommended + o Any two identical blocks produce identical encrypted values, + exposing data patterns. (See the image of Tux on wikipedia) + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_codebook_.28ECB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.1''' + + + name = "Electronic Codebook (ECB)" + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + return _bytes_to_string(self._aes.encrypt(plaintext)) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + ciphertext = _string_to_bytes(ciphertext) + return _bytes_to_string(self._aes.decrypt(ciphertext)) + + + +class AESModeOfOperationCBC(AESBlockModeOfOperation): + '''AES Cipher-Block Chaining Mode of Operation. + + o The Initialization Vector (IV) + o Block-cipher, so data must be padded to 16 byte boundaries + o An incorrect initialization vector will only cause the first + block to be corrupt; all other blocks will be intact + o A corrupt bit in the cipher text will cause a block to be + corrupted, and the next block to be inverted, but all other + blocks will be intact. + + Security Notes: + o This method (and CTR) ARE recommended. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.2''' + + + name = "Cipher-Block Chaining (CBC)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_cipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_cipherblock = _string_to_bytes(iv) + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + if len(plaintext) != 16: + raise ValueError('plaintext block must be 16 bytes') + + plaintext = _string_to_bytes(plaintext) + precipherblock = [ (p ^ l) for (p, l) in zip(plaintext, self._last_cipherblock) ] + self._last_cipherblock = self._aes.encrypt(precipherblock) + + return _bytes_to_string(self._last_cipherblock) + + def decrypt(self, ciphertext): + if len(ciphertext) != 16: + raise ValueError('ciphertext block must be 16 bytes') + + cipherblock = _string_to_bytes(ciphertext) + plaintext = [ (p ^ l) for (p, l) in zip(self._aes.decrypt(cipherblock), self._last_cipherblock) ] + self._last_cipherblock = cipherblock + + return _bytes_to_string(plaintext) + + + +class AESModeOfOperationCFB(AESSegmentModeOfOperation): + '''AES Cipher Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + but does need to be padded to segment_size + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_.28CFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.3''' + + + name = "Cipher Feedback (CFB)" + + def __init__(self, key, iv, segment_size = 1): + if segment_size == 0: segment_size = 1 + + if iv is None: + self._shift_register = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._shift_register = _string_to_bytes(iv) + + self._segment_bytes = segment_size + + AESBlockModeOfOperation.__init__(self, key) + + segment_bytes = property(lambda s: s._segment_bytes) + + def encrypt(self, plaintext): + if len(plaintext) % self._segment_bytes != 0: + raise ValueError('plaintext block must be a multiple of segment_size') + + plaintext = _string_to_bytes(plaintext) + + # Break block into segments + encrypted = [ ] + for i in xrange(0, len(plaintext), self._segment_bytes): + plaintext_segment = plaintext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(plaintext_segment)] + cipher_segment = [ (p ^ x) for (p, x) in zip(plaintext_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + encrypted.extend(cipher_segment) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + if len(ciphertext) % self._segment_bytes != 0: + raise ValueError('ciphertext block must be a multiple of segment_size') + + ciphertext = _string_to_bytes(ciphertext) + + # Break block into segments + decrypted = [ ] + for i in xrange(0, len(ciphertext), self._segment_bytes): + cipher_segment = ciphertext[i: i + self._segment_bytes] + xor_segment = self._aes.encrypt(self._shift_register)[:len(cipher_segment)] + plaintext_segment = [ (p ^ x) for (p, x) in zip(cipher_segment, xor_segment) ] + + # Shift the top bits out and the ciphertext in + self._shift_register = _concat_list(self._shift_register[len(cipher_segment):], cipher_segment) + + decrypted.extend(plaintext_segment) + + return _bytes_to_string(decrypted) + + + +class AESModeOfOperationOFB(AESStreamModeOfOperation): + '''AES Output Feedback Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o A bit twiddled in the cipher text, twiddles the same bit in the + same bit in the plain text, which can be useful for error + correction techniques. + + Also see: + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Output_feedback_.28OFB.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.4''' + + + name = "Output Feedback (OFB)" + + def __init__(self, key, iv = None): + if iv is None: + self._last_precipherblock = [ 0 ] * 16 + elif len(iv) != 16: + raise ValueError('initialization vector must be 16 bytes') + else: + self._last_precipherblock = _string_to_bytes(iv) + + self._remaining_block = [ ] + + AESBlockModeOfOperation.__init__(self, key) + + def encrypt(self, plaintext): + encrypted = [ ] + for p in _string_to_bytes(plaintext): + if len(self._remaining_block) == 0: + self._remaining_block = self._aes.encrypt(self._last_precipherblock) + self._last_precipherblock = [ ] + precipherbyte = self._remaining_block.pop(0) + self._last_precipherblock.append(precipherbyte) + cipherbyte = p ^ precipherbyte + encrypted.append(cipherbyte) + + return _bytes_to_string(encrypted) + + def decrypt(self, ciphertext): + # AES-OFB is symetric + return self.encrypt(ciphertext) + + + +class AESModeOfOperationCTR(AESStreamModeOfOperation): + '''AES Counter Mode of Operation. + + o A stream-cipher, so input does not need to be padded to blocks, + allowing arbitrary length data. + o The counter must be the same size as the key size (ie. len(key)) + o Each block independant of the other, so a corrupt byte will not + damage future blocks. + o Each block has a uniue counter value associated with it, which + contributes to the encrypted value, so no data patterns are + leaked. + o Also known as: Counter Mode (CM), Integer Counter Mode (ICM) and + Segmented Integer Counter (SIC + + Security Notes: + o This method (and CBC) ARE recommended. + o Each message block is associated with a counter value which must be + unique for ALL messages with the same key. Otherwise security may be + compromised. + + Also see: + + o https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29 + o See NIST SP800-38A (http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf); section 6.5 + and Appendix B for managing the initial counter''' + + + name = "Counter (CTR)" + + def __init__(self, key, counter = None): + AESBlockModeOfOperation.__init__(self, key) + + if counter is None: + counter = Counter() + + self._counter = counter + self._remaining_counter = [ ] + + def encrypt(self, plaintext): + while len(self._remaining_counter) < len(plaintext): + self._remaining_counter += self._aes.encrypt(self._counter.value) + self._counter.increment() + + plaintext = _string_to_bytes(plaintext) + + encrypted = [ (p ^ c) for (p, c) in zip(plaintext, self._remaining_counter) ] + self._remaining_counter = self._remaining_counter[len(encrypted):] + + return _bytes_to_string(encrypted) + + def decrypt(self, crypttext): + # AES-CTR is symetric + return self.encrypt(crypttext) + + +# Simple lookup table for each mode +AESModesOfOperation = dict( + ctr = AESModeOfOperationCTR, + cbc = AESModeOfOperationCBC, + cfb = AESModeOfOperationCFB, + ecb = AESModeOfOperationECB, + ofb = AESModeOfOperationOFB, +) diff --git a/src/lib/pyaes/blockfeeder.py b/src/lib/pyaes/blockfeeder.py new file mode 100644 index 00000000..b9a904d2 --- /dev/null +++ b/src/lib/pyaes/blockfeeder.py @@ -0,0 +1,227 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +from .aes import AESBlockModeOfOperation, AESSegmentModeOfOperation, AESStreamModeOfOperation +from .util import append_PKCS7_padding, strip_PKCS7_padding, to_bufferable + + +# First we inject three functions to each of the modes of operations +# +# _can_consume(size) +# - Given a size, determine how many bytes could be consumed in +# a single call to either the decrypt or encrypt method +# +# _final_encrypt(data, padding = PADDING_DEFAULT) +# - call and return encrypt on this (last) chunk of data, +# padding as necessary; this will always be at least 16 +# bytes unless the total incoming input was less than 16 +# bytes +# +# _final_decrypt(data, padding = PADDING_DEFAULT) +# - same as _final_encrypt except for decrypt, for +# stripping off padding +# + +PADDING_NONE = 'none' +PADDING_DEFAULT = 'default' + +# @TODO: Ciphertext stealing and explicit PKCS#7 +# PADDING_CIPHERTEXT_STEALING +# PADDING_PKCS7 + +# ECB and CBC are block-only ciphers + +def _block_can_consume(self, size): + if size >= 16: return 16 + return 0 + +# After padding, we may have more than one block +def _block_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + data = append_PKCS7_padding(data) + + elif padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + else: + raise Exception('invalid padding option') + + if len(data) == 32: + return self.encrypt(data[:16]) + self.encrypt(data[16:]) + + return self.encrypt(data) + + +def _block_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding == PADDING_DEFAULT: + return strip_PKCS7_padding(self.decrypt(data)) + + if padding == PADDING_NONE: + if len(data) != 16: + raise Exception('invalid data length for final block') + return self.decrypt(data) + + raise Exception('invalid padding option') + +AESBlockModeOfOperation._can_consume = _block_can_consume +AESBlockModeOfOperation._final_encrypt = _block_final_encrypt +AESBlockModeOfOperation._final_decrypt = _block_final_decrypt + + + +# CFB is a segment cipher + +def _segment_can_consume(self, size): + return self.segment_bytes * int(size // self.segment_bytes) + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.encrypt(padded)[:len(data)] + +# CFB can handle a non-segment-sized block at the end using the remaining cipherblock +def _segment_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding != PADDING_DEFAULT: + raise Exception('invalid padding option') + + faux_padding = (chr(0) * (self.segment_bytes - (len(data) % self.segment_bytes))) + padded = data + to_bufferable(faux_padding) + return self.decrypt(padded)[:len(data)] + +AESSegmentModeOfOperation._can_consume = _segment_can_consume +AESSegmentModeOfOperation._final_encrypt = _segment_final_encrypt +AESSegmentModeOfOperation._final_decrypt = _segment_final_decrypt + + + +# OFB and CTR are stream ciphers + +def _stream_can_consume(self, size): + return size + +def _stream_final_encrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.encrypt(data) + +def _stream_final_decrypt(self, data, padding = PADDING_DEFAULT): + if padding not in [PADDING_NONE, PADDING_DEFAULT]: + raise Exception('invalid padding option') + + return self.decrypt(data) + +AESStreamModeOfOperation._can_consume = _stream_can_consume +AESStreamModeOfOperation._final_encrypt = _stream_final_encrypt +AESStreamModeOfOperation._final_decrypt = _stream_final_decrypt + + + +class BlockFeeder(object): + '''The super-class for objects to handle chunking a stream of bytes + into the appropriate block size for the underlying mode of operation + and applying (or stripping) padding, as necessary.''' + + def __init__(self, mode, feed, final, padding = PADDING_DEFAULT): + self._mode = mode + self._feed = feed + self._final = final + self._buffer = to_bufferable("") + self._padding = padding + + def feed(self, data = None): + '''Provide bytes to encrypt (or decrypt), returning any bytes + possible from this or any previous calls to feed. + + Call with None or an empty string to flush the mode of + operation and return any final bytes; no further calls to + feed may be made.''' + + if self._buffer is None: + raise ValueError('already finished feeder') + + # Finalize; process the spare bytes we were keeping + if data is None: + result = self._final(self._buffer, self._padding) + self._buffer = None + return result + + self._buffer += to_bufferable(data) + + # We keep 16 bytes around so we can determine padding + result = to_bufferable('') + while len(self._buffer) > 16: + can_consume = self._mode._can_consume(len(self._buffer) - 16) + if can_consume == 0: break + result += self._feed(self._buffer[:can_consume]) + self._buffer = self._buffer[can_consume:] + + return result + + +class Encrypter(BlockFeeder): + 'Accepts bytes of plaintext and returns encrypted ciphertext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.encrypt, mode._final_encrypt, padding) + + +class Decrypter(BlockFeeder): + 'Accepts bytes of ciphertext and returns decrypted plaintext.' + + def __init__(self, mode, padding = PADDING_DEFAULT): + BlockFeeder.__init__(self, mode, mode.decrypt, mode._final_decrypt, padding) + + +# 8kb blocks +BLOCK_SIZE = (1 << 13) + +def _feed_stream(feeder, in_stream, out_stream, block_size = BLOCK_SIZE): + 'Uses feeder to read and convert from in_stream and write to out_stream.' + + while True: + chunk = in_stream.read(block_size) + if not chunk: + break + converted = feeder.feed(chunk) + out_stream.write(converted) + converted = feeder.feed() + out_stream.write(converted) + + +def encrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Encrypts a stream of bytes from in_stream to out_stream using mode.' + + encrypter = Encrypter(mode, padding = padding) + _feed_stream(encrypter, in_stream, out_stream, block_size) + + +def decrypt_stream(mode, in_stream, out_stream, block_size = BLOCK_SIZE, padding = PADDING_DEFAULT): + 'Decrypts a stream of bytes from in_stream to out_stream using mode.' + + decrypter = Decrypter(mode, padding = padding) + _feed_stream(decrypter, in_stream, out_stream, block_size) diff --git a/src/lib/pyaes/util.py b/src/lib/pyaes/util.py new file mode 100644 index 00000000..081a3759 --- /dev/null +++ b/src/lib/pyaes/util.py @@ -0,0 +1,60 @@ +# The MIT License (MIT) +# +# Copyright (c) 2014 Richard Moore +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# Why to_bufferable? +# Python 3 is very different from Python 2.x when it comes to strings of text +# and strings of bytes; in Python 3, strings of bytes do not exist, instead to +# represent arbitrary binary data, we must use the "bytes" object. This method +# ensures the object behaves as we need it to. + +def to_bufferable(binary): + return binary + +def _get_byte(c): + return ord(c) + +try: + xrange +except: + + def to_bufferable(binary): + if isinstance(binary, bytes): + return binary + return bytes(ord(b) for b in binary) + + def _get_byte(c): + return c + +def append_PKCS7_padding(data): + pad = 16 - (len(data) % 16) + return data + to_bufferable(chr(pad) * pad) + +def strip_PKCS7_padding(data): + if len(data) % 16 != 0: + raise ValueError("invalid length") + + pad = _get_byte(data[-1]) + + if pad > 16: + raise ValueError("invalid padding byte") + + return data[:-pad] diff --git a/src/lib/pybitcointools/__init__.py b/src/lib/pybitcointools/__init__.py deleted file mode 100644 index 7d529abc..00000000 --- a/src/lib/pybitcointools/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from .py2specials import * -from .py3specials import * -from .main import * -from .transaction import * -from .deterministic import * -from .bci import * -from .composite import * -from .stealth import * -from .blocks import * -from .mnemonic import * diff --git a/src/lib/pybitcointools/bci.py b/src/lib/pybitcointools/bci.py deleted file mode 100644 index 79a2c401..00000000 --- a/src/lib/pybitcointools/bci.py +++ /dev/null @@ -1,528 +0,0 @@ -#!/usr/bin/python -import json, re -import random -import sys -try: - from urllib.request import build_opener -except: - from urllib2 import build_opener - - -# Makes a request to a given URL (first arg) and optional params (second arg) -def make_request(*args): - opener = build_opener() - opener.addheaders = [('User-agent', - 'Mozilla/5.0'+str(random.randrange(1000000)))] - try: - return opener.open(*args).read().strip() - except Exception as e: - try: - p = e.read().strip() - except: - p = e - raise Exception(p) - - -def is_testnet(inp): - '''Checks if inp is a testnet address or if UTXO is a known testnet TxID''' - if isinstance(inp, (list, tuple)) and len(inp) >= 1: - return any([is_testnet(x) for x in inp]) - elif not isinstance(inp, basestring): # sanity check - raise TypeError("Input must be str/unicode, not type %s" % str(type(inp))) - - if not inp or (inp.lower() in ("btc", "testnet")): - pass - - ## ADDRESSES - if inp[0] in "123mn": - if re.match("^[2mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp): - return True - elif re.match("^[13][a-km-zA-HJ-NP-Z0-9]{26,33}$", inp): - return False - else: - #sys.stderr.write("Bad address format %s") - return None - - ## TXID - elif re.match('^[0-9a-fA-F]{64}$', inp): - base_url = "http://api.blockcypher.com/v1/btc/{network}/txs/{txid}?includesHex=false" - try: - # try testnet fetchtx - make_request(base_url.format(network="test3", txid=inp.lower())) - return True - except: - # try mainnet fetchtx - make_request(base_url.format(network="main", txid=inp.lower())) - return False - sys.stderr.write("TxID %s has no match for testnet or mainnet (Bad TxID)") - return None - else: - raise TypeError("{0} is unknown input".format(inp)) - - -def set_network(*args): - '''Decides if args for unspent/fetchtx/pushtx are mainnet or testnet''' - r = [] - for arg in args: - if not arg: - pass - if isinstance(arg, basestring): - r.append(is_testnet(arg)) - elif isinstance(arg, (list, tuple)): - return set_network(*arg) - if any(r) and not all(r): - raise Exception("Mixed Testnet/Mainnet queries") - return "testnet" if any(r) else "btc" - - -def parse_addr_args(*args): - # Valid input formats: unspent([addr1, addr2, addr3]) - # unspent([addr1, addr2, addr3], network) - # unspent(addr1, addr2, addr3) - # unspent(addr1, addr2, addr3, network) - addr_args = args - network = "btc" - if len(args) == 0: - return [], 'btc' - if len(args) >= 1 and args[-1] in ('testnet', 'btc'): - network = args[-1] - addr_args = args[:-1] - if len(addr_args) == 1 and isinstance(addr_args, list): - network = set_network(*addr_args[0]) - addr_args = addr_args[0] - if addr_args and isinstance(addr_args, tuple) and isinstance(addr_args[0], list): - addr_args = addr_args[0] - network = set_network(addr_args) - return network, addr_args - - -# Gets the unspent outputs of one or more addresses -def bci_unspent(*args): - network, addrs = parse_addr_args(*args) - u = [] - for a in addrs: - try: - data = make_request('https://blockchain.info/unspent?active='+a) - except Exception as e: - if str(e) == 'No free outputs to spend': - continue - else: - raise Exception(e) - try: - jsonobj = json.loads(data.decode("utf-8")) - for o in jsonobj["unspent_outputs"]: - h = o['tx_hash'].decode('hex')[::-1].encode('hex') - u.append({ - "output": h+':'+str(o['tx_output_n']), - "value": o['value'] - }) - except: - raise Exception("Failed to decode data: "+data) - return u - - -def blockr_unspent(*args): - # Valid input formats: blockr_unspent([addr1, addr2,addr3]) - # blockr_unspent(addr1, addr2, addr3) - # blockr_unspent([addr1, addr2, addr3], network) - # blockr_unspent(addr1, addr2, addr3, network) - # Where network is 'btc' or 'testnet' - network, addr_args = parse_addr_args(*args) - - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/address/unspent/' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/address/unspent/' - else: - raise Exception( - 'Unsupported network {0} for blockr_unspent'.format(network)) - - if len(addr_args) == 0: - return [] - elif isinstance(addr_args[0], list): - addrs = addr_args[0] - else: - addrs = addr_args - res = make_request(blockr_url+','.join(addrs)) - data = json.loads(res.decode("utf-8"))['data'] - o = [] - if 'unspent' in data: - data = [data] - for dat in data: - for u in dat['unspent']: - o.append({ - "output": u['tx']+':'+str(u['n']), - "value": int(u['amount'].replace('.', '')) - }) - return o - - -def helloblock_unspent(*args): - addrs, network = parse_addr_args(*args) - if network == 'testnet': - url = 'https://testnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s' - elif network == 'btc': - url = 'https://mainnet.helloblock.io/v1/addresses/%s/unspents?limit=500&offset=%s' - o = [] - for addr in addrs: - for offset in xrange(0, 10**9, 500): - res = make_request(url % (addr, offset)) - data = json.loads(res.decode("utf-8"))["data"] - if not len(data["unspents"]): - break - elif offset: - sys.stderr.write("Getting more unspents: %d\n" % offset) - for dat in data["unspents"]: - o.append({ - "output": dat["txHash"]+':'+str(dat["index"]), - "value": dat["value"], - }) - return o - - -unspent_getters = { - 'bci': bci_unspent, - 'blockr': blockr_unspent, - 'helloblock': helloblock_unspent -} - - -def unspent(*args, **kwargs): - f = unspent_getters.get(kwargs.get('source', ''), bci_unspent) - return f(*args) - - -# Gets the transaction output history of a given set of addresses, -# including whether or not they have been spent -def history(*args): - # Valid input formats: history([addr1, addr2,addr3]) - # history(addr1, addr2, addr3) - if len(args) == 0: - return [] - elif isinstance(args[0], list): - addrs = args[0] - else: - addrs = args - - txs = [] - for addr in addrs: - offset = 0 - while 1: - gathered = False - while not gathered: - try: - data = make_request( - 'https://blockchain.info/address/%s?format=json&offset=%s' % - (addr, offset)) - gathered = True - except Exception as e: - try: - sys.stderr.write(e.read().strip()) - except: - sys.stderr.write(str(e)) - gathered = False - try: - jsonobj = json.loads(data.decode("utf-8")) - except: - raise Exception("Failed to decode data: "+data) - txs.extend(jsonobj["txs"]) - if len(jsonobj["txs"]) < 50: - break - offset += 50 - sys.stderr.write("Fetching more transactions... "+str(offset)+'\n') - outs = {} - for tx in txs: - for o in tx["out"]: - if o.get('addr', None) in addrs: - key = str(tx["tx_index"])+':'+str(o["n"]) - outs[key] = { - "address": o["addr"], - "value": o["value"], - "output": tx["hash"]+':'+str(o["n"]), - "block_height": tx.get("block_height", None) - } - for tx in txs: - for i, inp in enumerate(tx["inputs"]): - if "prev_out" in inp: - if inp["prev_out"].get("addr", None) in addrs: - key = str(inp["prev_out"]["tx_index"]) + \ - ':'+str(inp["prev_out"]["n"]) - if outs.get(key): - outs[key]["spend"] = tx["hash"]+':'+str(i) - return [outs[k] for k in outs] - - -# Pushes a transaction to the network using https://blockchain.info/pushtx -def bci_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request('https://blockchain.info/pushtx', 'tx='+tx) - - -def eligius_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - s = make_request( - 'http://eligius.st/~wizkid057/newstats/pushtxn.php', - 'transaction='+tx+'&send=Push') - strings = re.findall('string[^"]*"[^"]*"', s) - for string in strings: - quote = re.findall('"[^"]*"', string)[0] - if len(quote) >= 5: - return quote[1:-1] - - -def blockr_pushtx(tx, network='btc'): - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/tx/push' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/tx/push' - else: - raise Exception( - 'Unsupported network {0} for blockr_pushtx'.format(network)) - - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request(blockr_url, '{"hex":"%s"}' % tx) - - -def helloblock_pushtx(tx): - if not re.match('^[0-9a-fA-F]*$', tx): - tx = tx.encode('hex') - return make_request('https://mainnet.helloblock.io/v1/transactions', - 'rawTxHex='+tx) - -pushtx_getters = { - 'bci': bci_pushtx, - 'blockr': blockr_pushtx, - 'helloblock': helloblock_pushtx -} - - -def pushtx(*args, **kwargs): - f = pushtx_getters.get(kwargs.get('source', ''), bci_pushtx) - return f(*args) - - -def last_block_height(network='btc'): - if network == 'testnet': - data = make_request('http://tbtc.blockr.io/api/v1/block/info/last') - jsonobj = json.loads(data.decode("utf-8")) - return jsonobj["data"]["nb"] - - data = make_request('https://blockchain.info/latestblock') - jsonobj = json.loads(data.decode("utf-8")) - return jsonobj["height"] - - -# Gets a specific transaction -def bci_fetchtx(txhash): - if isinstance(txhash, list): - return [bci_fetchtx(h) for h in txhash] - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - data = make_request('https://blockchain.info/rawtx/'+txhash+'?format=hex') - return data - - -def blockr_fetchtx(txhash, network='btc'): - if network == 'testnet': - blockr_url = 'http://tbtc.blockr.io/api/v1/tx/raw/' - elif network == 'btc': - blockr_url = 'http://btc.blockr.io/api/v1/tx/raw/' - else: - raise Exception( - 'Unsupported network {0} for blockr_fetchtx'.format(network)) - if isinstance(txhash, list): - txhash = ','.join([x.encode('hex') if not re.match('^[0-9a-fA-F]*$', x) - else x for x in txhash]) - jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8")) - return [d['tx']['hex'] for d in jsondata['data']] - else: - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - jsondata = json.loads(make_request(blockr_url+txhash).decode("utf-8")) - return jsondata['data']['tx']['hex'] - - -def helloblock_fetchtx(txhash, network='btc'): - if isinstance(txhash, list): - return [helloblock_fetchtx(h) for h in txhash] - if not re.match('^[0-9a-fA-F]*$', txhash): - txhash = txhash.encode('hex') - if network == 'testnet': - url = 'https://testnet.helloblock.io/v1/transactions/' - elif network == 'btc': - url = 'https://mainnet.helloblock.io/v1/transactions/' - else: - raise Exception( - 'Unsupported network {0} for helloblock_fetchtx'.format(network)) - data = json.loads(make_request(url + txhash).decode("utf-8"))["data"]["transaction"] - o = { - "locktime": data["locktime"], - "version": data["version"], - "ins": [], - "outs": [] - } - for inp in data["inputs"]: - o["ins"].append({ - "script": inp["scriptSig"], - "outpoint": { - "index": inp["prevTxoutIndex"], - "hash": inp["prevTxHash"], - }, - "sequence": 4294967295 - }) - for outp in data["outputs"]: - o["outs"].append({ - "value": outp["value"], - "script": outp["scriptPubKey"] - }) - from .transaction import serialize - from .transaction import txhash as TXHASH - tx = serialize(o) - assert TXHASH(tx) == txhash - return tx - - -fetchtx_getters = { - 'bci': bci_fetchtx, - 'blockr': blockr_fetchtx, - 'helloblock': helloblock_fetchtx -} - - -def fetchtx(*args, **kwargs): - f = fetchtx_getters.get(kwargs.get('source', ''), bci_fetchtx) - return f(*args) - - -def firstbits(address): - if len(address) >= 25: - return make_request('https://blockchain.info/q/getfirstbits/'+address) - else: - return make_request( - 'https://blockchain.info/q/resolvefirstbits/'+address) - - -def get_block_at_height(height): - j = json.loads(make_request("https://blockchain.info/block-height/" + - str(height)+"?format=json").decode("utf-8")) - for b in j['blocks']: - if b['main_chain'] is True: - return b - raise Exception("Block at this height not found") - - -def _get_block(inp): - if len(str(inp)) < 64: - return get_block_at_height(inp) - else: - return json.loads(make_request( - 'https://blockchain.info/rawblock/'+inp).decode("utf-8")) - - -def bci_get_block_header_data(inp): - j = _get_block(inp) - return { - 'version': j['ver'], - 'hash': j['hash'], - 'prevhash': j['prev_block'], - 'timestamp': j['time'], - 'merkle_root': j['mrkl_root'], - 'bits': j['bits'], - 'nonce': j['nonce'], - } - -def blockr_get_block_header_data(height, network='btc'): - if network == 'testnet': - blockr_url = "http://tbtc.blockr.io/api/v1/block/raw/" - elif network == 'btc': - blockr_url = "http://btc.blockr.io/api/v1/block/raw/" - else: - raise Exception( - 'Unsupported network {0} for blockr_get_block_header_data'.format(network)) - - k = json.loads(make_request(blockr_url + str(height)).decode("utf-8")) - j = k['data'] - return { - 'version': j['version'], - 'hash': j['hash'], - 'prevhash': j['previousblockhash'], - 'timestamp': j['time'], - 'merkle_root': j['merkleroot'], - 'bits': int(j['bits'], 16), - 'nonce': j['nonce'], - } - - -def get_block_timestamp(height, network='btc'): - if network == 'testnet': - blockr_url = "http://tbtc.blockr.io/api/v1/block/info/" - elif network == 'btc': - blockr_url = "http://btc.blockr.io/api/v1/block/info/" - else: - raise Exception( - 'Unsupported network {0} for get_block_timestamp'.format(network)) - - import time, calendar - if isinstance(height, list): - k = json.loads(make_request(blockr_url + ','.join([str(x) for x in height])).decode("utf-8")) - o = {x['nb']: calendar.timegm(time.strptime(x['time_utc'], - "%Y-%m-%dT%H:%M:%SZ")) for x in k['data']} - return [o[x] for x in height] - else: - k = json.loads(make_request(blockr_url + str(height)).decode("utf-8")) - j = k['data']['time_utc'] - return calendar.timegm(time.strptime(j, "%Y-%m-%dT%H:%M:%SZ")) - - -block_header_data_getters = { - 'bci': bci_get_block_header_data, - 'blockr': blockr_get_block_header_data -} - - -def get_block_header_data(inp, **kwargs): - f = block_header_data_getters.get(kwargs.get('source', ''), - bci_get_block_header_data) - return f(inp, **kwargs) - - -def get_txs_in_block(inp): - j = _get_block(inp) - hashes = [t['hash'] for t in j['tx']] - return hashes - - -def get_block_height(txhash): - j = json.loads(make_request('https://blockchain.info/rawtx/'+txhash).decode("utf-8")) - return j['block_height'] - -# fromAddr, toAddr, 12345, changeAddress -def get_tx_composite(inputs, outputs, output_value, change_address=None, network=None): - """mktx using blockcypher API""" - inputs = [inputs] if not isinstance(inputs, list) else inputs - outputs = [outputs] if not isinstance(outputs, list) else outputs - network = set_network(change_address or inputs) if not network else network.lower() - url = "http://api.blockcypher.com/v1/btc/{network}/txs/new?includeToSignTx=true".format( - network=('test3' if network=='testnet' else 'main')) - is_address = lambda a: bool(re.match("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$", a)) - if any([is_address(x) for x in inputs]): - inputs_type = 'addresses' # also accepts UTXOs, only addresses supported presently - if any([is_address(x) for x in outputs]): - outputs_type = 'addresses' # TODO: add UTXO support - data = { - 'inputs': [{inputs_type: inputs}], - 'confirmations': 0, - 'preference': 'high', - 'outputs': [{outputs_type: outputs, "value": output_value}] - } - if change_address: - data["change_address"] = change_address # - jdata = json.loads(make_request(url, data)) - hash, txh = jdata.get("tosign")[0], jdata.get("tosign_tx")[0] - assert bin_dbl_sha256(txh.decode('hex')).encode('hex') == hash, "checksum mismatch %s" % hash - return txh.encode("utf-8") - -blockcypher_mktx = get_tx_composite diff --git a/src/lib/pybitcointools/blocks.py b/src/lib/pybitcointools/blocks.py deleted file mode 100644 index 9df6b35c..00000000 --- a/src/lib/pybitcointools/blocks.py +++ /dev/null @@ -1,50 +0,0 @@ -from .main import * - - -def serialize_header(inp): - o = encode(inp['version'], 256, 4)[::-1] + \ - inp['prevhash'].decode('hex')[::-1] + \ - inp['merkle_root'].decode('hex')[::-1] + \ - encode(inp['timestamp'], 256, 4)[::-1] + \ - encode(inp['bits'], 256, 4)[::-1] + \ - encode(inp['nonce'], 256, 4)[::-1] - h = bin_sha256(bin_sha256(o))[::-1].encode('hex') - assert h == inp['hash'], (sha256(o), inp['hash']) - return o.encode('hex') - - -def deserialize_header(inp): - inp = inp.decode('hex') - return { - "version": decode(inp[:4][::-1], 256), - "prevhash": inp[4:36][::-1].encode('hex'), - "merkle_root": inp[36:68][::-1].encode('hex'), - "timestamp": decode(inp[68:72][::-1], 256), - "bits": decode(inp[72:76][::-1], 256), - "nonce": decode(inp[76:80][::-1], 256), - "hash": bin_sha256(bin_sha256(inp))[::-1].encode('hex') - } - - -def mk_merkle_proof(header, hashes, index): - nodes = [h.decode('hex')[::-1] for h in hashes] - if len(nodes) % 2 and len(nodes) > 2: - nodes.append(nodes[-1]) - layers = [nodes] - while len(nodes) > 1: - newnodes = [] - for i in range(0, len(nodes) - 1, 2): - newnodes.append(bin_sha256(bin_sha256(nodes[i] + nodes[i+1]))) - if len(newnodes) % 2 and len(newnodes) > 2: - newnodes.append(newnodes[-1]) - nodes = newnodes - layers.append(nodes) - # Sanity check, make sure merkle root is valid - assert nodes[0][::-1].encode('hex') == header['merkle_root'] - merkle_siblings = \ - [layers[i][(index >> i) ^ 1] for i in range(len(layers)-1)] - return { - "hash": hashes[index], - "siblings": [x[::-1].encode('hex') for x in merkle_siblings], - "header": header - } diff --git a/src/lib/pybitcointools/composite.py b/src/lib/pybitcointools/composite.py deleted file mode 100644 index e5d50492..00000000 --- a/src/lib/pybitcointools/composite.py +++ /dev/null @@ -1,128 +0,0 @@ -from .main import * -from .transaction import * -from .bci import * -from .deterministic import * -from .blocks import * - - -# Takes privkey, address, value (satoshis), fee (satoshis) -def send(frm, to, value, fee=10000, **kwargs): - return sendmultitx(frm, to + ":" + str(value), fee, **kwargs) - - -# Takes privkey, "address1:value1,address2:value2" (satoshis), fee (satoshis) -def sendmultitx(frm, *args, **kwargs): - tv, fee = args[:-1], int(args[-1]) - outs = [] - outvalue = 0 - for a in tv: - outs.append(a) - outvalue += int(a.split(":")[1]) - - u = unspent(privtoaddr(frm), **kwargs) - u2 = select(u, int(outvalue)+int(fee)) - argz = u2 + outs + [privtoaddr(frm), fee] - tx = mksend(*argz) - tx2 = signall(tx, frm) - return pushtx(tx2, **kwargs) - - -# Takes address, address, value (satoshis), fee(satoshis) -def preparetx(frm, to, value, fee=10000, **kwargs): - tovalues = to + ":" + str(value) - return preparemultitx(frm, tovalues, fee, **kwargs) - - -# Takes address, address:value, address:value ... (satoshis), fee(satoshis) -def preparemultitx(frm, *args, **kwargs): - tv, fee = args[:-1], int(args[-1]) - outs = [] - outvalue = 0 - for a in tv: - outs.append(a) - outvalue += int(a.split(":")[1]) - - u = unspent(frm, **kwargs) - u2 = select(u, int(outvalue)+int(fee)) - argz = u2 + outs + [frm, fee] - return mksend(*argz) - - -# BIP32 hierarchical deterministic multisig script -def bip32_hdm_script(*args): - if len(args) == 3: - keys, req, path = args - else: - i, keys, path = 0, [], [] - while len(args[i]) > 40: - keys.append(args[i]) - i += 1 - req = int(args[i]) - path = map(int, args[i+1:]) - pubs = sorted(map(lambda x: bip32_descend(x, path), keys)) - return mk_multisig_script(pubs, req) - - -# BIP32 hierarchical deterministic multisig address -def bip32_hdm_addr(*args): - return scriptaddr(bip32_hdm_script(*args)) - - -# Setup a coinvault transaction -def setup_coinvault_tx(tx, script): - txobj = deserialize(tx) - N = deserialize_script(script)[-2] - for inp in txobj["ins"]: - inp["script"] = serialize_script([None] * (N+1) + [script]) - return serialize(txobj) - - -# Sign a coinvault transaction -def sign_coinvault_tx(tx, priv): - pub = privtopub(priv) - txobj = deserialize(tx) - subscript = deserialize_script(txobj['ins'][0]['script']) - oscript = deserialize_script(subscript[-1]) - k, pubs = oscript[0], oscript[1:-2] - for j in range(len(txobj['ins'])): - scr = deserialize_script(txobj['ins'][j]['script']) - for i, p in enumerate(pubs): - if p == pub: - scr[i+1] = multisign(tx, j, subscript[-1], priv) - if len(filter(lambda x: x, scr[1:-1])) >= k: - scr = [None] + filter(lambda x: x, scr[1:-1])[:k] + [scr[-1]] - txobj['ins'][j]['script'] = serialize_script(scr) - return serialize(txobj) - - -# Inspects a transaction -def inspect(tx, **kwargs): - d = deserialize(tx) - isum = 0 - ins = {} - for _in in d['ins']: - h = _in['outpoint']['hash'] - i = _in['outpoint']['index'] - prevout = deserialize(fetchtx(h, **kwargs))['outs'][i] - isum += prevout['value'] - a = script_to_address(prevout['script']) - ins[a] = ins.get(a, 0) + prevout['value'] - outs = [] - osum = 0 - for _out in d['outs']: - outs.append({'address': script_to_address(_out['script']), - 'value': _out['value']}) - osum += _out['value'] - return { - 'fee': isum - osum, - 'outs': outs, - 'ins': ins - } - - -def merkle_prove(txhash): - blocknum = str(get_block_height(txhash)) - header = get_block_header_data(blocknum) - hashes = get_txs_in_block(blocknum) - i = hashes.index(txhash) - return mk_merkle_proof(header, hashes, i) diff --git a/src/lib/pybitcointools/deterministic.py b/src/lib/pybitcointools/deterministic.py deleted file mode 100644 index b2bdbbc6..00000000 --- a/src/lib/pybitcointools/deterministic.py +++ /dev/null @@ -1,199 +0,0 @@ -from .main import * -import hmac -import hashlib -from binascii import hexlify -# Electrum wallets - - -def electrum_stretch(seed): - return slowsha(seed) - -# Accepts seed or stretched seed, returns master public key - - -def electrum_mpk(seed): - if len(seed) == 32: - seed = electrum_stretch(seed) - return privkey_to_pubkey(seed)[2:] - -# Accepts (seed or stretched seed), index and secondary index -# (conventionally 0 for ordinary addresses, 1 for change) , returns privkey - - -def electrum_privkey(seed, n, for_change=0): - if len(seed) == 32: - seed = electrum_stretch(seed) - mpk = electrum_mpk(seed) - offset = dbl_sha256(from_int_representation_to_bytes(n)+b':'+from_int_representation_to_bytes(for_change)+b':'+binascii.unhexlify(mpk)) - return add_privkeys(seed, offset) - -# Accepts (seed or stretched seed or master pubkey), index and secondary index -# (conventionally 0 for ordinary addresses, 1 for change) , returns pubkey - - -def electrum_pubkey(masterkey, n, for_change=0): - if len(masterkey) == 32: - mpk = electrum_mpk(electrum_stretch(masterkey)) - elif len(masterkey) == 64: - mpk = electrum_mpk(masterkey) - else: - mpk = masterkey - bin_mpk = encode_pubkey(mpk, 'bin_electrum') - offset = bin_dbl_sha256(from_int_representation_to_bytes(n)+b':'+from_int_representation_to_bytes(for_change)+b':'+bin_mpk) - return add_pubkeys('04'+mpk, privtopub(offset)) - -# seed/stretched seed/pubkey -> address (convenience method) - - -def electrum_address(masterkey, n, for_change=0, version=0): - return pubkey_to_address(electrum_pubkey(masterkey, n, for_change), version) - -# Given a master public key, a private key from that wallet and its index, -# cracks the secret exponent which can be used to generate all other private -# keys in the wallet - - -def crack_electrum_wallet(mpk, pk, n, for_change=0): - bin_mpk = encode_pubkey(mpk, 'bin_electrum') - offset = dbl_sha256(str(n)+':'+str(for_change)+':'+bin_mpk) - return subtract_privkeys(pk, offset) - -# Below code ASSUMES binary inputs and compressed pubkeys -MAINNET_PRIVATE = b'\x04\x88\xAD\xE4' -MAINNET_PUBLIC = b'\x04\x88\xB2\x1E' -TESTNET_PRIVATE = b'\x04\x35\x83\x94' -TESTNET_PUBLIC = b'\x04\x35\x87\xCF' -PRIVATE = [MAINNET_PRIVATE, TESTNET_PRIVATE] -PUBLIC = [MAINNET_PUBLIC, TESTNET_PUBLIC] - -# BIP32 child key derivation - - -def raw_bip32_ckd(rawtuple, i): - vbytes, depth, fingerprint, oldi, chaincode, key = rawtuple - i = int(i) - - if vbytes in PRIVATE: - priv = key - pub = privtopub(key) - else: - pub = key - - if i >= 2**31: - if vbytes in PUBLIC: - raise Exception("Can't do private derivation on public key!") - I = hmac.new(chaincode, b'\x00'+priv[:32]+encode(i, 256, 4), hashlib.sha512).digest() - else: - I = hmac.new(chaincode, pub+encode(i, 256, 4), hashlib.sha512).digest() - - if vbytes in PRIVATE: - newkey = add_privkeys(I[:32]+B'\x01', priv) - fingerprint = bin_hash160(privtopub(key))[:4] - if vbytes in PUBLIC: - newkey = add_pubkeys(compress(privtopub(I[:32])), key) - fingerprint = bin_hash160(key)[:4] - - return (vbytes, depth + 1, fingerprint, i, I[32:], newkey) - - -def bip32_serialize(rawtuple): - vbytes, depth, fingerprint, i, chaincode, key = rawtuple - i = encode(i, 256, 4) - chaincode = encode(hash_to_int(chaincode), 256, 32) - keydata = b'\x00'+key[:-1] if vbytes in PRIVATE else key - bindata = vbytes + from_int_to_byte(depth % 256) + fingerprint + i + chaincode + keydata - return changebase(bindata+bin_dbl_sha256(bindata)[:4], 256, 58) - - -def bip32_deserialize(data): - dbin = changebase(data, 58, 256) - if bin_dbl_sha256(dbin[:-4])[:4] != dbin[-4:]: - raise Exception("Invalid checksum") - vbytes = dbin[0:4] - depth = from_byte_to_int(dbin[4]) - fingerprint = dbin[5:9] - i = decode(dbin[9:13], 256) - chaincode = dbin[13:45] - key = dbin[46:78]+b'\x01' if vbytes in PRIVATE else dbin[45:78] - return (vbytes, depth, fingerprint, i, chaincode, key) - - -def raw_bip32_privtopub(rawtuple): - vbytes, depth, fingerprint, i, chaincode, key = rawtuple - newvbytes = MAINNET_PUBLIC if vbytes == MAINNET_PRIVATE else TESTNET_PUBLIC - return (newvbytes, depth, fingerprint, i, chaincode, privtopub(key)) - - -def bip32_privtopub(data): - return bip32_serialize(raw_bip32_privtopub(bip32_deserialize(data))) - - -def bip32_ckd(data, i): - return bip32_serialize(raw_bip32_ckd(bip32_deserialize(data), i)) - - -def bip32_master_key(seed, vbytes=MAINNET_PRIVATE): - I = hmac.new(from_string_to_bytes("Bitcoin seed"), seed, hashlib.sha512).digest() - return bip32_serialize((vbytes, 0, b'\x00'*4, 0, I[32:], I[:32]+b'\x01')) - - -def bip32_bin_extract_key(data): - return bip32_deserialize(data)[-1] - - -def bip32_extract_key(data): - return safe_hexlify(bip32_deserialize(data)[-1]) - -# Exploits the same vulnerability as above in Electrum wallets -# Takes a BIP32 pubkey and one of the child privkeys of its corresponding -# privkey and returns the BIP32 privkey associated with that pubkey - - -def raw_crack_bip32_privkey(parent_pub, priv): - vbytes, depth, fingerprint, i, chaincode, key = priv - pvbytes, pdepth, pfingerprint, pi, pchaincode, pkey = parent_pub - i = int(i) - - if i >= 2**31: - raise Exception("Can't crack private derivation!") - - I = hmac.new(pchaincode, pkey+encode(i, 256, 4), hashlib.sha512).digest() - - pprivkey = subtract_privkeys(key, I[:32]+b'\x01') - - newvbytes = MAINNET_PRIVATE if vbytes == MAINNET_PUBLIC else TESTNET_PRIVATE - return (newvbytes, pdepth, pfingerprint, pi, pchaincode, pprivkey) - - -def crack_bip32_privkey(parent_pub, priv): - dsppub = bip32_deserialize(parent_pub) - dspriv = bip32_deserialize(priv) - return bip32_serialize(raw_crack_bip32_privkey(dsppub, dspriv)) - - -def coinvault_pub_to_bip32(*args): - if len(args) == 1: - args = args[0].split(' ') - vals = map(int, args[34:]) - I1 = ''.join(map(chr, vals[:33])) - I2 = ''.join(map(chr, vals[35:67])) - return bip32_serialize((MAINNET_PUBLIC, 0, b'\x00'*4, 0, I2, I1)) - - -def coinvault_priv_to_bip32(*args): - if len(args) == 1: - args = args[0].split(' ') - vals = map(int, args[34:]) - I2 = ''.join(map(chr, vals[35:67])) - I3 = ''.join(map(chr, vals[72:104])) - return bip32_serialize((MAINNET_PRIVATE, 0, b'\x00'*4, 0, I2, I3+b'\x01')) - - -def bip32_descend(*args): - if len(args) == 2 and isinstance(args[1], list): - key, path = args - else: - key, path = args[0], map(int, args[1:]) - for p in path: - key = bip32_ckd(key, p) - return bip32_extract_key(key) diff --git a/src/lib/pybitcointools/english.txt b/src/lib/pybitcointools/english.txt deleted file mode 100644 index 942040ed..00000000 --- a/src/lib/pybitcointools/english.txt +++ /dev/null @@ -1,2048 +0,0 @@ -abandon -ability -able -about -above -absent -absorb -abstract -absurd -abuse -access -accident -account -accuse -achieve -acid -acoustic -acquire -across -act -action -actor -actress -actual -adapt -add -addict -address -adjust -admit -adult -advance -advice -aerobic -affair -afford -afraid -again -age -agent -agree -ahead -aim -air -airport -aisle -alarm -album -alcohol -alert -alien -all -alley -allow -almost -alone -alpha -already -also -alter -always -amateur -amazing -among -amount -amused -analyst -anchor -ancient -anger -angle -angry -animal -ankle -announce -annual -another -answer -antenna -antique -anxiety -any -apart -apology -appear -apple -approve -april -arch -arctic -area -arena -argue -arm -armed -armor -army -around -arrange -arrest -arrive -arrow -art -artefact -artist -artwork -ask -aspect -assault -asset -assist -assume -asthma -athlete -atom -attack -attend -attitude -attract -auction -audit -august -aunt -author -auto -autumn -average -avocado -avoid -awake -aware -away -awesome -awful -awkward -axis -baby -bachelor -bacon -badge -bag -balance -balcony -ball -bamboo -banana -banner -bar -barely -bargain -barrel -base -basic -basket -battle -beach -bean -beauty -because -become -beef -before -begin -behave -behind -believe -below -belt -bench -benefit -best -betray -better -between -beyond -bicycle -bid -bike -bind -biology -bird -birth -bitter -black -blade -blame -blanket -blast -bleak -bless -blind -blood -blossom -blouse -blue -blur -blush -board -boat -body -boil -bomb -bone -bonus -book -boost -border -boring -borrow -boss -bottom -bounce -box -boy -bracket -brain -brand -brass -brave -bread -breeze -brick -bridge -brief -bright -bring -brisk -broccoli -broken -bronze -broom -brother -brown -brush -bubble -buddy -budget -buffalo -build -bulb -bulk -bullet -bundle -bunker -burden -burger -burst -bus -business -busy -butter -buyer -buzz -cabbage -cabin -cable -cactus -cage -cake -call -calm -camera -camp -can -canal -cancel -candy -cannon -canoe -canvas -canyon -capable -capital -captain -car -carbon -card -cargo -carpet -carry -cart -case -cash -casino -castle -casual -cat -catalog -catch -category -cattle -caught -cause -caution -cave -ceiling -celery -cement -census -century -cereal -certain -chair -chalk -champion -change -chaos -chapter -charge -chase -chat -cheap -check -cheese -chef -cherry -chest -chicken -chief -child -chimney -choice -choose -chronic -chuckle -chunk -churn -cigar -cinnamon -circle -citizen -city -civil -claim -clap -clarify -claw -clay -clean -clerk -clever -click -client -cliff -climb -clinic -clip -clock -clog -close -cloth -cloud -clown -club -clump -cluster -clutch -coach -coast -coconut -code -coffee -coil -coin -collect -color -column -combine -come -comfort -comic -common -company -concert -conduct -confirm -congress -connect -consider -control -convince -cook -cool -copper -copy -coral -core -corn -correct -cost -cotton -couch -country -couple -course -cousin -cover -coyote -crack -cradle -craft -cram -crane -crash -crater -crawl -crazy -cream -credit -creek -crew -cricket -crime -crisp -critic -crop -cross -crouch -crowd -crucial -cruel -cruise -crumble -crunch -crush -cry -crystal -cube -culture -cup -cupboard -curious -current -curtain -curve -cushion -custom -cute -cycle -dad -damage -damp -dance -danger -daring -dash -daughter -dawn -day -deal -debate -debris -decade -december -decide -decline -decorate -decrease -deer -defense -define -defy -degree -delay -deliver -demand -demise -denial -dentist -deny -depart -depend -deposit -depth -deputy -derive -describe -desert -design -desk -despair -destroy -detail -detect -develop -device -devote -diagram -dial -diamond -diary -dice -diesel -diet -differ -digital -dignity -dilemma -dinner -dinosaur -direct -dirt -disagree -discover -disease -dish -dismiss -disorder -display -distance -divert -divide -divorce -dizzy -doctor -document -dog -doll -dolphin -domain -donate -donkey -donor -door -dose -double -dove -draft -dragon -drama -drastic -draw -dream -dress -drift -drill -drink -drip -drive -drop -drum -dry -duck -dumb -dune -during -dust -dutch -duty -dwarf -dynamic -eager -eagle -early -earn -earth -easily -east -easy -echo -ecology -economy -edge -edit -educate -effort -egg -eight -either -elbow -elder -electric -elegant -element -elephant -elevator -elite -else -embark -embody -embrace -emerge -emotion -employ -empower -empty -enable -enact -end -endless -endorse -enemy -energy -enforce -engage -engine -enhance -enjoy -enlist -enough -enrich -enroll -ensure -enter -entire -entry -envelope -episode -equal -equip -era -erase -erode -erosion -error -erupt -escape -essay -essence -estate -eternal -ethics -evidence -evil -evoke -evolve -exact -example -excess -exchange -excite -exclude -excuse -execute -exercise -exhaust -exhibit -exile -exist -exit -exotic -expand -expect -expire -explain -expose -express -extend -extra -eye -eyebrow -fabric -face -faculty -fade -faint -faith -fall -false -fame -family -famous -fan -fancy -fantasy -farm -fashion -fat -fatal -father -fatigue -fault -favorite -feature -february -federal -fee -feed -feel -female -fence -festival -fetch -fever -few -fiber -fiction -field -figure -file -film -filter -final -find -fine -finger -finish -fire -firm -first -fiscal -fish -fit -fitness -fix -flag -flame -flash -flat -flavor -flee -flight -flip -float -flock -floor -flower -fluid -flush -fly -foam -focus -fog -foil -fold -follow -food -foot -force -forest -forget -fork -fortune -forum -forward -fossil -foster -found -fox -fragile -frame -frequent -fresh -friend -fringe -frog -front -frost -frown -frozen -fruit -fuel -fun -funny -furnace -fury -future -gadget -gain -galaxy -gallery -game -gap -garage -garbage -garden -garlic -garment -gas -gasp -gate -gather -gauge -gaze -general -genius -genre -gentle -genuine -gesture -ghost -giant -gift -giggle -ginger -giraffe -girl -give -glad -glance -glare -glass -glide -glimpse -globe -gloom -glory -glove -glow -glue -goat -goddess -gold -good -goose -gorilla -gospel -gossip -govern -gown -grab -grace -grain -grant -grape -grass -gravity -great -green -grid -grief -grit -grocery -group -grow -grunt -guard -guess -guide -guilt -guitar -gun -gym -habit -hair -half -hammer -hamster -hand -happy -harbor -hard -harsh -harvest -hat -have -hawk -hazard -head -health -heart -heavy -hedgehog -height -hello -helmet -help -hen -hero -hidden -high -hill -hint -hip -hire -history -hobby -hockey -hold -hole -holiday -hollow -home -honey -hood -hope -horn -horror -horse -hospital -host -hotel -hour -hover -hub -huge -human -humble -humor -hundred -hungry -hunt -hurdle -hurry -hurt -husband -hybrid -ice -icon -idea -identify -idle -ignore -ill -illegal -illness -image -imitate -immense -immune -impact -impose -improve -impulse -inch -include -income -increase -index -indicate -indoor -industry -infant -inflict -inform -inhale -inherit -initial -inject -injury -inmate -inner -innocent -input -inquiry -insane -insect -inside -inspire -install -intact -interest -into -invest -invite -involve -iron -island -isolate -issue -item -ivory -jacket -jaguar -jar -jazz -jealous -jeans -jelly -jewel -job -join -joke -journey -joy -judge -juice -jump -jungle -junior -junk -just -kangaroo -keen -keep -ketchup -key -kick -kid -kidney -kind -kingdom -kiss -kit -kitchen -kite -kitten -kiwi -knee -knife -knock -know -lab -label -labor -ladder -lady -lake -lamp -language -laptop -large -later -latin -laugh -laundry -lava -law -lawn -lawsuit -layer -lazy -leader -leaf -learn -leave -lecture -left -leg -legal -legend -leisure -lemon -lend -length -lens -leopard -lesson -letter -level -liar -liberty -library -license -life -lift -light -like -limb -limit -link -lion -liquid -list -little -live -lizard -load -loan -lobster -local -lock -logic -lonely -long -loop -lottery -loud -lounge -love -loyal -lucky -luggage -lumber -lunar -lunch -luxury -lyrics -machine -mad -magic -magnet -maid -mail -main -major -make -mammal -man -manage -mandate -mango -mansion -manual -maple -marble -march -margin -marine -market -marriage -mask -mass -master -match -material -math -matrix -matter -maximum -maze -meadow -mean -measure -meat -mechanic -medal -media -melody -melt -member -memory -mention -menu -mercy -merge -merit -merry -mesh -message -metal -method -middle -midnight -milk -million -mimic -mind -minimum -minor -minute -miracle -mirror -misery -miss -mistake -mix -mixed -mixture -mobile -model -modify -mom -moment -monitor -monkey -monster -month -moon -moral -more -morning -mosquito -mother -motion -motor -mountain -mouse -move -movie -much -muffin -mule -multiply -muscle -museum -mushroom -music -must -mutual -myself -mystery -myth -naive -name -napkin -narrow -nasty -nation -nature -near -neck -need -negative -neglect -neither -nephew -nerve -nest -net -network -neutral -never -news -next -nice -night -noble -noise -nominee -noodle -normal -north -nose -notable -note -nothing -notice -novel -now -nuclear -number -nurse -nut -oak -obey -object -oblige -obscure -observe -obtain -obvious -occur -ocean -october -odor -off -offer -office -often -oil -okay -old -olive -olympic -omit -once -one -onion -online -only -open -opera -opinion -oppose -option -orange -orbit -orchard -order -ordinary -organ -orient -original -orphan -ostrich -other -outdoor -outer -output -outside -oval -oven -over -own -owner -oxygen -oyster -ozone -pact -paddle -page -pair -palace -palm -panda -panel -panic -panther -paper -parade -parent -park -parrot -party -pass -patch -path -patient -patrol -pattern -pause -pave -payment -peace -peanut -pear -peasant -pelican -pen -penalty -pencil -people -pepper -perfect -permit -person -pet -phone -photo -phrase -physical -piano -picnic -picture -piece -pig -pigeon -pill -pilot -pink -pioneer -pipe -pistol -pitch -pizza -place -planet -plastic -plate -play -please -pledge -pluck -plug -plunge -poem -poet -point -polar -pole -police -pond -pony -pool -popular -portion -position -possible -post -potato -pottery -poverty -powder -power -practice -praise -predict -prefer -prepare -present -pretty -prevent -price -pride -primary -print -priority -prison -private -prize -problem -process -produce -profit -program -project -promote -proof -property -prosper -protect -proud -provide -public -pudding -pull -pulp -pulse -pumpkin -punch -pupil -puppy -purchase -purity -purpose -purse -push -put -puzzle -pyramid -quality -quantum -quarter -question -quick -quit -quiz -quote -rabbit -raccoon -race -rack -radar -radio -rail -rain -raise -rally -ramp -ranch -random -range -rapid -rare -rate -rather -raven -raw -razor -ready -real -reason -rebel -rebuild -recall -receive -recipe -record -recycle -reduce -reflect -reform -refuse -region -regret -regular -reject -relax -release -relief -rely -remain -remember -remind -remove -render -renew -rent -reopen -repair -repeat -replace -report -require -rescue -resemble -resist -resource -response -result -retire -retreat -return -reunion -reveal -review -reward -rhythm -rib -ribbon -rice -rich -ride -ridge -rifle -right -rigid -ring -riot -ripple -risk -ritual -rival -river -road -roast -robot -robust -rocket -romance -roof -rookie -room -rose -rotate -rough -round -route -royal -rubber -rude -rug -rule -run -runway -rural -sad -saddle -sadness -safe -sail -salad -salmon -salon -salt -salute -same -sample -sand -satisfy -satoshi -sauce -sausage -save -say -scale -scan -scare -scatter -scene -scheme -school -science -scissors -scorpion -scout -scrap -screen -script -scrub -sea -search -season -seat -second -secret -section -security -seed -seek -segment -select -sell -seminar -senior -sense -sentence -series -service -session -settle -setup -seven -shadow -shaft -shallow -share -shed -shell -sheriff -shield -shift -shine -ship -shiver -shock -shoe -shoot -shop -short -shoulder -shove -shrimp -shrug -shuffle -shy -sibling -sick -side -siege -sight -sign -silent -silk -silly -silver -similar -simple -since -sing -siren -sister -situate -six -size -skate -sketch -ski -skill -skin -skirt -skull -slab -slam -sleep -slender -slice -slide -slight -slim -slogan -slot -slow -slush -small -smart -smile -smoke -smooth -snack -snake -snap -sniff -snow -soap -soccer -social -sock -soda -soft -solar -soldier -solid -solution -solve -someone -song -soon -sorry -sort -soul -sound -soup -source -south -space -spare -spatial -spawn -speak -special -speed -spell -spend -sphere -spice -spider -spike -spin -spirit -split -spoil -sponsor -spoon -sport -spot -spray -spread -spring -spy -square -squeeze -squirrel -stable -stadium -staff -stage -stairs -stamp -stand -start -state -stay -steak -steel -stem -step -stereo -stick -still -sting -stock -stomach -stone -stool -story -stove -strategy -street -strike -strong -struggle -student -stuff -stumble -style -subject -submit -subway -success -such -sudden -suffer -sugar -suggest -suit -summer -sun -sunny -sunset -super -supply -supreme -sure -surface -surge -surprise -surround -survey -suspect -sustain -swallow -swamp -swap -swarm -swear -sweet -swift -swim -swing -switch -sword -symbol -symptom -syrup -system -table -tackle -tag -tail -talent -talk -tank -tape -target -task -taste -tattoo -taxi -teach -team -tell -ten -tenant -tennis -tent -term -test -text -thank -that -theme -then -theory -there -they -thing -this -thought -three -thrive -throw -thumb -thunder -ticket -tide -tiger -tilt -timber -time -tiny -tip -tired -tissue -title -toast -tobacco -today -toddler -toe -together -toilet -token -tomato -tomorrow -tone -tongue -tonight -tool -tooth -top -topic -topple -torch -tornado -tortoise -toss -total -tourist -toward -tower -town -toy -track -trade -traffic -tragic -train -transfer -trap -trash -travel -tray -treat -tree -trend -trial -tribe -trick -trigger -trim -trip -trophy -trouble -truck -true -truly -trumpet -trust -truth -try -tube -tuition -tumble -tuna -tunnel -turkey -turn -turtle -twelve -twenty -twice -twin -twist -two -type -typical -ugly -umbrella -unable -unaware -uncle -uncover -under -undo -unfair -unfold -unhappy -uniform -unique -unit -universe -unknown -unlock -until -unusual -unveil -update -upgrade -uphold -upon -upper -upset -urban -urge -usage -use -used -useful -useless -usual -utility -vacant -vacuum -vague -valid -valley -valve -van -vanish -vapor -various -vast -vault -vehicle -velvet -vendor -venture -venue -verb -verify -version -very -vessel -veteran -viable -vibrant -vicious -victory -video -view -village -vintage -violin -virtual -virus -visa -visit -visual -vital -vivid -vocal -voice -void -volcano -volume -vote -voyage -wage -wagon -wait -walk -wall -walnut -want -warfare -warm -warrior -wash -wasp -waste -water -wave -way -wealth -weapon -wear -weasel -weather -web -wedding -weekend -weird -welcome -west -wet -whale -what -wheat -wheel -when -where -whip -whisper -wide -width -wife -wild -will -win -window -wine -wing -wink -winner -winter -wire -wisdom -wise -wish -witness -wolf -woman -wonder -wood -wool -word -work -world -worry -worth -wrap -wreck -wrestle -wrist -write -wrong -yard -year -yellow -you -young -youth -zebra -zero -zone -zoo diff --git a/src/lib/pybitcointools/main.py b/src/lib/pybitcointools/main.py deleted file mode 100644 index 8cf3a9f7..00000000 --- a/src/lib/pybitcointools/main.py +++ /dev/null @@ -1,581 +0,0 @@ -#!/usr/bin/python -from .py2specials import * -from .py3specials import * -import binascii -import hashlib -import re -import sys -import os -import base64 -import time -import random -import hmac -from .ripemd import * - -# Elliptic curve parameters (secp256k1) - -P = 2**256 - 2**32 - 977 -N = 115792089237316195423570985008687907852837564279074904382605163141518161494337 -A = 0 -B = 7 -Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 -Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 -G = (Gx, Gy) - - -def change_curve(p, n, a, b, gx, gy): - global P, N, A, B, Gx, Gy, G - P, N, A, B, Gx, Gy = p, n, a, b, gx, gy - G = (Gx, Gy) - - -def getG(): - return G - -# Extended Euclidean Algorithm - - -def inv(a, n): - if a == 0: - return 0 - lm, hm = 1, 0 - low, high = a % n, n - while low > 1: - r = high//low - nm, new = hm-lm*r, high-low*r - lm, low, hm, high = nm, new, lm, low - return lm % n - - - -# JSON access (for pybtctool convenience) - - -def access(obj, prop): - if isinstance(obj, dict): - if prop in obj: - return obj[prop] - elif '.' in prop: - return obj[float(prop)] - else: - return obj[int(prop)] - else: - return obj[int(prop)] - - -def multiaccess(obj, prop): - return [access(o, prop) for o in obj] - - -def slice(obj, start=0, end=2**200): - return obj[int(start):int(end)] - - -def count(obj): - return len(obj) - -_sum = sum - - -def sum(obj): - return _sum(obj) - - -def isinf(p): - return p[0] == 0 and p[1] == 0 - - -def to_jacobian(p): - o = (p[0], p[1], 1) - return o - - -def jacobian_double(p): - if not p[1]: - return (0, 0, 0) - ysq = (p[1] ** 2) % P - S = (4 * p[0] * ysq) % P - M = (3 * p[0] ** 2 + A * p[2] ** 4) % P - nx = (M**2 - 2 * S) % P - ny = (M * (S - nx) - 8 * ysq ** 2) % P - nz = (2 * p[1] * p[2]) % P - return (nx, ny, nz) - - -def jacobian_add(p, q): - if not p[1]: - return q - if not q[1]: - return p - U1 = (p[0] * q[2] ** 2) % P - U2 = (q[0] * p[2] ** 2) % P - S1 = (p[1] * q[2] ** 3) % P - S2 = (q[1] * p[2] ** 3) % P - if U1 == U2: - if S1 != S2: - return (0, 0, 1) - return jacobian_double(p) - H = U2 - U1 - R = S2 - S1 - H2 = (H * H) % P - H3 = (H * H2) % P - U1H2 = (U1 * H2) % P - nx = (R ** 2 - H3 - 2 * U1H2) % P - ny = (R * (U1H2 - nx) - S1 * H3) % P - nz = (H * p[2] * q[2]) % P - return (nx, ny, nz) - - -def from_jacobian(p): - z = inv(p[2], P) - return ((p[0] * z**2) % P, (p[1] * z**3) % P) - - -def jacobian_multiply(a, n): - if a[1] == 0 or n == 0: - return (0, 0, 1) - if n == 1: - return a - if n < 0 or n >= N: - return jacobian_multiply(a, n % N) - if (n % 2) == 0: - return jacobian_double(jacobian_multiply(a, n//2)) - if (n % 2) == 1: - return jacobian_add(jacobian_double(jacobian_multiply(a, n//2)), a) - - -def fast_multiply(a, n): - return from_jacobian(jacobian_multiply(to_jacobian(a), n)) - - -def fast_add(a, b): - return from_jacobian(jacobian_add(to_jacobian(a), to_jacobian(b))) - -# Functions for handling pubkey and privkey formats - - -def get_pubkey_format(pub): - if is_python2: - two = '\x02' - three = '\x03' - four = '\x04' - else: - two = 2 - three = 3 - four = 4 - - if isinstance(pub, (tuple, list)): return 'decimal' - elif len(pub) == 65 and pub[0] == four: return 'bin' - elif len(pub) == 130 and pub[0:2] == '04': return 'hex' - elif len(pub) == 33 and pub[0] in [two, three]: return 'bin_compressed' - elif len(pub) == 66 and pub[0:2] in ['02', '03']: return 'hex_compressed' - elif len(pub) == 64: return 'bin_electrum' - elif len(pub) == 128: return 'hex_electrum' - else: raise Exception("Pubkey not in recognized format") - - -def encode_pubkey(pub, formt): - if not isinstance(pub, (tuple, list)): - pub = decode_pubkey(pub) - if formt == 'decimal': return pub - elif formt == 'bin': return b'\x04' + encode(pub[0], 256, 32) + encode(pub[1], 256, 32) - elif formt == 'bin_compressed': - return from_int_to_byte(2+(pub[1] % 2)) + encode(pub[0], 256, 32) - elif formt == 'hex': return '04' + encode(pub[0], 16, 64) + encode(pub[1], 16, 64) - elif formt == 'hex_compressed': - return '0'+str(2+(pub[1] % 2)) + encode(pub[0], 16, 64) - elif formt == 'bin_electrum': return encode(pub[0], 256, 32) + encode(pub[1], 256, 32) - elif formt == 'hex_electrum': return encode(pub[0], 16, 64) + encode(pub[1], 16, 64) - else: raise Exception("Invalid format!") - - -def decode_pubkey(pub, formt=None): - if not formt: formt = get_pubkey_format(pub) - if formt == 'decimal': return pub - elif formt == 'bin': return (decode(pub[1:33], 256), decode(pub[33:65], 256)) - elif formt == 'bin_compressed': - x = decode(pub[1:33], 256) - beta = pow(int(x*x*x+A*x+B), int((P+1)//4), int(P)) - y = (P-beta) if ((beta + from_byte_to_int(pub[0])) % 2) else beta - return (x, y) - elif formt == 'hex': return (decode(pub[2:66], 16), decode(pub[66:130], 16)) - elif formt == 'hex_compressed': - return decode_pubkey(safe_from_hex(pub), 'bin_compressed') - elif formt == 'bin_electrum': - return (decode(pub[:32], 256), decode(pub[32:64], 256)) - elif formt == 'hex_electrum': - return (decode(pub[:64], 16), decode(pub[64:128], 16)) - else: raise Exception("Invalid format!") - -def get_privkey_format(priv): - if isinstance(priv, int_types): return 'decimal' - elif len(priv) == 32: return 'bin' - elif len(priv) == 33: return 'bin_compressed' - elif len(priv) == 64: return 'hex' - elif len(priv) == 66: return 'hex_compressed' - else: - bin_p = b58check_to_bin(priv) - if len(bin_p) == 32: return 'wif' - elif len(bin_p) == 33: return 'wif_compressed' - else: raise Exception("WIF does not represent privkey") - -def encode_privkey(priv, formt, vbyte=0): - if not isinstance(priv, int_types): - return encode_privkey(decode_privkey(priv), formt, vbyte) - if formt == 'decimal': return priv - elif formt == 'bin': return encode(priv, 256, 32) - elif formt == 'bin_compressed': return encode(priv, 256, 32)+b'\x01' - elif formt == 'hex': return encode(priv, 16, 64) - elif formt == 'hex_compressed': return encode(priv, 16, 64)+'01' - elif formt == 'wif': - return bin_to_b58check(encode(priv, 256, 32), 128+int(vbyte)) - elif formt == 'wif_compressed': - return bin_to_b58check(encode(priv, 256, 32)+b'\x01', 128+int(vbyte)) - else: raise Exception("Invalid format!") - -def decode_privkey(priv,formt=None): - if not formt: formt = get_privkey_format(priv) - if formt == 'decimal': return priv - elif formt == 'bin': return decode(priv, 256) - elif formt == 'bin_compressed': return decode(priv[:32], 256) - elif formt == 'hex': return decode(priv, 16) - elif formt == 'hex_compressed': return decode(priv[:64], 16) - elif formt == 'wif': return decode(b58check_to_bin(priv),256) - elif formt == 'wif_compressed': - return decode(b58check_to_bin(priv)[:32],256) - else: raise Exception("WIF does not represent privkey") - -def add_pubkeys(p1, p2): - f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2) - return encode_pubkey(fast_add(decode_pubkey(p1, f1), decode_pubkey(p2, f2)), f1) - -def add_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - return encode_privkey((decode_privkey(p1, f1) + decode_privkey(p2, f2)) % N, f1) - -def mul_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - return encode_privkey((decode_privkey(p1, f1) * decode_privkey(p2, f2)) % N, f1) - -def multiply(pubkey, privkey): - f1, f2 = get_pubkey_format(pubkey), get_privkey_format(privkey) - pubkey, privkey = decode_pubkey(pubkey, f1), decode_privkey(privkey, f2) - # http://safecurves.cr.yp.to/twist.html - if not isinf(pubkey) and (pubkey[0]**3+B-pubkey[1]*pubkey[1]) % P != 0: - raise Exception("Point not on curve") - return encode_pubkey(fast_multiply(pubkey, privkey), f1) - - -def divide(pubkey, privkey): - factor = inv(decode_privkey(privkey), N) - return multiply(pubkey, factor) - - -def compress(pubkey): - f = get_pubkey_format(pubkey) - if 'compressed' in f: return pubkey - elif f == 'bin': return encode_pubkey(decode_pubkey(pubkey, f), 'bin_compressed') - elif f == 'hex' or f == 'decimal': - return encode_pubkey(decode_pubkey(pubkey, f), 'hex_compressed') - - -def decompress(pubkey): - f = get_pubkey_format(pubkey) - if 'compressed' not in f: return pubkey - elif f == 'bin_compressed': return encode_pubkey(decode_pubkey(pubkey, f), 'bin') - elif f == 'hex_compressed' or f == 'decimal': - return encode_pubkey(decode_pubkey(pubkey, f), 'hex') - - -def privkey_to_pubkey(privkey): - f = get_privkey_format(privkey) - privkey = decode_privkey(privkey, f) - if privkey >= N: - raise Exception("Invalid privkey") - if f in ['bin', 'bin_compressed', 'hex', 'hex_compressed', 'decimal']: - return encode_pubkey(fast_multiply(G, privkey), f) - else: - return encode_pubkey(fast_multiply(G, privkey), f.replace('wif', 'hex')) - -privtopub = privkey_to_pubkey - - -def privkey_to_address(priv, magicbyte=0): - return pubkey_to_address(privkey_to_pubkey(priv), magicbyte) -privtoaddr = privkey_to_address - - -def neg_pubkey(pubkey): - f = get_pubkey_format(pubkey) - pubkey = decode_pubkey(pubkey, f) - return encode_pubkey((pubkey[0], (P-pubkey[1]) % P), f) - - -def neg_privkey(privkey): - f = get_privkey_format(privkey) - privkey = decode_privkey(privkey, f) - return encode_privkey((N - privkey) % N, f) - -def subtract_pubkeys(p1, p2): - f1, f2 = get_pubkey_format(p1), get_pubkey_format(p2) - k2 = decode_pubkey(p2, f2) - return encode_pubkey(fast_add(decode_pubkey(p1, f1), (k2[0], (P - k2[1]) % P)), f1) - - -def subtract_privkeys(p1, p2): - f1, f2 = get_privkey_format(p1), get_privkey_format(p2) - k2 = decode_privkey(p2, f2) - return encode_privkey((decode_privkey(p1, f1) - k2) % N, f1) - -# Hashes - - -def bin_hash160(string): - intermed = hashlib.sha256(string).digest() - digest = '' - try: - digest = hashlib.new('ripemd160', intermed).digest() - except: - digest = RIPEMD160(intermed).digest() - return digest - - -def hash160(string): - return safe_hexlify(bin_hash160(string)) - - -def bin_sha256(string): - binary_data = string if isinstance(string, bytes) else bytes(string, 'utf-8') - return hashlib.sha256(binary_data).digest() - -def sha256(string): - return bytes_to_hex_string(bin_sha256(string)) - - -def bin_ripemd160(string): - try: - digest = hashlib.new('ripemd160', string).digest() - except: - digest = RIPEMD160(string).digest() - return digest - - -def ripemd160(string): - return safe_hexlify(bin_ripemd160(string)) - - -def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - -def dbl_sha256(string): - return safe_hexlify(bin_dbl_sha256(string)) - - -def bin_slowsha(string): - string = from_string_to_bytes(string) - orig_input = string - for i in range(100000): - string = hashlib.sha256(string + orig_input).digest() - return string - - -def slowsha(string): - return safe_hexlify(bin_slowsha(string)) - - -def hash_to_int(x): - if len(x) in [40, 64]: - return decode(x, 16) - return decode(x, 256) - - -def num_to_var_int(x): - x = int(x) - if x < 253: return from_int_to_byte(x) - elif x < 65536: return from_int_to_byte(253)+encode(x, 256, 2)[::-1] - elif x < 4294967296: return from_int_to_byte(254) + encode(x, 256, 4)[::-1] - else: return from_int_to_byte(255) + encode(x, 256, 8)[::-1] - - -# WTF, Electrum? -def electrum_sig_hash(message): - padded = b"\x18Bitcoin Signed Message:\n" + num_to_var_int(len(message)) + from_string_to_bytes(message) - return bin_dbl_sha256(padded) - - -def random_key(): - # Gotta be secure after that java.SecureRandom fiasco... - entropy = random_string(32) \ - + str(random.randrange(2**256)) \ - + str(int(time.time() * 1000000)) - return sha256(entropy) - - -def random_electrum_seed(): - entropy = os.urandom(32) \ - + str(random.randrange(2**256)) \ - + str(int(time.time() * 1000000)) - return sha256(entropy)[:32] - -# Encodings - -def b58check_to_bin(inp): - leadingzbytes = len(re.match('^1*', inp).group(0)) - data = b'\x00' * leadingzbytes + changebase(inp, 58, 256) - assert bin_dbl_sha256(data[:-4])[:4] == data[-4:] - return data[1:-4] - - -def get_version_byte(inp): - leadingzbytes = len(re.match('^1*', inp).group(0)) - data = b'\x00' * leadingzbytes + changebase(inp, 58, 256) - assert bin_dbl_sha256(data[:-4])[:4] == data[-4:] - return ord(data[0]) - - -def hex_to_b58check(inp, magicbyte=0): - return bin_to_b58check(binascii.unhexlify(inp), magicbyte) - - -def b58check_to_hex(inp): - return safe_hexlify(b58check_to_bin(inp)) - - -def pubkey_to_address(pubkey, magicbyte=0): - if isinstance(pubkey, (list, tuple)): - pubkey = encode_pubkey(pubkey, 'bin') - if len(pubkey) in [66, 130]: - return bin_to_b58check( - bin_hash160(binascii.unhexlify(pubkey)), magicbyte) - return bin_to_b58check(bin_hash160(pubkey), magicbyte) - -pubtoaddr = pubkey_to_address - - -def is_privkey(priv): - try: - get_privkey_format(priv) - return True - except: - return False - -def is_pubkey(pubkey): - try: - get_pubkey_format(pubkey) - return True - except: - return False - -def is_address(addr): - ADDR_RE = re.compile("^[123mn][a-km-zA-HJ-NP-Z0-9]{26,33}$") - return bool(ADDR_RE.match(addr)) - - -# EDCSA - - -def encode_sig(v, r, s): - vb, rb, sb = from_int_to_byte(v), encode(r, 256), encode(s, 256) - - result = base64.b64encode(vb+b'\x00'*(32-len(rb))+rb+b'\x00'*(32-len(sb))+sb) - return result if is_python2 else str(result, 'utf-8') - - -def decode_sig(sig): - bytez = base64.b64decode(sig) - return from_byte_to_int(bytez[0]), decode(bytez[1:33], 256), decode(bytez[33:], 256) - -# https://tools.ietf.org/html/rfc6979#section-3.2 - - -def deterministic_generate_k(msghash, priv): - v = b'\x01' * 32 - k = b'\x00' * 32 - priv = encode_privkey(priv, 'bin') - msghash = encode(hash_to_int(msghash), 256, 32) - k = hmac.new(k, v+b'\x00'+priv+msghash, hashlib.sha256).digest() - v = hmac.new(k, v, hashlib.sha256).digest() - k = hmac.new(k, v+b'\x01'+priv+msghash, hashlib.sha256).digest() - v = hmac.new(k, v, hashlib.sha256).digest() - return decode(hmac.new(k, v, hashlib.sha256).digest(), 256) - - -def ecdsa_raw_sign(msghash, priv): - - z = hash_to_int(msghash) - k = deterministic_generate_k(msghash, priv) - - r, y = fast_multiply(G, k) - s = inv(k, N) * (z + r*decode_privkey(priv)) % N - - v, r, s = 27+((y % 2) ^ (0 if s * 2 < N else 1)), r, s if s * 2 < N else N - s - if 'compressed' in get_privkey_format(priv): - v += 4 - return v, r, s - - -def ecdsa_sign(msg, priv): - v, r, s = ecdsa_raw_sign(electrum_sig_hash(msg), priv) - sig = encode_sig(v, r, s) - assert ecdsa_verify(msg, sig, - privtopub(priv)), "Bad Sig!\t %s\nv = %d\n,r = %d\ns = %d" % (sig, v, r, s) - return sig - - -def ecdsa_raw_verify(msghash, vrs, pub): - v, r, s = vrs - if not (27 <= v <= 34): - return False - - w = inv(s, N) - z = hash_to_int(msghash) - - u1, u2 = z*w % N, r*w % N - x, y = fast_add(fast_multiply(G, u1), fast_multiply(decode_pubkey(pub), u2)) - return bool(r == x and (r % N) and (s % N)) - - -# For BitcoinCore, (msg = addr or msg = "") be default -def ecdsa_verify_addr(msg, sig, addr): - assert is_address(addr) - Q = ecdsa_recover(msg, sig) - magic = get_version_byte(addr) - return (addr == pubtoaddr(Q, int(magic))) or (addr == pubtoaddr(compress(Q), int(magic))) - - -def ecdsa_verify(msg, sig, pub): - if is_address(pub): - return ecdsa_verify_addr(msg, sig, pub) - return ecdsa_raw_verify(electrum_sig_hash(msg), decode_sig(sig), pub) - - -def ecdsa_raw_recover(msghash, vrs): - v, r, s = vrs - if not (27 <= v <= 34): - raise ValueError("%d must in range 27-31" % v) - x = r - xcubedaxb = (x*x*x+A*x+B) % P - beta = pow(xcubedaxb, (P+1)//4, P) - y = beta if v % 2 ^ beta % 2 else (P - beta) - # If xcubedaxb is not a quadratic residue, then r cannot be the x coord - # for a point on the curve, and so the sig is invalid - if (xcubedaxb - y*y) % P != 0 or not (r % N) or not (s % N): - return False - z = hash_to_int(msghash) - Gz = jacobian_multiply((Gx, Gy, 1), (N - z) % N) - XY = jacobian_multiply((x, y, 1), s) - Qr = jacobian_add(Gz, XY) - Q = jacobian_multiply(Qr, inv(r, N)) - Q = from_jacobian(Q) - - # if ecdsa_raw_verify(msghash, vrs, Q): - return Q - # return False - - -def ecdsa_recover(msg, sig): - v,r,s = decode_sig(sig) - Q = ecdsa_raw_recover(electrum_sig_hash(msg), (v,r,s)) - return encode_pubkey(Q, 'hex_compressed') if v >= 31 else encode_pubkey(Q, 'hex') diff --git a/src/lib/pybitcointools/mnemonic.py b/src/lib/pybitcointools/mnemonic.py deleted file mode 100644 index a9df3617..00000000 --- a/src/lib/pybitcointools/mnemonic.py +++ /dev/null @@ -1,127 +0,0 @@ -import hashlib -import os.path -import binascii -import random -from bisect import bisect_left - -wordlist_english=list(open(os.path.join(os.path.dirname(os.path.realpath(__file__)),'english.txt'),'r')) - -def eint_to_bytes(entint,entbits): - a=hex(entint)[2:].rstrip('L').zfill(32) - print(a) - return binascii.unhexlify(a) - -def mnemonic_int_to_words(mint,mint_num_words,wordlist=wordlist_english): - backwords=[wordlist[(mint >> (11*x)) & 0x7FF].strip() for x in range(mint_num_words)] - return backwords[::-1] - -def entropy_cs(entbytes): - entropy_size=8*len(entbytes) - checksum_size=entropy_size//32 - hd=hashlib.sha256(entbytes).hexdigest() - csint=int(hd,16) >> (256-checksum_size) - return csint,checksum_size - -def entropy_to_words(entbytes,wordlist=wordlist_english): - if(len(entbytes) < 4 or len(entbytes) % 4 != 0): - raise ValueError("The size of the entropy must be a multiple of 4 bytes (multiple of 32 bits)") - entropy_size=8*len(entbytes) - csint,checksum_size = entropy_cs(entbytes) - entint=int(binascii.hexlify(entbytes),16) - mint=(entint << checksum_size) | csint - mint_num_words=(entropy_size+checksum_size)//11 - - return mnemonic_int_to_words(mint,mint_num_words,wordlist) - -def words_bisect(word,wordlist=wordlist_english): - lo=bisect_left(wordlist,word) - hi=len(wordlist)-bisect_left(wordlist[:lo:-1],word) - - return lo,hi - -def words_split(wordstr,wordlist=wordlist_english): - def popword(wordstr,wordlist): - for fwl in range(1,9): - w=wordstr[:fwl].strip() - lo,hi=words_bisect(w,wordlist) - if(hi-lo == 1): - return w,wordstr[fwl:].lstrip() - wordlist=wordlist[lo:hi] - raise Exception("Wordstr %s not found in list" %(w)) - - words=[] - tail=wordstr - while(len(tail)): - head,tail=popword(tail,wordlist) - words.append(head) - return words - -def words_to_mnemonic_int(words,wordlist=wordlist_english): - if(isinstance(words,str)): - words=words_split(words,wordlist) - return sum([wordlist.index(w) << (11*x) for x,w in enumerate(words[::-1])]) - -def words_verify(words,wordlist=wordlist_english): - if(isinstance(words,str)): - words=words_split(words,wordlist) - - mint = words_to_mnemonic_int(words,wordlist) - mint_bits=len(words)*11 - cs_bits=mint_bits//32 - entropy_bits=mint_bits-cs_bits - eint=mint >> cs_bits - csint=mint & ((1 << cs_bits)-1) - ebytes=_eint_to_bytes(eint,entropy_bits) - return csint == entropy_cs(ebytes) - -def mnemonic_to_seed(mnemonic_phrase,passphrase=b''): - try: - from hashlib import pbkdf2_hmac - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return pbkdf2_hmac(hash_name='sha512',password=password,salt=salt,iterations=iters) - except: - try: - from Crypto.Protocol.KDF import PBKDF2 - from Crypto.Hash import SHA512,HMAC - - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return PBKDF2(password=password,salt=salt,dkLen=64,count=iters,prf=lambda p,s: HMAC.new(p,s,SHA512).digest()) - except: - try: - - from pbkdf2 import PBKDF2 - import hmac - def pbkdf2_hmac_sha256(password,salt,iters=2048): - return PBKDF2(password,salt, iterations=iters, macmodule=hmac, digestmodule=hashlib.sha512).read(64) - except: - raise RuntimeError("No implementation of pbkdf2 was found!") - - return pbkdf2_hmac_sha256(password=mnemonic_phrase,salt=b'mnemonic'+passphrase) - -def words_mine(prefix,entbits,satisfunction,wordlist=wordlist_english,randombits=random.getrandbits): - prefix_bits=len(prefix)*11 - mine_bits=entbits-prefix_bits - pint=words_to_mnemonic_int(prefix,wordlist) - pint<<=mine_bits - dint=randombits(mine_bits) - count=0 - while(not satisfunction(entropy_to_words(eint_to_bytes(pint+dint,entbits)))): - dint=randombits(mine_bits) - if((count & 0xFFFF) == 0): - print("Searched %f percent of the space" % (float(count)/float(1 << mine_bits))) - - return entropy_to_words(eint_to_bytes(pint+dint,entbits)) - -if __name__=="__main__": - import json - testvectors=json.load(open('vectors.json','r')) - passed=True - for v in testvectors['english']: - ebytes=binascii.unhexlify(v[0]) - w=' '.join(entropy_to_words(ebytes)) - seed=mnemonic_to_seed(w,passphrase='TREZOR') - passed = passed and w==v[1] - passed = passed and binascii.hexlify(seed)==v[2] - print("Tests %s." % ("Passed" if passed else "Failed")) - - diff --git a/src/lib/pybitcointools/py2specials.py b/src/lib/pybitcointools/py2specials.py deleted file mode 100644 index 337154f3..00000000 --- a/src/lib/pybitcointools/py2specials.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys, re -import binascii -import os -import hashlib - - -if sys.version_info.major == 2: - string_types = (str, unicode) - string_or_bytes_types = string_types - int_types = (int, float, long) - - # Base switching - code_strings = { - 2: '01', - 10: '0123456789', - 16: '0123456789abcdef', - 32: 'abcdefghijklmnopqrstuvwxyz234567', - 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', - 256: ''.join([chr(x) for x in range(256)]) - } - - def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - def lpad(msg, symbol, length): - if len(msg) >= length: - return msg - return symbol * (length - len(msg)) + msg - - def get_code_string(base): - if base in code_strings: - return code_strings[base] - else: - raise ValueError("Invalid base!") - - def changebase(string, frm, to, minlen=0): - if frm == to: - return lpad(string, get_code_string(frm)[0], minlen) - return encode(decode(string, frm), to, minlen) - - def bin_to_b58check(inp, magicbyte=0): - if magicbyte == 0: - inp = '\x00' + inp - while magicbyte > 0: - inp = chr(int(magicbyte % 256)) + inp - magicbyte //= 256 - leadingzbytes = len(re.match('^\x00*', inp).group(0)) - checksum = bin_dbl_sha256(inp)[:4] - return '1' * leadingzbytes + changebase(inp+checksum, 256, 58) - - def bytes_to_hex_string(b): - return b.encode('hex') - - def safe_from_hex(s): - return s.decode('hex') - - def from_int_representation_to_bytes(a): - return str(a) - - def from_int_to_byte(a): - return chr(a) - - def from_byte_to_int(a): - return ord(a) - - def from_bytes_to_string(s): - return s - - def from_string_to_bytes(a): - return a - - def safe_hexlify(a): - return binascii.hexlify(a) - - def encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = get_code_string(base) - result = "" - while val > 0: - result = code_string[val % base] + result - val //= base - return code_string[0] * max(minlen - len(result), 0) + result - - def decode(string, base): - base = int(base) - code_string = get_code_string(base) - result = 0 - if base == 16: - string = string.lower() - while len(string) > 0: - result *= base - result += code_string.find(string[0]) - string = string[1:] - return result - - def random_string(x): - return os.urandom(x) diff --git a/src/lib/pybitcointools/py3specials.py b/src/lib/pybitcointools/py3specials.py deleted file mode 100644 index 7593b9a6..00000000 --- a/src/lib/pybitcointools/py3specials.py +++ /dev/null @@ -1,123 +0,0 @@ -import sys, os -import binascii -import hashlib - - -if sys.version_info.major == 3: - string_types = (str) - string_or_bytes_types = (str, bytes) - int_types = (int, float) - # Base switching - code_strings = { - 2: '01', - 10: '0123456789', - 16: '0123456789abcdef', - 32: 'abcdefghijklmnopqrstuvwxyz234567', - 58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', - 256: ''.join([chr(x) for x in range(256)]) - } - - def bin_dbl_sha256(s): - bytes_to_hash = from_string_to_bytes(s) - return hashlib.sha256(hashlib.sha256(bytes_to_hash).digest()).digest() - - def lpad(msg, symbol, length): - if len(msg) >= length: - return msg - return symbol * (length - len(msg)) + msg - - def get_code_string(base): - if base in code_strings: - return code_strings[base] - else: - raise ValueError("Invalid base!") - - def changebase(string, frm, to, minlen=0): - if frm == to: - return lpad(string, get_code_string(frm)[0], minlen) - return encode(decode(string, frm), to, minlen) - - def bin_to_b58check(inp, magicbyte=0): - if magicbyte == 0: - inp = from_int_to_byte(0) + inp - while magicbyte > 0: - inp = from_int_to_byte(magicbyte % 256) + inp - magicbyte //= 256 - - leadingzbytes = 0 - for x in inp: - if x != 0: - break - leadingzbytes += 1 - - checksum = bin_dbl_sha256(inp)[:4] - return '1' * leadingzbytes + changebase(inp+checksum, 256, 58) - - def bytes_to_hex_string(b): - if isinstance(b, str): - return b - - return ''.join('{:02x}'.format(y) for y in b) - - def safe_from_hex(s): - return bytes.fromhex(s) - - def from_int_representation_to_bytes(a): - return bytes(str(a), 'utf-8') - - def from_int_to_byte(a): - return bytes([a]) - - def from_byte_to_int(a): - return a - - def from_string_to_bytes(a): - return a if isinstance(a, bytes) else bytes(a, 'utf-8') - - def safe_hexlify(a): - return str(binascii.hexlify(a), 'utf-8') - - def encode(val, base, minlen=0): - base, minlen = int(base), int(minlen) - code_string = get_code_string(base) - result_bytes = bytes() - while val > 0: - curcode = code_string[val % base] - result_bytes = bytes([ord(curcode)]) + result_bytes - val //= base - - pad_size = minlen - len(result_bytes) - - padding_element = b'\x00' if base == 256 else b'1' \ - if base == 58 else b'0' - if (pad_size > 0): - result_bytes = padding_element*pad_size + result_bytes - - result_string = ''.join([chr(y) for y in result_bytes]) - result = result_bytes if base == 256 else result_string - - return result - - def decode(string, base): - if base == 256 and isinstance(string, str): - string = bytes(bytearray.fromhex(string)) - base = int(base) - code_string = get_code_string(base) - result = 0 - if base == 256: - def extract(d, cs): - return d - else: - def extract(d, cs): - return cs.find(d if isinstance(d, str) else chr(d)) - - if base == 16: - string = string.lower() - while len(string) > 0: - result *= base - result += extract(string[0], code_string) - string = string[1:] - return result - - def random_string(x): - return str(os.urandom(x)) diff --git a/src/lib/pybitcointools/ripemd.py b/src/lib/pybitcointools/ripemd.py deleted file mode 100644 index 4b0c6045..00000000 --- a/src/lib/pybitcointools/ripemd.py +++ /dev/null @@ -1,414 +0,0 @@ -## ripemd.py - pure Python implementation of the RIPEMD-160 algorithm. -## Bjorn Edstrom 16 december 2007. -## -## Copyrights -## ========== -## -## This code is a derived from an implementation by Markus Friedl which is -## subject to the following license. This Python implementation is not -## subject to any other license. -## -##/* -## * Copyright (c) 2001 Markus Friedl. All rights reserved. -## * -## * Redistribution and use in source and binary forms, with or without -## * modification, are permitted provided that the following conditions -## * are met: -## * 1. Redistributions of source code must retain the above copyright -## * notice, this list of conditions and the following disclaimer. -## * 2. Redistributions in binary form must reproduce the above copyright -## * notice, this list of conditions and the following disclaimer in the -## * documentation and/or other materials provided with the distribution. -## * -## * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -## * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -## * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -## * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -## * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -## * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -## * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -## * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -## * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -## * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -## */ -##/* -## * Preneel, Bosselaers, Dobbertin, "The Cryptographic Hash Function RIPEMD-160", -## * RSA Laboratories, CryptoBytes, Volume 3, Number 2, Autumn 1997, -## * ftp://ftp.rsasecurity.com/pub/cryptobytes/crypto3n2.pdf -## */ - -try: - import psyco - psyco.full() -except ImportError: - pass - -import sys - -is_python2 = sys.version_info.major == 2 -#block_size = 1 -digest_size = 20 -digestsize = 20 - -try: - range = xrange -except: - pass - -class RIPEMD160: - """Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed.""" - - def __init__(self, arg=None): - self.ctx = RMDContext() - if arg: - self.update(arg) - self.dig = None - - def update(self, arg): - """update(arg)""" - RMD160Update(self.ctx, arg, len(arg)) - self.dig = None - - def digest(self): - """digest()""" - if self.dig: - return self.dig - ctx = self.ctx.copy() - self.dig = RMD160Final(self.ctx) - self.ctx = ctx - return self.dig - - def hexdigest(self): - """hexdigest()""" - dig = self.digest() - hex_digest = '' - for d in dig: - if (is_python2): - hex_digest += '%02x' % ord(d) - else: - hex_digest += '%02x' % d - return hex_digest - - def copy(self): - """copy()""" - import copy - return copy.deepcopy(self) - - - -def new(arg=None): - """Return a new RIPEMD160 object. An optional string argument - may be provided; if present, this string will be automatically - hashed.""" - return RIPEMD160(arg) - - - -# -# Private. -# - -class RMDContext: - def __init__(self): - self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, - 0x10325476, 0xC3D2E1F0] # uint32 - self.count = 0 # uint64 - self.buffer = [0]*64 # uchar - def copy(self): - ctx = RMDContext() - ctx.state = self.state[:] - ctx.count = self.count - ctx.buffer = self.buffer[:] - return ctx - -K0 = 0x00000000 -K1 = 0x5A827999 -K2 = 0x6ED9EBA1 -K3 = 0x8F1BBCDC -K4 = 0xA953FD4E - -KK0 = 0x50A28BE6 -KK1 = 0x5C4DD124 -KK2 = 0x6D703EF3 -KK3 = 0x7A6D76E9 -KK4 = 0x00000000 - -def ROL(n, x): - return ((x << n) & 0xffffffff) | (x >> (32 - n)) - -def F0(x, y, z): - return x ^ y ^ z - -def F1(x, y, z): - return (x & y) | (((~x) % 0x100000000) & z) - -def F2(x, y, z): - return (x | ((~y) % 0x100000000)) ^ z - -def F3(x, y, z): - return (x & z) | (((~z) % 0x100000000) & y) - -def F4(x, y, z): - return x ^ (y | ((~z) % 0x100000000)) - -def R(a, b, c, d, e, Fj, Kj, sj, rj, X): - a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e - c = ROL(10, c) - return a % 0x100000000, c - -PADDING = [0x80] + [0]*63 - -import sys -import struct - -def RMD160Transform(state, block): #uint32 state[5], uchar block[64] - x = [0]*16 - if sys.byteorder == 'little': - if is_python2: - x = struct.unpack('<16L', ''.join([chr(x) for x in block[0:64]])) - else: - x = struct.unpack('<16L', bytes(block[0:64])) - else: - raise "Error!!" - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - #/* Round 1 */ - a, c = R(a, b, c, d, e, F0, K0, 11, 0, x); - e, b = R(e, a, b, c, d, F0, K0, 14, 1, x); - d, a = R(d, e, a, b, c, F0, K0, 15, 2, x); - c, e = R(c, d, e, a, b, F0, K0, 12, 3, x); - b, d = R(b, c, d, e, a, F0, K0, 5, 4, x); - a, c = R(a, b, c, d, e, F0, K0, 8, 5, x); - e, b = R(e, a, b, c, d, F0, K0, 7, 6, x); - d, a = R(d, e, a, b, c, F0, K0, 9, 7, x); - c, e = R(c, d, e, a, b, F0, K0, 11, 8, x); - b, d = R(b, c, d, e, a, F0, K0, 13, 9, x); - a, c = R(a, b, c, d, e, F0, K0, 14, 10, x); - e, b = R(e, a, b, c, d, F0, K0, 15, 11, x); - d, a = R(d, e, a, b, c, F0, K0, 6, 12, x); - c, e = R(c, d, e, a, b, F0, K0, 7, 13, x); - b, d = R(b, c, d, e, a, F0, K0, 9, 14, x); - a, c = R(a, b, c, d, e, F0, K0, 8, 15, x); #/* #15 */ - #/* Round 2 */ - e, b = R(e, a, b, c, d, F1, K1, 7, 7, x); - d, a = R(d, e, a, b, c, F1, K1, 6, 4, x); - c, e = R(c, d, e, a, b, F1, K1, 8, 13, x); - b, d = R(b, c, d, e, a, F1, K1, 13, 1, x); - a, c = R(a, b, c, d, e, F1, K1, 11, 10, x); - e, b = R(e, a, b, c, d, F1, K1, 9, 6, x); - d, a = R(d, e, a, b, c, F1, K1, 7, 15, x); - c, e = R(c, d, e, a, b, F1, K1, 15, 3, x); - b, d = R(b, c, d, e, a, F1, K1, 7, 12, x); - a, c = R(a, b, c, d, e, F1, K1, 12, 0, x); - e, b = R(e, a, b, c, d, F1, K1, 15, 9, x); - d, a = R(d, e, a, b, c, F1, K1, 9, 5, x); - c, e = R(c, d, e, a, b, F1, K1, 11, 2, x); - b, d = R(b, c, d, e, a, F1, K1, 7, 14, x); - a, c = R(a, b, c, d, e, F1, K1, 13, 11, x); - e, b = R(e, a, b, c, d, F1, K1, 12, 8, x); #/* #31 */ - #/* Round 3 */ - d, a = R(d, e, a, b, c, F2, K2, 11, 3, x); - c, e = R(c, d, e, a, b, F2, K2, 13, 10, x); - b, d = R(b, c, d, e, a, F2, K2, 6, 14, x); - a, c = R(a, b, c, d, e, F2, K2, 7, 4, x); - e, b = R(e, a, b, c, d, F2, K2, 14, 9, x); - d, a = R(d, e, a, b, c, F2, K2, 9, 15, x); - c, e = R(c, d, e, a, b, F2, K2, 13, 8, x); - b, d = R(b, c, d, e, a, F2, K2, 15, 1, x); - a, c = R(a, b, c, d, e, F2, K2, 14, 2, x); - e, b = R(e, a, b, c, d, F2, K2, 8, 7, x); - d, a = R(d, e, a, b, c, F2, K2, 13, 0, x); - c, e = R(c, d, e, a, b, F2, K2, 6, 6, x); - b, d = R(b, c, d, e, a, F2, K2, 5, 13, x); - a, c = R(a, b, c, d, e, F2, K2, 12, 11, x); - e, b = R(e, a, b, c, d, F2, K2, 7, 5, x); - d, a = R(d, e, a, b, c, F2, K2, 5, 12, x); #/* #47 */ - #/* Round 4 */ - c, e = R(c, d, e, a, b, F3, K3, 11, 1, x); - b, d = R(b, c, d, e, a, F3, K3, 12, 9, x); - a, c = R(a, b, c, d, e, F3, K3, 14, 11, x); - e, b = R(e, a, b, c, d, F3, K3, 15, 10, x); - d, a = R(d, e, a, b, c, F3, K3, 14, 0, x); - c, e = R(c, d, e, a, b, F3, K3, 15, 8, x); - b, d = R(b, c, d, e, a, F3, K3, 9, 12, x); - a, c = R(a, b, c, d, e, F3, K3, 8, 4, x); - e, b = R(e, a, b, c, d, F3, K3, 9, 13, x); - d, a = R(d, e, a, b, c, F3, K3, 14, 3, x); - c, e = R(c, d, e, a, b, F3, K3, 5, 7, x); - b, d = R(b, c, d, e, a, F3, K3, 6, 15, x); - a, c = R(a, b, c, d, e, F3, K3, 8, 14, x); - e, b = R(e, a, b, c, d, F3, K3, 6, 5, x); - d, a = R(d, e, a, b, c, F3, K3, 5, 6, x); - c, e = R(c, d, e, a, b, F3, K3, 12, 2, x); #/* #63 */ - #/* Round 5 */ - b, d = R(b, c, d, e, a, F4, K4, 9, 4, x); - a, c = R(a, b, c, d, e, F4, K4, 15, 0, x); - e, b = R(e, a, b, c, d, F4, K4, 5, 5, x); - d, a = R(d, e, a, b, c, F4, K4, 11, 9, x); - c, e = R(c, d, e, a, b, F4, K4, 6, 7, x); - b, d = R(b, c, d, e, a, F4, K4, 8, 12, x); - a, c = R(a, b, c, d, e, F4, K4, 13, 2, x); - e, b = R(e, a, b, c, d, F4, K4, 12, 10, x); - d, a = R(d, e, a, b, c, F4, K4, 5, 14, x); - c, e = R(c, d, e, a, b, F4, K4, 12, 1, x); - b, d = R(b, c, d, e, a, F4, K4, 13, 3, x); - a, c = R(a, b, c, d, e, F4, K4, 14, 8, x); - e, b = R(e, a, b, c, d, F4, K4, 11, 11, x); - d, a = R(d, e, a, b, c, F4, K4, 8, 6, x); - c, e = R(c, d, e, a, b, F4, K4, 5, 15, x); - b, d = R(b, c, d, e, a, F4, K4, 6, 13, x); #/* #79 */ - - aa = a; - bb = b; - cc = c; - dd = d; - ee = e; - - a = state[0] - b = state[1] - c = state[2] - d = state[3] - e = state[4] - - #/* Parallel round 1 */ - a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) - e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) - d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) - c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) - b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) - a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) - e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) - d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) - c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) - b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) - a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) - e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) - d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) - c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) - b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) - a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) #/* #15 */ - #/* Parallel round 2 */ - e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) - d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) - c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) - a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) - e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) - d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) - c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) - b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) - a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) - e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) - d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) - c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) - b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) - a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) - e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) #/* #31 */ - #/* Parallel round 3 */ - d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) - c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) - b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) - a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) - e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) - d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) - c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) - b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) - a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) - e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) - c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) - b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) - a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) - e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) - d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) #/* #47 */ - #/* Parallel round 4 */ - c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) - b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) - a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) - e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) - d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) - c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) - b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) - a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) - e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) - d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) - c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) - b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) - a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) - e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) - d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) - c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) #/* #63 */ - #/* Parallel round 5 */ - b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) - e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) - d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) - c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) - b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) - a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) - e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) - d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) - c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) - b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) - a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) - e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) - d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) - c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) - b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) #/* #79 */ - - t = (state[1] + cc + d) % 0x100000000; - state[1] = (state[2] + dd + e) % 0x100000000; - state[2] = (state[3] + ee + a) % 0x100000000; - state[3] = (state[4] + aa + b) % 0x100000000; - state[4] = (state[0] + bb + c) % 0x100000000; - state[0] = t % 0x100000000; - - pass - - -def RMD160Update(ctx, inp, inplen): - if type(inp) == str: - inp = [ord(i)&0xff for i in inp] - - have = int((ctx.count // 8) % 64) - inplen = int(inplen) - need = 64 - have - ctx.count += 8 * inplen - off = 0 - if inplen >= need: - if have: - for i in range(need): - ctx.buffer[have+i] = inp[i] - RMD160Transform(ctx.state, ctx.buffer) - off = need - have = 0 - while off + 64 <= inplen: - RMD160Transform(ctx.state, inp[off:]) #<--- - off += 64 - if off < inplen: - # memcpy(ctx->buffer + have, input+off, len-off); - for i in range(inplen - off): - ctx.buffer[have+i] = inp[off+i] - -def RMD160Final(ctx): - size = struct.pack(" 73: return False - if (sig[0] != 0x30): return False - if (sig[1] != len(sig)-3): return False - rlen = sig[3] - if (5+rlen >= len(sig)): return False - slen = sig[5+rlen] - if (rlen + slen + 7 != len(sig)): return False - if (sig[2] != 0x02): return False - if (rlen == 0): return False - if (sig[4] & 0x80): return False - if (rlen > 1 and (sig[4] == 0x00) and not (sig[5] & 0x80)): return False - if (sig[4+rlen] != 0x02): return False - if (slen == 0): return False - if (sig[rlen+6] & 0x80): return False - if (slen > 1 and (sig[6+rlen] == 0x00) and not (sig[7+rlen] & 0x80)): - return False - return True - -def txhash(tx, hashcode=None): - if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx): - tx = changebase(tx, 16, 256) - if hashcode: - return dbl_sha256(from_string_to_bytes(tx) + encode(int(hashcode), 256, 4)[::-1]) - else: - return safe_hexlify(bin_dbl_sha256(tx)[::-1]) - - -def bin_txhash(tx, hashcode=None): - return binascii.unhexlify(txhash(tx, hashcode)) - - -def ecdsa_tx_sign(tx, priv, hashcode=SIGHASH_ALL): - rawsig = ecdsa_raw_sign(bin_txhash(tx, hashcode), priv) - return der_encode_sig(*rawsig)+encode(hashcode, 16, 2) - - -def ecdsa_tx_verify(tx, sig, pub, hashcode=SIGHASH_ALL): - return ecdsa_raw_verify(bin_txhash(tx, hashcode), der_decode_sig(sig), pub) - - -def ecdsa_tx_recover(tx, sig, hashcode=SIGHASH_ALL): - z = bin_txhash(tx, hashcode) - _, r, s = der_decode_sig(sig) - left = ecdsa_raw_recover(z, (0, r, s)) - right = ecdsa_raw_recover(z, (1, r, s)) - return (encode_pubkey(left, 'hex'), encode_pubkey(right, 'hex')) - -# Scripts - - -def mk_pubkey_script(addr): - # Keep the auxiliary functions around for altcoins' sake - return '76a914' + b58check_to_hex(addr) + '88ac' - - -def mk_scripthash_script(addr): - return 'a914' + b58check_to_hex(addr) + '87' - -# Address representation to output script - - -def address_to_script(addr): - if addr[0] == '3' or addr[0] == '2': - return mk_scripthash_script(addr) - else: - return mk_pubkey_script(addr) - -# Output script to address representation - - -def script_to_address(script, vbyte=0): - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - if script[:3] == b'\x76\xa9\x14' and script[-2:] == b'\x88\xac' and len(script) == 25: - return bin_to_b58check(script[3:-2], vbyte) # pubkey hash addresses - else: - if vbyte in [111, 196]: - # Testnet - scripthash_byte = 196 - elif vbyte == 0: - # Mainnet - scripthash_byte = 5 - else: - scripthash_byte = vbyte - # BIP0016 scripthash addresses - return bin_to_b58check(script[2:-1], scripthash_byte) - - -def p2sh_scriptaddr(script, magicbyte=5): - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - return hex_to_b58check(hash160(script), magicbyte) -scriptaddr = p2sh_scriptaddr - - -def deserialize_script(script): - if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script): - return json_changebase(deserialize_script(binascii.unhexlify(script)), - lambda x: safe_hexlify(x)) - out, pos = [], 0 - while pos < len(script): - code = from_byte_to_int(script[pos]) - if code == 0: - out.append(None) - pos += 1 - elif code <= 75: - out.append(script[pos+1:pos+1+code]) - pos += 1 + code - elif code <= 78: - szsz = pow(2, code - 76) - sz = decode(script[pos+szsz: pos:-1], 256) - out.append(script[pos + 1 + szsz:pos + 1 + szsz + sz]) - pos += 1 + szsz + sz - elif code <= 96: - out.append(code - 80) - pos += 1 - else: - out.append(code) - pos += 1 - return out - - -def serialize_script_unit(unit): - if isinstance(unit, int): - if unit < 16: - return from_int_to_byte(unit + 80) - else: - return from_int_to_byte(unit) - elif unit is None: - return b'\x00' - else: - if len(unit) <= 75: - return from_int_to_byte(len(unit))+unit - elif len(unit) < 256: - return from_int_to_byte(76)+from_int_to_byte(len(unit))+unit - elif len(unit) < 65536: - return from_int_to_byte(77)+encode(len(unit), 256, 2)[::-1]+unit - else: - return from_int_to_byte(78)+encode(len(unit), 256, 4)[::-1]+unit - - -if is_python2: - def serialize_script(script): - if json_is_base(script, 16): - return binascii.hexlify(serialize_script(json_changebase(script, - lambda x: binascii.unhexlify(x)))) - return ''.join(map(serialize_script_unit, script)) -else: - def serialize_script(script): - if json_is_base(script, 16): - return safe_hexlify(serialize_script(json_changebase(script, - lambda x: binascii.unhexlify(x)))) - - result = bytes() - for b in map(serialize_script_unit, script): - result += b if isinstance(b, bytes) else bytes(b, 'utf-8') - return result - - -def mk_multisig_script(*args): # [pubs],k or pub1,pub2...pub[n],k - if isinstance(args[0], list): - pubs, k = args[0], int(args[1]) - else: - pubs = list(filter(lambda x: len(str(x)) >= 32, args)) - k = int(args[len(pubs)]) - return serialize_script([k]+pubs+[len(pubs)]+[0xae]) - -# Signing and verifying - - -def verify_tx_input(tx, i, script, sig, pub): - if re.match('^[0-9a-fA-F]*$', tx): - tx = binascii.unhexlify(tx) - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - if not re.match('^[0-9a-fA-F]*$', sig): - sig = safe_hexlify(sig) - hashcode = decode(sig[-2:], 16) - modtx = signature_form(tx, int(i), script, hashcode) - return ecdsa_tx_verify(modtx, sig, pub, hashcode) - - -def sign(tx, i, priv, hashcode=SIGHASH_ALL): - i = int(i) - if (not is_python2 and isinstance(re, bytes)) or not re.match('^[0-9a-fA-F]*$', tx): - return binascii.unhexlify(sign(safe_hexlify(tx), i, priv)) - if len(priv) <= 33: - priv = safe_hexlify(priv) - pub = privkey_to_pubkey(priv) - address = pubkey_to_address(pub) - signing_tx = signature_form(tx, i, mk_pubkey_script(address), hashcode) - sig = ecdsa_tx_sign(signing_tx, priv, hashcode) - txobj = deserialize(tx) - txobj["ins"][i]["script"] = serialize_script([sig, pub]) - return serialize(txobj) - - -def signall(tx, priv): - # if priv is a dictionary, assume format is - # { 'txinhash:txinidx' : privkey } - if isinstance(priv, dict): - for e, i in enumerate(deserialize(tx)["ins"]): - k = priv["%s:%d" % (i["outpoint"]["hash"], i["outpoint"]["index"])] - tx = sign(tx, e, k) - else: - for i in range(len(deserialize(tx)["ins"])): - tx = sign(tx, i, priv) - return tx - - -def multisign(tx, i, script, pk, hashcode=SIGHASH_ALL): - if re.match('^[0-9a-fA-F]*$', tx): - tx = binascii.unhexlify(tx) - if re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - modtx = signature_form(tx, i, script, hashcode) - return ecdsa_tx_sign(modtx, pk, hashcode) - - -def apply_multisignatures(*args): - # tx,i,script,sigs OR tx,i,script,sig1,sig2...,sig[n] - tx, i, script = args[0], int(args[1]), args[2] - sigs = args[3] if isinstance(args[3], list) else list(args[3:]) - - if isinstance(script, str) and re.match('^[0-9a-fA-F]*$', script): - script = binascii.unhexlify(script) - sigs = [binascii.unhexlify(x) if x[:2] == '30' else x for x in sigs] - if isinstance(tx, str) and re.match('^[0-9a-fA-F]*$', tx): - return safe_hexlify(apply_multisignatures(binascii.unhexlify(tx), i, script, sigs)) - - # Not pushing empty elements on the top of the stack if passing no - # script (in case of bare multisig inputs there is no script) - script_blob = [] if script.__len__() == 0 else [script] - - txobj = deserialize(tx) - txobj["ins"][i]["script"] = serialize_script([None]+sigs+script_blob) - return serialize(txobj) - - -def is_inp(arg): - return len(arg) > 64 or "output" in arg or "outpoint" in arg - - -def mktx(*args): - # [in0, in1...],[out0, out1...] or in0, in1 ... out0 out1 ... - ins, outs = [], [] - for arg in args: - if isinstance(arg, list): - for a in arg: (ins if is_inp(a) else outs).append(a) - else: - (ins if is_inp(arg) else outs).append(arg) - - txobj = {"locktime": 0, "version": 1, "ins": [], "outs": []} - for i in ins: - if isinstance(i, dict) and "outpoint" in i: - txobj["ins"].append(i) - else: - if isinstance(i, dict) and "output" in i: - i = i["output"] - txobj["ins"].append({ - "outpoint": {"hash": i[:64], "index": int(i[65:])}, - "script": "", - "sequence": 4294967295 - }) - for o in outs: - if isinstance(o, string_or_bytes_types): - addr = o[:o.find(':')] - val = int(o[o.find(':')+1:]) - o = {} - if re.match('^[0-9a-fA-F]*$', addr): - o["script"] = addr - else: - o["address"] = addr - o["value"] = val - - outobj = {} - if "address" in o: - outobj["script"] = address_to_script(o["address"]) - elif "script" in o: - outobj["script"] = o["script"] - else: - raise Exception("Could not find 'address' or 'script' in output.") - outobj["value"] = o["value"] - txobj["outs"].append(outobj) - - return serialize(txobj) - - -def select(unspent, value): - value = int(value) - high = [u for u in unspent if u["value"] >= value] - high.sort(key=lambda u: u["value"]) - low = [u for u in unspent if u["value"] < value] - low.sort(key=lambda u: -u["value"]) - if len(high): - return [high[0]] - i, tv = 0, 0 - while tv < value and i < len(low): - tv += low[i]["value"] - i += 1 - if tv < value: - raise Exception("Not enough funds") - return low[:i] - -# Only takes inputs of the form { "output": blah, "value": foo } - - -def mksend(*args): - argz, change, fee = args[:-2], args[-2], int(args[-1]) - ins, outs = [], [] - for arg in argz: - if isinstance(arg, list): - for a in arg: - (ins if is_inp(a) else outs).append(a) - else: - (ins if is_inp(arg) else outs).append(arg) - - isum = sum([i["value"] for i in ins]) - osum, outputs2 = 0, [] - for o in outs: - if isinstance(o, string_types): - o2 = { - "address": o[:o.find(':')], - "value": int(o[o.find(':')+1:]) - } - else: - o2 = o - outputs2.append(o2) - osum += o2["value"] - - if isum < osum+fee: - raise Exception("Not enough money") - elif isum > osum+fee+5430: - outputs2 += [{"address": change, "value": isum-osum-fee}] - - return mktx(ins, outputs2) diff --git a/src/lib/pyelliptic/LICENSE b/src/lib/pyelliptic/LICENSE deleted file mode 100644 index 94a9ed02..00000000 --- a/src/lib/pyelliptic/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/src/lib/pyelliptic/README.md b/src/lib/pyelliptic/README.md deleted file mode 100644 index 3acf819c..00000000 --- a/src/lib/pyelliptic/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# PyElliptic - -PyElliptic is a high level wrapper for the cryptographic library : OpenSSL. -Under the GNU General Public License - -Python3 compatible. For GNU/Linux and Windows. -Require OpenSSL - -## Version - -The [upstream pyelliptic](https://github.com/yann2192/pyelliptic) has been -deprecated by the author at 1.5.8 and ECC API has been removed. - -This version is a fork of the pyelliptic extracted from the [BitMessage source -tree](https://github.com/Bitmessage/PyBitmessage), and does contain the ECC -API. To minimize confusion but to avoid renaming the module, major version has -been bumped. - -BitMessage is actively maintained, and this fork of pyelliptic will track and -incorporate any changes to pyelliptic from BitMessage. Ideally, in the future, -BitMessage would import this module as a dependency instead of maintaining a -copy of the source in its repository. - -The BitMessage fork forked from v1.3 of upstream pyelliptic. The commits in -this repository are the commits extracted from the BitMessage repository and -applied to pyelliptic v1.3 upstream repository (i.e. to the base of the fork), -so history with athorship is preserved. - -Some of the changes in upstream pyelliptic between 1.3 and 1.5.8 came from -BitMessage, those changes are present in this fork. Other changes do not exist -in this fork (they may be added in the future). - -Also, a few minor changes exist in this fork but is not (yet) present in -BitMessage source. See: - - git log 1.3-PyBitmessage-37489cf7feff8d5047f24baa8f6d27f353a6d6ac..HEAD - -## Features - -### Asymmetric cryptography using Elliptic Curve Cryptography (ECC) - -* Key agreement : ECDH -* Digital signatures : ECDSA -* Hybrid encryption : ECIES (like RSA) - -### Symmetric cryptography - -* AES-128 (CBC, OFB, CFB, CTR) -* AES-256 (CBC, OFB, CFB, CTR) -* Blowfish (CFB and CBC) -* RC4 - -### Other - -* CSPRNG -* HMAC (using SHA512) -* PBKDF2 (SHA256 and SHA512) - -## Example - -```python -#!/usr/bin/python - -import pyelliptic - -# Symmetric encryption -iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') -ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') - -ciphertext = ctx.update('test1') -ciphertext += ctx.update('test2') -ciphertext += ctx.final() - -ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') -print ctx2.ciphering(ciphertext) - -# Asymmetric encryption -alice = pyelliptic.ECC() # default curve: sect283r1 -bob = pyelliptic.ECC(curve='sect571r1') - -ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) -print bob.decrypt(ciphertext) - -signature = bob.sign("Hello Alice") -# alice's job : -print pyelliptic.ECC(pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") - -# ERROR !!! -try: - key = alice.get_ecdh_key(bob.get_pubkey()) -except: print("For ECDH key agreement, the keys must be defined on the same curve !") - -alice = pyelliptic.ECC(curve='sect571r1') -print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') -print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') -``` diff --git a/src/lib/pyelliptic/__init__.py b/src/lib/pyelliptic/__init__.py deleted file mode 100644 index 761d08af..00000000 --- a/src/lib/pyelliptic/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (C) 2010 -# Author: Yann GUIBET -# Contact: - -__version__ = '1.3' - -__all__ = [ - 'OpenSSL', - 'ECC', - 'Cipher', - 'hmac_sha256', - 'hmac_sha512', - 'pbkdf2' -] - -from .openssl import OpenSSL -from .ecc import ECC -from .cipher import Cipher -from .hash import hmac_sha256, hmac_sha512, pbkdf2 diff --git a/src/lib/pyelliptic/arithmetic.py b/src/lib/pyelliptic/arithmetic.py deleted file mode 100644 index 95c85b93..00000000 --- a/src/lib/pyelliptic/arithmetic.py +++ /dev/null @@ -1,144 +0,0 @@ -# pylint: disable=missing-docstring,too-many-function-args - -import hashlib -import re - -P = 2**256 - 2**32 - 2**9 - 2**8 - 2**7 - 2**6 - 2**4 - 1 -A = 0 -Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240 -Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424 -G = (Gx, Gy) - - -def inv(a, n): - lm, hm = 1, 0 - low, high = a % n, n - while low > 1: - r = high / low - nm, new = hm - lm * r, high - low * r - lm, low, hm, high = nm, new, lm, low - return lm % n - - -def get_code_string(base): - if base == 2: - return '01' - elif base == 10: - return '0123456789' - elif base == 16: - return "0123456789abcdef" - elif base == 58: - return "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - elif base == 256: - return ''.join([chr(x) for x in range(256)]) - else: - raise ValueError("Invalid base!") - - -def encode(val, base, minlen=0): - code_string = get_code_string(base) - result = "" - while val > 0: - result = code_string[val % base] + result - val /= base - if len(result) < minlen: - result = code_string[0] * (minlen - len(result)) + result - return result - - -def decode(string, base): - code_string = get_code_string(base) - result = 0 - if base == 16: - string = string.lower() - while string: - result *= base - result += code_string.find(string[0]) - string = string[1:] - return result - - -def changebase(string, frm, to, minlen=0): - return encode(decode(string, frm), to, minlen) - - -def base10_add(a, b): - if a is None: - return b[0], b[1] - if b is None: - return a[0], a[1] - if a[0] == b[0]: - if a[1] == b[1]: - return base10_double(a[0], a[1]) - return None - m = ((b[1] - a[1]) * inv(b[0] - a[0], P)) % P - x = (m * m - a[0] - b[0]) % P - y = (m * (a[0] - x) - a[1]) % P - return (x, y) - - -def base10_double(a): - if a is None: - return None - m = ((3 * a[0] * a[0] + A) * inv(2 * a[1], P)) % P - x = (m * m - 2 * a[0]) % P - y = (m * (a[0] - x) - a[1]) % P - return (x, y) - - -def base10_multiply(a, n): - if n == 0: - return G - if n == 1: - return a - if (n % 2) == 0: - return base10_double(base10_multiply(a, n / 2)) - if (n % 2) == 1: - return base10_add(base10_double(base10_multiply(a, n / 2)), a) - return None - - -def hex_to_point(h): - return (decode(h[2:66], 16), decode(h[66:], 16)) - - -def point_to_hex(p): - return '04' + encode(p[0], 16, 64) + encode(p[1], 16, 64) - - -def multiply(privkey, pubkey): - return point_to_hex(base10_multiply(hex_to_point(pubkey), decode(privkey, 16))) - - -def privtopub(privkey): - return point_to_hex(base10_multiply(G, decode(privkey, 16))) - - -def add(p1, p2): - if len(p1) == 32: - return encode(decode(p1, 16) + decode(p2, 16) % P, 16, 32) - return point_to_hex(base10_add(hex_to_point(p1), hex_to_point(p2))) - - -def hash_160(string): - intermed = hashlib.sha256(string).digest() - ripemd160 = hashlib.new('ripemd160') - ripemd160.update(intermed) - return ripemd160.digest() - - -def dbl_sha256(string): - return hashlib.sha256(hashlib.sha256(string).digest()).digest() - - -def bin_to_b58check(inp): - inp_fmtd = '\x00' + inp - leadingzbytes = len(re.match('^\x00*', inp_fmtd).group(0)) - checksum = dbl_sha256(inp_fmtd)[:4] - return '1' * leadingzbytes + changebase(inp_fmtd + checksum, 256, 58) - -# Convert a public key (in hex) to a Bitcoin address - - -def pubkey_to_address(pubkey): - return bin_to_b58check(hash_160(changebase(pubkey, 16, 256))) diff --git a/src/lib/pyelliptic/cipher.py b/src/lib/pyelliptic/cipher.py deleted file mode 100644 index 54ae7a09..00000000 --- a/src/lib/pyelliptic/cipher.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from .openssl import OpenSSL - - -class Cipher: - """ - Symmetric encryption - - import pyelliptic - iv = pyelliptic.Cipher.gen_IV('aes-256-cfb') - ctx = pyelliptic.Cipher("secretkey", iv, 1, ciphername='aes-256-cfb') - ciphertext = ctx.update('test1') - ciphertext += ctx.update('test2') - ciphertext += ctx.final() - - ctx2 = pyelliptic.Cipher("secretkey", iv, 0, ciphername='aes-256-cfb') - print ctx2.ciphering(ciphertext) - """ - def __init__(self, key, iv, do, ciphername='aes-256-cbc'): - """ - do == 1 => Encrypt; do == 0 => Decrypt - """ - self.cipher = OpenSSL.get_cipher(ciphername) - self.ctx = OpenSSL.EVP_CIPHER_CTX_new() - if do == 1 or do == 0: - k = OpenSSL.malloc(key, len(key)) - IV = OpenSSL.malloc(iv, len(iv)) - OpenSSL.EVP_CipherInit_ex( - self.ctx, self.cipher.get_pointer(), 0, k, IV, do) - else: - raise Exception("RTFM ...") - - @staticmethod - def get_all_cipher(): - """ - static method, returns all ciphers available - """ - return OpenSSL.cipher_algo.keys() - - @staticmethod - def get_blocksize(ciphername): - cipher = OpenSSL.get_cipher(ciphername) - return cipher.get_blocksize() - - @staticmethod - def gen_IV(ciphername): - cipher = OpenSSL.get_cipher(ciphername) - return OpenSSL.rand(cipher.get_blocksize()) - - def update(self, input): - i = OpenSSL.c_int(0) - buffer = OpenSSL.malloc(b"", len(input) + self.cipher.get_blocksize()) - inp = OpenSSL.malloc(input, len(input)) - if OpenSSL.EVP_CipherUpdate(self.ctx, OpenSSL.byref(buffer), - OpenSSL.byref(i), inp, len(input)) == 0: - raise Exception("[OpenSSL] EVP_CipherUpdate FAIL ...") - return buffer.raw[0:i.value] - - def final(self): - i = OpenSSL.c_int(0) - buffer = OpenSSL.malloc(b"", self.cipher.get_blocksize()) - if (OpenSSL.EVP_CipherFinal_ex(self.ctx, OpenSSL.byref(buffer), - OpenSSL.byref(i))) == 0: - raise Exception("[OpenSSL] EVP_CipherFinal_ex FAIL ...") - return buffer.raw[0:i.value] - - def ciphering(self, input): - """ - Do update and final in one method - """ - buff = self.update(input) - return buff + self.final() - - def __del__(self): - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_CIPHER_CTX_reset(self.ctx) - else: - OpenSSL.EVP_CIPHER_CTX_cleanup(self.ctx) - OpenSSL.EVP_CIPHER_CTX_free(self.ctx) diff --git a/src/lib/pyelliptic/ecc.py b/src/lib/pyelliptic/ecc.py deleted file mode 100644 index a45f8d78..00000000 --- a/src/lib/pyelliptic/ecc.py +++ /dev/null @@ -1,505 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -pyelliptic/ecc.py -===================== -""" -# pylint: disable=protected-access - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from hashlib import sha512 -from struct import pack, unpack - -from .cipher import Cipher -from .hash import equals, hmac_sha256 -from .openssl import OpenSSL - - -class ECC(object): - """ - Asymmetric encryption with Elliptic Curve Cryptography (ECC) - ECDH, ECDSA and ECIES - - >>> import pyelliptic - - >>> alice = pyelliptic.ECC() # default curve: sect283r1 - >>> bob = pyelliptic.ECC(curve='sect571r1') - - >>> ciphertext = alice.encrypt("Hello Bob", bob.get_pubkey()) - >>> print bob.decrypt(ciphertext) - - >>> signature = bob.sign("Hello Alice") - >>> # alice's job : - >>> print pyelliptic.ECC( - >>> pubkey=bob.get_pubkey()).verify(signature, "Hello Alice") - - >>> # ERROR !!! - >>> try: - >>> key = alice.get_ecdh_key(bob.get_pubkey()) - >>> except: - >>> print("For ECDH key agreement, the keys must be defined on the same curve !") - - >>> alice = pyelliptic.ECC(curve='sect571r1') - >>> print alice.get_ecdh_key(bob.get_pubkey()).encode('hex') - >>> print bob.get_ecdh_key(alice.get_pubkey()).encode('hex') - - """ - - def __init__( - self, - pubkey=None, - privkey=None, - pubkey_x=None, - pubkey_y=None, - raw_privkey=None, - curve='sect283r1', - ): # pylint: disable=too-many-arguments - """ - For a normal and High level use, specifie pubkey, - privkey (if you need) and the curve - """ - if isinstance(curve, str): - self.curve = OpenSSL.get_curve(curve) - else: - self.curve = curve - - if pubkey_x is not None and pubkey_y is not None: - self._set_keys(pubkey_x, pubkey_y, raw_privkey) - elif pubkey is not None: - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if privkey is not None: - curve2, raw_privkey, _ = ECC._decode_privkey(privkey) - if curve != curve2: - raise Exception("Bad ECC keys ...") - self.curve = curve - self._set_keys(pubkey_x, pubkey_y, raw_privkey) - else: - self.privkey, self.pubkey_x, self.pubkey_y = self._generate() - - def _set_keys(self, pubkey_x, pubkey_y, privkey): - if self.raw_check_key(privkey, pubkey_x, pubkey_y) < 0: - self.pubkey_x = None - self.pubkey_y = None - self.privkey = None - raise Exception("Bad ECC keys ...") - else: - self.pubkey_x = pubkey_x - self.pubkey_y = pubkey_y - self.privkey = privkey - - @staticmethod - def get_curves(): - """ - static method, returns the list of all the curves available - """ - return OpenSSL.curves.keys() - - def get_curve(self): - """Encryption object from curve name""" - return OpenSSL.get_curve_by_id(self.curve) - - def get_curve_id(self): - """Currently used curve""" - return self.curve - - def get_pubkey(self): - """ - High level function which returns : - curve(2) + len_of_pubkeyX(2) + pubkeyX + len_of_pubkeyY + pubkeyY - """ - return b''.join(( - pack('!H', self.curve), - pack('!H', len(self.pubkey_x)), - self.pubkey_x, - pack('!H', len(self.pubkey_y)), - self.pubkey_y, - )) - - def get_privkey(self): - """ - High level function which returns - curve(2) + len_of_privkey(2) + privkey - """ - return b''.join(( - pack('!H', self.curve), - pack('!H', len(self.privkey)), - self.privkey, - )) - - @staticmethod - def _decode_pubkey(pubkey): - i = 0 - curve = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - tmplen = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - pubkey_x = pubkey[i:i + tmplen] - i += tmplen - tmplen = unpack('!H', pubkey[i:i + 2])[0] - i += 2 - pubkey_y = pubkey[i:i + tmplen] - i += tmplen - return curve, pubkey_x, pubkey_y, i - - @staticmethod - def _decode_privkey(privkey): - i = 0 - curve = unpack('!H', privkey[i:i + 2])[0] - i += 2 - tmplen = unpack('!H', privkey[i:i + 2])[0] - i += 2 - privkey = privkey[i:i + tmplen] - i += tmplen - return curve, privkey, i - - def _generate(self): - try: - pub_key_x = OpenSSL.BN_new() - pub_key_y = OpenSSL.BN_new() - - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - if (OpenSSL.EC_KEY_generate_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_generate_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - priv_key = OpenSSL.EC_KEY_get0_private_key(key) - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_KEY_get0_public_key(key) - - if OpenSSL.EC_POINT_get_affine_coordinates_GFp( - group, pub_key, pub_key_x, pub_key_y, 0) == 0: - raise Exception("[OpenSSL] EC_POINT_get_affine_coordinates_GFp FAIL ...") - - privkey = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(priv_key)) - pubkeyx = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_x)) - pubkeyy = OpenSSL.malloc(0, OpenSSL.BN_num_bytes(pub_key_y)) - OpenSSL.BN_bn2bin(priv_key, privkey) - privkey = privkey.raw - OpenSSL.BN_bn2bin(pub_key_x, pubkeyx) - pubkeyx = pubkeyx.raw - OpenSSL.BN_bn2bin(pub_key_y, pubkeyy) - pubkeyy = pubkeyy.raw - self.raw_check_key(privkey, pubkeyx, pubkeyy) - - return privkey, pubkeyx, pubkeyy - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - - def get_ecdh_key(self, pubkey): - """ - High level function. Compute public key with the local private key - and returns a 512bits shared key - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if curve != self.curve: - raise Exception("ECC keys must be from the same curve !") - return sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - - def raw_get_ecdh_key(self, pubkey_x, pubkey_y): - """ECDH key as binary data""" - try: - ecdh_keybuffer = OpenSSL.malloc(0, 32) - - other_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if other_key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - other_pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) - other_pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) - - other_group = OpenSSL.EC_KEY_get0_group(other_key) - other_pub_key = OpenSSL.EC_POINT_new(other_group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(other_group, - other_pub_key, - other_pub_key_x, - other_pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(other_key, other_pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(other_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - - own_key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if own_key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - own_priv_key = OpenSSL.BN_bin2bn( - self.privkey, len(self.privkey), 0) - - if (OpenSSL.EC_KEY_set_private_key(own_key, own_priv_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") - - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EC_KEY_set_method(own_key, OpenSSL.EC_KEY_OpenSSL()) - else: - OpenSSL.ECDH_set_method(own_key, OpenSSL.ECDH_OpenSSL()) - ecdh_keylen = OpenSSL.ECDH_compute_key( - ecdh_keybuffer, 32, other_pub_key, own_key, 0) - - if ecdh_keylen != 32: - raise Exception("[OpenSSL] ECDH keylen FAIL ...") - - return ecdh_keybuffer.raw - - finally: - OpenSSL.EC_KEY_free(other_key) - OpenSSL.BN_free(other_pub_key_x) - OpenSSL.BN_free(other_pub_key_y) - OpenSSL.EC_POINT_free(other_pub_key) - OpenSSL.EC_KEY_free(own_key) - OpenSSL.BN_free(own_priv_key) - - def check_key(self, privkey, pubkey): - """ - Check the public key and the private key. - The private key is optional (replace by None) - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - if privkey is None: - raw_privkey = None - curve2 = curve - else: - curve2, raw_privkey, _ = ECC._decode_privkey(privkey) - if curve != curve2: - raise Exception("Bad public and private key") - return self.raw_check_key(raw_privkey, pubkey_x, pubkey_y, curve) - - def raw_check_key(self, privkey, pubkey_x, pubkey_y, curve=None): - """Check key validity, key is supplied as binary data""" - # pylint: disable=too-many-branches - if curve is None: - curve = self.curve - elif isinstance(curve, str): - curve = OpenSSL.get_curve(curve) - else: - curve = curve - try: - key = OpenSSL.EC_KEY_new_by_curve_name(curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - if privkey is not None: - priv_key = OpenSSL.BN_bin2bn(privkey, len(privkey), 0) - pub_key_x = OpenSSL.BN_bin2bn(pubkey_x, len(pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(pubkey_y, len(pubkey_y), 0) - - if privkey is not None: - if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: - raise Exception( - "[OpenSSL] EC_KEY_set_private_key FAIL ...") - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - return 0 - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.EC_POINT_free(pub_key) - if privkey is not None: - OpenSSL.BN_free(priv_key) - - def sign(self, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): - """ - Sign the input with ECDSA method and returns the signature - """ - # pylint: disable=too-many-branches,too-many-locals - try: - size = len(inputb) - buff = OpenSSL.malloc(inputb, size) - digest = OpenSSL.malloc(0, 64) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - md_ctx = OpenSSL.EVP_MD_CTX_new() - else: - md_ctx = OpenSSL.EVP_MD_CTX_create() - dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) - siglen = OpenSSL.pointer(OpenSSL.c_int(0)) - sig = OpenSSL.malloc(0, 151) - - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - priv_key = OpenSSL.BN_bin2bn(self.privkey, len(self.privkey), 0) - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) - - if (OpenSSL.EC_KEY_set_private_key(key, priv_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_private_key FAIL ...") - - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_new(md_ctx) - else: - OpenSSL.EVP_MD_CTX_init(md_ctx) - OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - - if (OpenSSL.EVP_DigestUpdate(md_ctx, buff, size)) == 0: - raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") - OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) - OpenSSL.ECDSA_sign(0, digest, dgst_len.contents, sig, siglen, key) - if (OpenSSL.ECDSA_verify(0, digest, dgst_len.contents, sig, - siglen.contents, key)) != 1: - raise Exception("[OpenSSL] ECDSA_verify FAIL ...") - - return sig.raw[:siglen.contents.value] - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.BN_free(priv_key) - OpenSSL.EC_POINT_free(pub_key) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_free(md_ctx) - else: - OpenSSL.EVP_MD_CTX_destroy(md_ctx) - - def verify(self, sig, inputb, digest_alg=OpenSSL.digest_ecdsa_sha1): - """ - Verify the signature with the input and the local public key. - Returns a boolean - """ - # pylint: disable=too-many-branches - try: - bsig = OpenSSL.malloc(sig, len(sig)) - binputb = OpenSSL.malloc(inputb, len(inputb)) - digest = OpenSSL.malloc(0, 64) - dgst_len = OpenSSL.pointer(OpenSSL.c_int(0)) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - md_ctx = OpenSSL.EVP_MD_CTX_new() - else: - md_ctx = OpenSSL.EVP_MD_CTX_create() - key = OpenSSL.EC_KEY_new_by_curve_name(self.curve) - - if key == 0: - raise Exception("[OpenSSL] EC_KEY_new_by_curve_name FAIL ...") - - pub_key_x = OpenSSL.BN_bin2bn(self.pubkey_x, len(self.pubkey_x), 0) - pub_key_y = OpenSSL.BN_bin2bn(self.pubkey_y, len(self.pubkey_y), 0) - group = OpenSSL.EC_KEY_get0_group(key) - pub_key = OpenSSL.EC_POINT_new(group) - - if (OpenSSL.EC_POINT_set_affine_coordinates_GFp(group, pub_key, - pub_key_x, - pub_key_y, - 0)) == 0: - raise Exception( - "[OpenSSL] EC_POINT_set_affine_coordinates_GFp FAIL ...") - if (OpenSSL.EC_KEY_set_public_key(key, pub_key)) == 0: - raise Exception("[OpenSSL] EC_KEY_set_public_key FAIL ...") - if (OpenSSL.EC_KEY_check_key(key)) == 0: - raise Exception("[OpenSSL] EC_KEY_check_key FAIL ...") - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_new(md_ctx) - else: - OpenSSL.EVP_MD_CTX_init(md_ctx) - OpenSSL.EVP_DigestInit_ex(md_ctx, digest_alg(), None) - if (OpenSSL.EVP_DigestUpdate(md_ctx, binputb, len(inputb))) == 0: - raise Exception("[OpenSSL] EVP_DigestUpdate FAIL ...") - - OpenSSL.EVP_DigestFinal_ex(md_ctx, digest, dgst_len) - ret = OpenSSL.ECDSA_verify( - 0, digest, dgst_len.contents, bsig, len(sig), key) - - if ret == -1: - return False # Fail to Check - if ret == 0: - return False # Bad signature ! - return True # Good - - finally: - OpenSSL.EC_KEY_free(key) - OpenSSL.BN_free(pub_key_x) - OpenSSL.BN_free(pub_key_y) - OpenSSL.EC_POINT_free(pub_key) - if OpenSSL._hexversion > 0x10100000 and not OpenSSL._libreSSL: - OpenSSL.EVP_MD_CTX_free(md_ctx) - else: - OpenSSL.EVP_MD_CTX_destroy(md_ctx) - - @staticmethod - def encrypt(data, pubkey, ephemcurve=None, ciphername='aes-256-cbc'): - """ - Encrypt data with ECIES method using the public key of the recipient. - """ - curve, pubkey_x, pubkey_y, _ = ECC._decode_pubkey(pubkey) - return ECC.raw_encrypt(data, pubkey_x, pubkey_y, curve=curve, - ephemcurve=ephemcurve, ciphername=ciphername) - - @staticmethod - def raw_encrypt( - data, - pubkey_x, - pubkey_y, - curve='sect283r1', - ephemcurve=None, - ciphername='aes-256-cbc', - ): # pylint: disable=too-many-arguments - """ECHD encryption, keys supplied in binary data format""" - - if ephemcurve is None: - ephemcurve = curve - ephem = ECC(curve=ephemcurve) - key = sha512(ephem.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - pubkey = ephem.get_pubkey() - iv = OpenSSL.rand(OpenSSL.get_cipher(ciphername).get_blocksize()) - ctx = Cipher(key_e, iv, 1, ciphername) - ciphertext = iv + pubkey + ctx.ciphering(data) - mac = hmac_sha256(key_m, ciphertext) - return ciphertext + mac - - def decrypt(self, data, ciphername='aes-256-cbc'): - """ - Decrypt data with ECIES method using the local private key - """ - # pylint: disable=too-many-locals - blocksize = OpenSSL.get_cipher(ciphername).get_blocksize() - iv = data[:blocksize] - i = blocksize - _, pubkey_x, pubkey_y, i2 = ECC._decode_pubkey(data[i:]) - i += i2 - ciphertext = data[i:len(data) - 32] - i += len(ciphertext) - mac = data[i:] - key = sha512(self.raw_get_ecdh_key(pubkey_x, pubkey_y)).digest() - key_e, key_m = key[:32], key[32:] - if not equals(hmac_sha256(key_m, data[:len(data) - 32]), mac): - raise RuntimeError("Fail to verify data") - ctx = Cipher(key_e, iv, 0, ciphername) - return ctx.ciphering(ciphertext) diff --git a/src/lib/pyelliptic/hash.py b/src/lib/pyelliptic/hash.py deleted file mode 100644 index d6a15811..00000000 --- a/src/lib/pyelliptic/hash.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. - -from .openssl import OpenSSL - - -# For python3 -def _equals_bytes(a, b): - if len(a) != len(b): - return False - result = 0 - for x, y in zip(a, b): - result |= x ^ y - return result == 0 - - -def _equals_str(a, b): - if len(a) != len(b): - return False - result = 0 - for x, y in zip(a, b): - result |= ord(x) ^ ord(y) - return result == 0 - - -def equals(a, b): - if isinstance(a, str): - return _equals_str(a, b) - else: - return _equals_bytes(a, b) - - -def hmac_sha256(k, m): - """ - Compute the key and the message with HMAC SHA5256 - """ - key = OpenSSL.malloc(k, len(k)) - d = OpenSSL.malloc(m, len(m)) - md = OpenSSL.malloc(0, 32) - i = OpenSSL.pointer(OpenSSL.c_int(0)) - OpenSSL.HMAC(OpenSSL.EVP_sha256(), key, len(k), d, len(m), md, i) - return md.raw - - -def hmac_sha512(k, m): - """ - Compute the key and the message with HMAC SHA512 - """ - key = OpenSSL.malloc(k, len(k)) - d = OpenSSL.malloc(m, len(m)) - md = OpenSSL.malloc(0, 64) - i = OpenSSL.pointer(OpenSSL.c_int(0)) - OpenSSL.HMAC(OpenSSL.EVP_sha512(), key, len(k), d, len(m), md, i) - return md.raw - - -def pbkdf2(password, salt=None, i=10000, keylen=64): - if salt is None: - salt = OpenSSL.rand(8) - p_password = OpenSSL.malloc(password, len(password)) - p_salt = OpenSSL.malloc(salt, len(salt)) - output = OpenSSL.malloc(0, keylen) - OpenSSL.PKCS5_PBKDF2_HMAC(p_password, len(password), p_salt, - len(p_salt), i, OpenSSL.EVP_sha256(), - keylen, output) - return salt, output.raw diff --git a/src/lib/pyelliptic/openssl.py b/src/lib/pyelliptic/openssl.py deleted file mode 100644 index bc4fe6a6..00000000 --- a/src/lib/pyelliptic/openssl.py +++ /dev/null @@ -1,553 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# Copyright (C) 2011 Yann GUIBET -# See LICENSE for details. -# -# Software slightly changed by Jonathan Warren - -import sys -import ctypes - -OpenSSL = None - - -class CipherName: - def __init__(self, name, pointer, blocksize): - self._name = name - self._pointer = pointer - self._blocksize = blocksize - - def __str__(self): - return "Cipher : " + self._name + " | Blocksize : " + str(self._blocksize) + " | Fonction pointer : " + str(self._pointer) - - def get_pointer(self): - return self._pointer() - - def get_name(self): - return self._name - - def get_blocksize(self): - return self._blocksize - - -def get_version(library): - version = None - hexversion = None - cflags = None - try: - #OpenSSL 1.1 - OPENSSL_VERSION = 0 - OPENSSL_CFLAGS = 1 - library.OpenSSL_version.argtypes = [ctypes.c_int] - library.OpenSSL_version.restype = ctypes.c_char_p - version = library.OpenSSL_version(OPENSSL_VERSION) - cflags = library.OpenSSL_version(OPENSSL_CFLAGS) - library.OpenSSL_version_num.restype = ctypes.c_long - hexversion = library.OpenSSL_version_num() - except AttributeError: - try: - #OpenSSL 1.0 - SSLEAY_VERSION = 0 - SSLEAY_CFLAGS = 2 - library.SSLeay.restype = ctypes.c_long - library.SSLeay_version.restype = ctypes.c_char_p - library.SSLeay_version.argtypes = [ctypes.c_int] - version = library.SSLeay_version(SSLEAY_VERSION) - cflags = library.SSLeay_version(SSLEAY_CFLAGS) - hexversion = library.SSLeay() - except AttributeError: - #raise NotImplementedError('Cannot determine version of this OpenSSL library.') - pass - return (version, hexversion, cflags) - - -class _OpenSSL: - """ - Wrapper for OpenSSL using ctypes - """ - def __init__(self, library): - """ - Build the wrapper - """ - self._lib = ctypes.CDLL(library) - self._version, self._hexversion, self._cflags = get_version(self._lib) - self._libreSSL = self._version.startswith(b"LibreSSL") - - self.pointer = ctypes.pointer - self.c_int = ctypes.c_int - self.byref = ctypes.byref - self.create_string_buffer = ctypes.create_string_buffer - - self.BN_new = self._lib.BN_new - self.BN_new.restype = ctypes.c_void_p - self.BN_new.argtypes = [] - - self.BN_free = self._lib.BN_free - self.BN_free.restype = None - self.BN_free.argtypes = [ctypes.c_void_p] - - self.BN_num_bits = self._lib.BN_num_bits - self.BN_num_bits.restype = ctypes.c_int - self.BN_num_bits.argtypes = [ctypes.c_void_p] - - self.BN_bn2bin = self._lib.BN_bn2bin - self.BN_bn2bin.restype = ctypes.c_int - self.BN_bn2bin.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.BN_bin2bn = self._lib.BN_bin2bn - self.BN_bin2bn.restype = ctypes.c_void_p - self.BN_bin2bn.argtypes = [ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p] - - self.EC_KEY_free = self._lib.EC_KEY_free - self.EC_KEY_free.restype = None - self.EC_KEY_free.argtypes = [ctypes.c_void_p] - - self.EC_KEY_new_by_curve_name = self._lib.EC_KEY_new_by_curve_name - self.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p - self.EC_KEY_new_by_curve_name.argtypes = [ctypes.c_int] - - self.EC_KEY_generate_key = self._lib.EC_KEY_generate_key - self.EC_KEY_generate_key.restype = ctypes.c_int - self.EC_KEY_generate_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_check_key = self._lib.EC_KEY_check_key - self.EC_KEY_check_key.restype = ctypes.c_int - self.EC_KEY_check_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_private_key = self._lib.EC_KEY_get0_private_key - self.EC_KEY_get0_private_key.restype = ctypes.c_void_p - self.EC_KEY_get0_private_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_public_key = self._lib.EC_KEY_get0_public_key - self.EC_KEY_get0_public_key.restype = ctypes.c_void_p - self.EC_KEY_get0_public_key.argtypes = [ctypes.c_void_p] - - self.EC_KEY_get0_group = self._lib.EC_KEY_get0_group - self.EC_KEY_get0_group.restype = ctypes.c_void_p - self.EC_KEY_get0_group.argtypes = [ctypes.c_void_p] - - self.EC_POINT_get_affine_coordinates_GFp = self._lib.EC_POINT_get_affine_coordinates_GFp - self.EC_POINT_get_affine_coordinates_GFp.restype = ctypes.c_int - self.EC_POINT_get_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key - self.EC_KEY_set_private_key.restype = ctypes.c_int - self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - self.EC_KEY_set_public_key = self._lib.EC_KEY_set_public_key - self.EC_KEY_set_public_key.restype = ctypes.c_int - self.EC_KEY_set_public_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - self.EC_KEY_set_group = self._lib.EC_KEY_set_group - self.EC_KEY_set_group.restype = ctypes.c_int - self.EC_KEY_set_group.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EC_POINT_set_affine_coordinates_GFp = self._lib.EC_POINT_set_affine_coordinates_GFp - self.EC_POINT_set_affine_coordinates_GFp.restype = ctypes.c_int - self.EC_POINT_set_affine_coordinates_GFp.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_POINT_new = self._lib.EC_POINT_new - self.EC_POINT_new.restype = ctypes.c_void_p - self.EC_POINT_new.argtypes = [ctypes.c_void_p] - - self.EC_POINT_free = self._lib.EC_POINT_free - self.EC_POINT_free.restype = None - self.EC_POINT_free.argtypes = [ctypes.c_void_p] - - self.BN_CTX_free = self._lib.BN_CTX_free - self.BN_CTX_free.restype = None - self.BN_CTX_free.argtypes = [ctypes.c_void_p] - - self.EC_POINT_mul = self._lib.EC_POINT_mul - self.EC_POINT_mul.restype = ctypes.c_int - self.EC_POINT_mul.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.EC_KEY_set_private_key = self._lib.EC_KEY_set_private_key - self.EC_KEY_set_private_key.restype = ctypes.c_int - self.EC_KEY_set_private_key.argtypes = [ctypes.c_void_p, - ctypes.c_void_p] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EC_KEY_OpenSSL = self._lib.EC_KEY_OpenSSL - self._lib.EC_KEY_OpenSSL.restype = ctypes.c_void_p - self._lib.EC_KEY_OpenSSL.argtypes = [] - - self.EC_KEY_set_method = self._lib.EC_KEY_set_method - self._lib.EC_KEY_set_method.restype = ctypes.c_int - self._lib.EC_KEY_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - else: - self.ECDH_OpenSSL = self._lib.ECDH_OpenSSL - self._lib.ECDH_OpenSSL.restype = ctypes.c_void_p - self._lib.ECDH_OpenSSL.argtypes = [] - - self.ECDH_set_method = self._lib.ECDH_set_method - self._lib.ECDH_set_method.restype = ctypes.c_int - self._lib.ECDH_set_method.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.BN_CTX_new = self._lib.BN_CTX_new - self._lib.BN_CTX_new.restype = ctypes.c_void_p - self._lib.BN_CTX_new.argtypes = [] - - self.ECDH_compute_key = self._lib.ECDH_compute_key - self.ECDH_compute_key.restype = ctypes.c_int - self.ECDH_compute_key.argtypes = [ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_CipherInit_ex = self._lib.EVP_CipherInit_ex - self.EVP_CipherInit_ex.restype = ctypes.c_int - self.EVP_CipherInit_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_CIPHER_CTX_new = self._lib.EVP_CIPHER_CTX_new - self.EVP_CIPHER_CTX_new.restype = ctypes.c_void_p - self.EVP_CIPHER_CTX_new.argtypes = [] - - # Cipher - self.EVP_aes_128_cfb128 = self._lib.EVP_aes_128_cfb128 - self.EVP_aes_128_cfb128.restype = ctypes.c_void_p - self.EVP_aes_128_cfb128.argtypes = [] - - self.EVP_aes_256_cfb128 = self._lib.EVP_aes_256_cfb128 - self.EVP_aes_256_cfb128.restype = ctypes.c_void_p - self.EVP_aes_256_cfb128.argtypes = [] - - self.EVP_aes_128_cbc = self._lib.EVP_aes_128_cbc - self.EVP_aes_128_cbc.restype = ctypes.c_void_p - self.EVP_aes_128_cbc.argtypes = [] - - self.EVP_aes_256_cbc = self._lib.EVP_aes_256_cbc - self.EVP_aes_256_cbc.restype = ctypes.c_void_p - self.EVP_aes_256_cbc.argtypes = [] - - #self.EVP_aes_128_ctr = self._lib.EVP_aes_128_ctr - #self.EVP_aes_128_ctr.restype = ctypes.c_void_p - #self.EVP_aes_128_ctr.argtypes = [] - - #self.EVP_aes_256_ctr = self._lib.EVP_aes_256_ctr - #self.EVP_aes_256_ctr.restype = ctypes.c_void_p - #self.EVP_aes_256_ctr.argtypes = [] - - self.EVP_aes_128_ofb = self._lib.EVP_aes_128_ofb - self.EVP_aes_128_ofb.restype = ctypes.c_void_p - self.EVP_aes_128_ofb.argtypes = [] - - self.EVP_aes_256_ofb = self._lib.EVP_aes_256_ofb - self.EVP_aes_256_ofb.restype = ctypes.c_void_p - self.EVP_aes_256_ofb.argtypes = [] - - self.EVP_bf_cbc = self._lib.EVP_bf_cbc - self.EVP_bf_cbc.restype = ctypes.c_void_p - self.EVP_bf_cbc.argtypes = [] - - self.EVP_bf_cfb64 = self._lib.EVP_bf_cfb64 - self.EVP_bf_cfb64.restype = ctypes.c_void_p - self.EVP_bf_cfb64.argtypes = [] - - self.EVP_rc4 = self._lib.EVP_rc4 - self.EVP_rc4.restype = ctypes.c_void_p - self.EVP_rc4.argtypes = [] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EVP_CIPHER_CTX_reset = self._lib.EVP_CIPHER_CTX_reset - self.EVP_CIPHER_CTX_reset.restype = ctypes.c_int - self.EVP_CIPHER_CTX_reset.argtypes = [ctypes.c_void_p] - else: - self.EVP_CIPHER_CTX_cleanup = self._lib.EVP_CIPHER_CTX_cleanup - self.EVP_CIPHER_CTX_cleanup.restype = ctypes.c_int - self.EVP_CIPHER_CTX_cleanup.argtypes = [ctypes.c_void_p] - - self.EVP_CIPHER_CTX_free = self._lib.EVP_CIPHER_CTX_free - self.EVP_CIPHER_CTX_free.restype = None - self.EVP_CIPHER_CTX_free.argtypes = [ctypes.c_void_p] - - self.EVP_CipherUpdate = self._lib.EVP_CipherUpdate - self.EVP_CipherUpdate.restype = ctypes.c_int - self.EVP_CipherUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] - - self.EVP_CipherFinal_ex = self._lib.EVP_CipherFinal_ex - self.EVP_CipherFinal_ex.restype = ctypes.c_int - self.EVP_CipherFinal_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestInit = self._lib.EVP_DigestInit - self.EVP_DigestInit.restype = ctypes.c_int - self._lib.EVP_DigestInit.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestInit_ex = self._lib.EVP_DigestInit_ex - self.EVP_DigestInit_ex.restype = ctypes.c_int - self._lib.EVP_DigestInit_ex.argtypes = 3 * [ctypes.c_void_p] - - self.EVP_DigestUpdate = self._lib.EVP_DigestUpdate - self.EVP_DigestUpdate.restype = ctypes.c_int - self.EVP_DigestUpdate.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_int] - - self.EVP_DigestFinal = self._lib.EVP_DigestFinal - self.EVP_DigestFinal.restype = ctypes.c_int - self.EVP_DigestFinal.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_DigestFinal_ex = self._lib.EVP_DigestFinal_ex - self.EVP_DigestFinal_ex.restype = ctypes.c_int - self.EVP_DigestFinal_ex.argtypes = [ctypes.c_void_p, - ctypes.c_void_p, ctypes.c_void_p] - - self.ECDSA_sign = self._lib.ECDSA_sign - self.ECDSA_sign.restype = ctypes.c_int - self.ECDSA_sign.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] - - self.ECDSA_verify = self._lib.ECDSA_verify - self.ECDSA_verify.restype = ctypes.c_int - self.ECDSA_verify.argtypes = [ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] - - if self._hexversion >= 0x10100000 and not self._libreSSL: - self.EVP_MD_CTX_new = self._lib.EVP_MD_CTX_new - self.EVP_MD_CTX_new.restype = ctypes.c_void_p - self.EVP_MD_CTX_new.argtypes = [] - - self.EVP_MD_CTX_reset = self._lib.EVP_MD_CTX_reset - self.EVP_MD_CTX_reset.restype = None - self.EVP_MD_CTX_reset.argtypes = [ctypes.c_void_p] - - self.EVP_MD_CTX_free = self._lib.EVP_MD_CTX_free - self.EVP_MD_CTX_free.restype = None - self.EVP_MD_CTX_free.argtypes = [ctypes.c_void_p] - - self.EVP_sha1 = self._lib.EVP_sha1 - self.EVP_sha1.restype = ctypes.c_void_p - self.EVP_sha1.argtypes = [] - - self.digest_ecdsa_sha1 = self.EVP_sha1 - else: - self.EVP_MD_CTX_create = self._lib.EVP_MD_CTX_create - self.EVP_MD_CTX_create.restype = ctypes.c_void_p - self.EVP_MD_CTX_create.argtypes = [] - - self.EVP_MD_CTX_init = self._lib.EVP_MD_CTX_init - self.EVP_MD_CTX_init.restype = None - self.EVP_MD_CTX_init.argtypes = [ctypes.c_void_p] - - self.EVP_MD_CTX_destroy = self._lib.EVP_MD_CTX_destroy - self.EVP_MD_CTX_destroy.restype = None - self.EVP_MD_CTX_destroy.argtypes = [ctypes.c_void_p] - - self.EVP_ecdsa = self._lib.EVP_ecdsa - self._lib.EVP_ecdsa.restype = ctypes.c_void_p - self._lib.EVP_ecdsa.argtypes = [] - - self.digest_ecdsa_sha1 = self.EVP_ecdsa - - self.RAND_bytes = self._lib.RAND_bytes - self.RAND_bytes.restype = ctypes.c_int - self.RAND_bytes.argtypes = [ctypes.c_void_p, ctypes.c_int] - - self.EVP_sha256 = self._lib.EVP_sha256 - self.EVP_sha256.restype = ctypes.c_void_p - self.EVP_sha256.argtypes = [] - - self.i2o_ECPublicKey = self._lib.i2o_ECPublicKey - self.i2o_ECPublicKey.restype = ctypes.c_int - self.i2o_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p] - - self.EVP_sha512 = self._lib.EVP_sha512 - self.EVP_sha512.restype = ctypes.c_void_p - self.EVP_sha512.argtypes = [] - - self.HMAC = self._lib.HMAC - self.HMAC.restype = ctypes.c_void_p - self.HMAC.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p] - - try: - self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC - except: - # The above is not compatible with all versions of OSX. - self.PKCS5_PBKDF2_HMAC = self._lib.PKCS5_PBKDF2_HMAC_SHA1 - - self.PKCS5_PBKDF2_HMAC.restype = ctypes.c_int - self.PKCS5_PBKDF2_HMAC.argtypes = [ctypes.c_void_p, ctypes.c_int, - ctypes.c_void_p, ctypes.c_int, - ctypes.c_int, ctypes.c_void_p, - ctypes.c_int, ctypes.c_void_p] - - self._set_ciphers() - self._set_curves() - - def _set_ciphers(self): - self.cipher_algo = { - 'aes-128-cbc': CipherName('aes-128-cbc', self.EVP_aes_128_cbc, 16), - 'aes-256-cbc': CipherName('aes-256-cbc', self.EVP_aes_256_cbc, 16), - 'aes-128-cfb': CipherName('aes-128-cfb', self.EVP_aes_128_cfb128, 16), - 'aes-256-cfb': CipherName('aes-256-cfb', self.EVP_aes_256_cfb128, 16), - 'aes-128-ofb': CipherName('aes-128-ofb', self._lib.EVP_aes_128_ofb, 16), - 'aes-256-ofb': CipherName('aes-256-ofb', self._lib.EVP_aes_256_ofb, 16), - #'aes-128-ctr': CipherName('aes-128-ctr', self._lib.EVP_aes_128_ctr, 16), - #'aes-256-ctr': CipherName('aes-256-ctr', self._lib.EVP_aes_256_ctr, 16), - 'bf-cfb': CipherName('bf-cfb', self.EVP_bf_cfb64, 8), - 'bf-cbc': CipherName('bf-cbc', self.EVP_bf_cbc, 8), - 'rc4': CipherName('rc4', self.EVP_rc4, 128), # 128 is the initialisation size not block size - } - - def _set_curves(self): - self.curves = { - 'secp112r1': 704, - 'secp112r2': 705, - 'secp128r1': 706, - 'secp128r2': 707, - 'secp160k1': 708, - 'secp160r1': 709, - 'secp160r2': 710, - 'secp192k1': 711, - 'secp224k1': 712, - 'secp224r1': 713, - 'secp256k1': 714, - 'secp384r1': 715, - 'secp521r1': 716, - 'sect113r1': 717, - 'sect113r2': 718, - 'sect131r1': 719, - 'sect131r2': 720, - 'sect163k1': 721, - 'sect163r1': 722, - 'sect163r2': 723, - 'sect193r1': 724, - 'sect193r2': 725, - 'sect233k1': 726, - 'sect233r1': 727, - 'sect239k1': 728, - 'sect283k1': 729, - 'sect283r1': 730, - 'sect409k1': 731, - 'sect409r1': 732, - 'sect571k1': 733, - 'sect571r1': 734, - } - - def BN_num_bytes(self, x): - """ - returns the length of a BN (OpenSSl API) - """ - return int((self.BN_num_bits(x) + 7) / 8) - - def get_cipher(self, name): - """ - returns the OpenSSL cipher instance - """ - if name not in self.cipher_algo: - raise Exception("Unknown cipher") - return self.cipher_algo[name] - - def get_curve(self, name): - """ - returns the id of a elliptic curve - """ - if name not in self.curves: - raise Exception("Unknown curve") - return self.curves[name] - - def get_curve_by_id(self, id): - """ - returns the name of a elliptic curve with his id - """ - res = None - for i in self.curves: - if self.curves[i] == id: - res = i - break - if res is None: - raise Exception("Unknown curve") - return res - - def rand(self, size): - """ - OpenSSL random function - """ - buffer = self.malloc(0, size) - # This pyelliptic library, by default, didn't check the return value of RAND_bytes. It is - # evidently possible that it returned an error and not-actually-random data. However, in - # tests on various operating systems, while generating hundreds of gigabytes of random - # strings of various sizes I could not get an error to occur. Also Bitcoin doesn't check - # the return value of RAND_bytes either. - # Fixed in Bitmessage version 0.4.2 (in source code on 2013-10-13) - while self.RAND_bytes(buffer, size) != 1: - import time - time.sleep(1) - return buffer.raw - - def malloc(self, data, size): - """ - returns a create_string_buffer (ctypes) - """ - buffer = None - if data != 0: - if sys.version_info.major == 3 and isinstance(data, type('')): - data = data.encode() - buffer = self.create_string_buffer(data, size) - else: - buffer = self.create_string_buffer(size) - return buffer - -def loadOpenSSL(): - global OpenSSL - from os import path, environ - from ctypes.util import find_library - - libdir = [] - - if 'linux' in sys.platform or 'darwin' in sys.platform or 'bsd' in sys.platform: - libdir.append(find_library('ssl')) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append(find_library('libeay32')) - - if getattr(sys,'frozen', None): - if 'darwin' in sys.platform: - libdir.extend([ - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.1.0.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.2.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.1.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.1.0.0.dylib'), - path.join(environ['RESOURCEPATH'], '..', 'Frameworks','libcrypto.0.9.8.dylib'), - ]) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append(path.join(sys._MEIPASS, 'libeay32.dll')) - else: - libdir.extend([ - path.join(sys._MEIPASS, 'libcrypto.so'), - path.join(sys._MEIPASS, 'libssl.so'), - path.join(sys._MEIPASS, 'libcrypto.so.1.1.0'), - path.join(sys._MEIPASS, 'libssl.so.1.1.0'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.2'), - path.join(sys._MEIPASS, 'libssl.so.1.0.2'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.1'), - path.join(sys._MEIPASS, 'libssl.so.1.0.1'), - path.join(sys._MEIPASS, 'libcrypto.so.1.0.0'), - path.join(sys._MEIPASS, 'libssl.so.1.0.0'), - path.join(sys._MEIPASS, 'libcrypto.so.0.9.8'), - path.join(sys._MEIPASS, 'libssl.so.0.9.8'), - ]) - if 'darwin' in sys.platform: - libdir.extend(['libcrypto.dylib', '/usr/local/opt/openssl/lib/libcrypto.dylib']) - elif 'win32' in sys.platform or 'win64' in sys.platform: - libdir.append('libeay32.dll') - else: - libdir.append('libcrypto.so') - libdir.append('libssl.so') - libdir.append('libcrypto.so.1.0.0') - libdir.append('libssl.so.1.0.0') - for library in libdir: - try: - OpenSSL = _OpenSSL(library) - return - except: - pass - raise Exception("Failed to load OpenSSL library, searched for: " + " ".join(libdir)) - -loadOpenSSL() diff --git a/src/lib/pyelliptic/setup.py b/src/lib/pyelliptic/setup.py deleted file mode 100644 index cc9c0a21..00000000 --- a/src/lib/pyelliptic/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="pyelliptic", - version='2.0.1', - url='https://github.com/radfish/pyelliptic', - license='GPL', - description="Python OpenSSL wrapper for ECC (ECDSA, ECIES), AES, HMAC, Blowfish, ...", - author='Yann GUIBET', - author_email='yannguibet@gmail.com', - maintainer="redfish", - maintainer_email='redfish@galactica.pw', - packages=find_packages(), - classifiers=[ - 'Operating System :: Unix', - 'Operating System :: Microsoft :: Windows', - 'Environment :: MacOS X', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.7', - 'Topic :: Security :: Cryptography', - ], -) diff --git a/src/lib/sslcrypto/LICENSE b/src/lib/sslcrypto/LICENSE new file mode 100644 index 00000000..2feefc45 --- /dev/null +++ b/src/lib/sslcrypto/LICENSE @@ -0,0 +1,27 @@ +MIT License + +Copyright (c) 2019 Ivan Machugovskiy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Additionally, the following licenses must be preserved: + +- ripemd implementation is licensed under BSD-3 by Markus Friedl, see `_ripemd.py`; +- jacobian curve implementation is dual-licensed under MIT or public domain license, see `_jacobian.py`. diff --git a/src/lib/sslcrypto/__init__.py b/src/lib/sslcrypto/__init__.py new file mode 100644 index 00000000..77f9b3f3 --- /dev/null +++ b/src/lib/sslcrypto/__init__.py @@ -0,0 +1,6 @@ +__all__ = ["aes", "ecc", "rsa"] + +try: + from .openssl import aes, ecc, rsa +except OSError: + from .fallback import aes, ecc, rsa diff --git a/src/lib/sslcrypto/_aes.py b/src/lib/sslcrypto/_aes.py new file mode 100644 index 00000000..4f8d4ec2 --- /dev/null +++ b/src/lib/sslcrypto/_aes.py @@ -0,0 +1,53 @@ +# pylint: disable=import-outside-toplevel + +class AES: + def __init__(self, backend, fallback=None): + self._backend = backend + self._fallback = fallback + + + def get_algo_key_length(self, algo): + if algo.count("-") != 2: + raise ValueError("Invalid algorithm name") + try: + return int(algo.split("-")[1]) // 8 + except ValueError: + raise ValueError("Invalid algorithm name") from None + + + def new_key(self, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.new_key(algo) + return self._backend.random(self.get_algo_key_length(algo)) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.encrypt(data, key, algo) + + key_length = self.get_algo_key_length(algo) + if len(key) != key_length: + raise ValueError("Expected key to be {} bytes, got {} bytes".format(key_length, len(key))) + + return self._backend.encrypt(data, key, algo) + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + if not self._backend.is_algo_supported(algo): + if self._fallback is None: + raise ValueError("This algorithm is not supported") + return self._fallback.decrypt(ciphertext, iv, key, algo) + + key_length = self.get_algo_key_length(algo) + if len(key) != key_length: + raise ValueError("Expected key to be {} bytes, got {} bytes".format(key_length, len(key))) + + return self._backend.decrypt(ciphertext, iv, key, algo) + + + def get_backend(self): + return self._backend.get_backend() diff --git a/src/lib/sslcrypto/_ecc.py b/src/lib/sslcrypto/_ecc.py new file mode 100644 index 00000000..9831d688 --- /dev/null +++ b/src/lib/sslcrypto/_ecc.py @@ -0,0 +1,334 @@ +import hashlib +import struct +import hmac +import base58 + + +try: + hashlib.new("ripemd160") +except ValueError: + # No native implementation + from . import _ripemd + def ripemd160(*args): + return _ripemd.new(*args) +else: + # Use OpenSSL + def ripemd160(*args): + return hashlib.new("ripemd160", *args) + + +class ECC: + CURVES = { + "secp112r1": 704, + "secp112r2": 705, + "secp128r1": 706, + "secp128r2": 707, + "secp160k1": 708, + "secp160r1": 709, + "secp160r2": 710, + "secp192k1": 711, + "prime192v1": 409, + "secp224k1": 712, + "secp224r1": 713, + "secp256k1": 714, + "prime256v1": 415, + "secp384r1": 715, + "secp521r1": 716 + } + + def __init__(self, backend, aes): + self._backend = backend + self._aes = aes + + + def get_curve(self, name): + if name not in self.CURVES: + raise ValueError("Unknown curve {}".format(name)) + nid = self.CURVES[name] + return EllipticCurve(self._backend(nid), self._aes, nid) + + + def get_backend(self): + return self._backend.get_backend() + + +class EllipticCurve: + def __init__(self, backend, aes, nid): + self._backend = backend + self._aes = aes + self.nid = nid + + + def _encode_public_key(self, x, y, is_compressed=True, raw=True): + if raw: + if is_compressed: + return bytes([0x02 + (y[-1] % 2)]) + x + else: + return bytes([0x04]) + x + y + else: + return struct.pack("!HH", self.nid, len(x)) + x + struct.pack("!H", len(y)) + y + + + def _decode_public_key(self, public_key, partial=False): + if not public_key: + raise ValueError("No public key") + + if public_key[0] == 0x04: + # Uncompressed + expected_length = 1 + 2 * self._backend.public_key_length + if partial: + if len(public_key) < expected_length: + raise ValueError("Invalid uncompressed public key length") + else: + if len(public_key) != expected_length: + raise ValueError("Invalid uncompressed public key length") + x = public_key[1:1 + self._backend.public_key_length] + y = public_key[1 + self._backend.public_key_length:expected_length] + if partial: + return (x, y), expected_length + else: + return x, y + elif public_key[0] in (0x02, 0x03): + # Compressed + expected_length = 1 + self._backend.public_key_length + if partial: + if len(public_key) < expected_length: + raise ValueError("Invalid compressed public key length") + else: + if len(public_key) != expected_length: + raise ValueError("Invalid compressed public key length") + + x, y = self._backend.decompress_point(public_key[:expected_length]) + # Sanity check + if x != public_key[1:expected_length]: + raise ValueError("Incorrect compressed public key") + if partial: + return (x, y), expected_length + else: + return x, y + else: + raise ValueError("Invalid public key prefix") + + + def _decode_public_key_openssl(self, public_key, partial=False): + if not public_key: + raise ValueError("No public key") + + i = 0 + + nid, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if nid != self.nid: + raise ValueError("Wrong curve") + + xlen, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if len(public_key) - i < xlen: + raise ValueError("Too short public key") + x = public_key[i:i + xlen] + i += xlen + + ylen, = struct.unpack("!H", public_key[i:i + 2]) + i += 2 + if len(public_key) - i < ylen: + raise ValueError("Too short public key") + y = public_key[i:i + ylen] + i += ylen + + if partial: + return (x, y), i + else: + if i < len(public_key): + raise ValueError("Too long public key") + return x, y + + + def new_private_key(self): + return self._backend.new_private_key() + + + def private_to_public(self, private_key, is_compressed=True): + x, y = self._backend.private_to_public(private_key) + return self._encode_public_key(x, y, is_compressed=is_compressed) + + + def private_to_wif(self, private_key): + return base58.b58encode_check(b"\x80" + private_key) + + + def wif_to_private(self, wif): + dec = base58.b58decode_check(wif) + if dec[0] != 0x80: + raise ValueError("Invalid network (expected mainnet)") + return dec[1:] + + + def public_to_address(self, public_key): + h = hashlib.sha256(public_key).digest() + hash160 = ripemd160(h).digest() + return base58.b58encode_check(b"\x00" + hash160) + + + def private_to_address(self, private_key, is_compressed=True): + # Kinda useless but left for quick migration from pybitcointools + return self.public_to_address(self.private_to_public(private_key, is_compressed=is_compressed)) + + + def derive(self, private_key, public_key): + if not isinstance(public_key, tuple): + public_key = self._decode_public_key(public_key) + return self._backend.ecdh(private_key, public_key) + + + def _digest(self, data, hash): + if hash is None: + return data + elif callable(hash): + return hash(data) + elif hash == "sha1": + return hashlib.sha1(data).digest() + elif hash == "sha256": + return hashlib.sha256(data).digest() + elif hash == "sha512": + return hashlib.sha512(data).digest() + else: + raise ValueError("Unknown hash/derivation method") + + + # High-level functions + def encrypt(self, data, public_key, algo="aes-256-cbc", derivation="sha256", mac="hmac-sha256", return_aes_key=False): + # Generate ephemeral private key + private_key = self.new_private_key() + + # Derive key + ecdh = self.derive(private_key, public_key) + key = self._digest(ecdh, derivation) + k_enc_len = self._aes.get_algo_key_length(algo) + if len(key) < k_enc_len: + raise ValueError("Too short digest") + k_enc, k_mac = key[:k_enc_len], key[k_enc_len:] + + # Encrypt + ciphertext, iv = self._aes.encrypt(data, k_enc, algo=algo) + ephem_public_key = self.private_to_public(private_key) + ephem_public_key = self._decode_public_key(ephem_public_key) + ephem_public_key = self._encode_public_key(*ephem_public_key, raw=False) + ciphertext = iv + ephem_public_key + ciphertext + + # Add MAC tag + if callable(mac): + tag = mac(k_mac, ciphertext) + elif mac == "hmac-sha256": + h = hmac.new(k_mac, digestmod="sha256") + h.update(ciphertext) + tag = h.digest() + elif mac == "hmac-sha512": + h = hmac.new(k_mac, digestmod="sha512") + h.update(ciphertext) + tag = h.digest() + elif mac is None: + tag = b"" + else: + raise ValueError("Unsupported MAC") + + if return_aes_key: + return ciphertext + tag, k_enc + else: + return ciphertext + tag + + + def decrypt(self, ciphertext, private_key, algo="aes-256-cbc", derivation="sha256", mac="hmac-sha256"): + # Get MAC tag + if callable(mac): + tag_length = mac.digest_size + elif mac == "hmac-sha256": + tag_length = hmac.new(b"", digestmod="sha256").digest_size + elif mac == "hmac-sha512": + tag_length = hmac.new(b"", digestmod="sha512").digest_size + elif mac is None: + tag_length = 0 + else: + raise ValueError("Unsupported MAC") + + if len(ciphertext) < tag_length: + raise ValueError("Ciphertext is too small to contain MAC tag") + if tag_length == 0: + tag = b"" + else: + ciphertext, tag = ciphertext[:-tag_length], ciphertext[-tag_length:] + + orig_ciphertext = ciphertext + + if len(ciphertext) < 16: + raise ValueError("Ciphertext is too small to contain IV") + iv, ciphertext = ciphertext[:16], ciphertext[16:] + + public_key, pos = self._decode_public_key_openssl(ciphertext, partial=True) + ciphertext = ciphertext[pos:] + + # Derive key + ecdh = self.derive(private_key, public_key) + key = self._digest(ecdh, derivation) + k_enc_len = self._aes.get_algo_key_length(algo) + if len(key) < k_enc_len: + raise ValueError("Too short digest") + k_enc, k_mac = key[:k_enc_len], key[k_enc_len:] + + # Verify MAC tag + if callable(mac): + expected_tag = mac(k_mac, orig_ciphertext) + elif mac == "hmac-sha256": + h = hmac.new(k_mac, digestmod="sha256") + h.update(orig_ciphertext) + expected_tag = h.digest() + elif mac == "hmac-sha512": + h = hmac.new(k_mac, digestmod="sha512") + h.update(orig_ciphertext) + expected_tag = h.digest() + elif mac is None: + expected_tag = b"" + + if not hmac.compare_digest(tag, expected_tag): + raise ValueError("Invalid MAC tag") + + return self._aes.decrypt(ciphertext, iv, k_enc, algo=algo) + + + def sign(self, data, private_key, hash="sha256", recoverable=False, is_compressed=True, entropy=None): + data = self._digest(data, hash) + if not entropy: + v = b"\x01" * len(data) + k = b"\x00" * len(data) + k = hmac.new(k, v + b"\x00" + private_key + data, "sha256").digest() + v = hmac.new(k, v, "sha256").digest() + k = hmac.new(k, v + b"\x01" + private_key + data, "sha256").digest() + v = hmac.new(k, v, "sha256").digest() + entropy = hmac.new(k, v, "sha256").digest() + return self._backend.sign(data, private_key, recoverable, is_compressed, entropy=entropy) + + + def recover(self, signature, data, hash="sha256"): + # Sanity check: is this signature recoverable? + if len(signature) != 1 + 2 * self._backend.public_key_length: + raise ValueError("Cannot recover an unrecoverable signature") + x, y = self._backend.recover(signature, self._digest(data, hash)) + is_compressed = signature[0] >= 31 + return self._encode_public_key(x, y, is_compressed=is_compressed) + + + def verify(self, signature, data, public_key, hash="sha256"): + if len(signature) == 1 + 2 * self._backend.public_key_length: + # Recoverable signature + signature = signature[1:] + if len(signature) != 2 * self._backend.public_key_length: + raise ValueError("Invalid signature format") + if not isinstance(public_key, tuple): + public_key = self._decode_public_key(public_key) + return self._backend.verify(signature, self._digest(data, hash), public_key) + + + def derive_child(self, seed, child): + # Based on BIP32 + if not 0 <= child < 2 ** 31: + raise ValueError("Invalid child index") + return self._backend.derive_child(seed, child) diff --git a/src/lib/sslcrypto/_ripemd.py b/src/lib/sslcrypto/_ripemd.py new file mode 100644 index 00000000..89377cc2 --- /dev/null +++ b/src/lib/sslcrypto/_ripemd.py @@ -0,0 +1,375 @@ +# Copyright (c) 2001 Markus Friedl. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# pylint: skip-file + +import sys + +digest_size = 20 +digestsize = 20 + +class RIPEMD160: + """ + Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed. + """ + + def __init__(self, arg=None): + self.ctx = RMDContext() + if arg: + self.update(arg) + self.dig = None + + def update(self, arg): + RMD160Update(self.ctx, arg, len(arg)) + self.dig = None + + def digest(self): + if self.dig: + return self.dig + ctx = self.ctx.copy() + self.dig = RMD160Final(self.ctx) + self.ctx = ctx + return self.dig + + def hexdigest(self): + dig = self.digest() + hex_digest = "" + for d in dig: + hex_digest += "%02x" % d + return hex_digest + + def copy(self): + import copy + return copy.deepcopy(self) + + + +def new(arg=None): + """ + Return a new RIPEMD160 object. An optional string argument + may be provided; if present, this string will be automatically + hashed. + """ + return RIPEMD160(arg) + + + +# +# Private. +# + +class RMDContext: + def __init__(self): + self.state = [0x67452301, 0xEFCDAB89, 0x98BADCFE, + 0x10325476, 0xC3D2E1F0] # uint32 + self.count = 0 # uint64 + self.buffer = [0] * 64 # uchar + def copy(self): + ctx = RMDContext() + ctx.state = self.state[:] + ctx.count = self.count + ctx.buffer = self.buffer[:] + return ctx + +K0 = 0x00000000 +K1 = 0x5A827999 +K2 = 0x6ED9EBA1 +K3 = 0x8F1BBCDC +K4 = 0xA953FD4E + +KK0 = 0x50A28BE6 +KK1 = 0x5C4DD124 +KK2 = 0x6D703EF3 +KK3 = 0x7A6D76E9 +KK4 = 0x00000000 + +def ROL(n, x): + return ((x << n) & 0xffffffff) | (x >> (32 - n)) + +def F0(x, y, z): + return x ^ y ^ z + +def F1(x, y, z): + return (x & y) | (((~x) % 0x100000000) & z) + +def F2(x, y, z): + return (x | ((~y) % 0x100000000)) ^ z + +def F3(x, y, z): + return (x & z) | (((~z) % 0x100000000) & y) + +def F4(x, y, z): + return x ^ (y | ((~z) % 0x100000000)) + +def R(a, b, c, d, e, Fj, Kj, sj, rj, X): + a = ROL(sj, (a + Fj(b, c, d) + X[rj] + Kj) % 0x100000000) + e + c = ROL(10, c) + return a % 0x100000000, c + +PADDING = [0x80] + [0] * 63 + +import sys +import struct + +def RMD160Transform(state, block): # uint32 state[5], uchar block[64] + x = [0] * 16 + if sys.byteorder == "little": + x = struct.unpack("<16L", bytes(block[0:64])) + else: + raise ValueError("Big-endian platforms are not supported") + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + # Round 1 + a, c = R(a, b, c, d, e, F0, K0, 11, 0, x) + e, b = R(e, a, b, c, d, F0, K0, 14, 1, x) + d, a = R(d, e, a, b, c, F0, K0, 15, 2, x) + c, e = R(c, d, e, a, b, F0, K0, 12, 3, x) + b, d = R(b, c, d, e, a, F0, K0, 5, 4, x) + a, c = R(a, b, c, d, e, F0, K0, 8, 5, x) + e, b = R(e, a, b, c, d, F0, K0, 7, 6, x) + d, a = R(d, e, a, b, c, F0, K0, 9, 7, x) + c, e = R(c, d, e, a, b, F0, K0, 11, 8, x) + b, d = R(b, c, d, e, a, F0, K0, 13, 9, x) + a, c = R(a, b, c, d, e, F0, K0, 14, 10, x) + e, b = R(e, a, b, c, d, F0, K0, 15, 11, x) + d, a = R(d, e, a, b, c, F0, K0, 6, 12, x) + c, e = R(c, d, e, a, b, F0, K0, 7, 13, x) + b, d = R(b, c, d, e, a, F0, K0, 9, 14, x) + a, c = R(a, b, c, d, e, F0, K0, 8, 15, x) # #15 + # Round 2 + e, b = R(e, a, b, c, d, F1, K1, 7, 7, x) + d, a = R(d, e, a, b, c, F1, K1, 6, 4, x) + c, e = R(c, d, e, a, b, F1, K1, 8, 13, x) + b, d = R(b, c, d, e, a, F1, K1, 13, 1, x) + a, c = R(a, b, c, d, e, F1, K1, 11, 10, x) + e, b = R(e, a, b, c, d, F1, K1, 9, 6, x) + d, a = R(d, e, a, b, c, F1, K1, 7, 15, x) + c, e = R(c, d, e, a, b, F1, K1, 15, 3, x) + b, d = R(b, c, d, e, a, F1, K1, 7, 12, x) + a, c = R(a, b, c, d, e, F1, K1, 12, 0, x) + e, b = R(e, a, b, c, d, F1, K1, 15, 9, x) + d, a = R(d, e, a, b, c, F1, K1, 9, 5, x) + c, e = R(c, d, e, a, b, F1, K1, 11, 2, x) + b, d = R(b, c, d, e, a, F1, K1, 7, 14, x) + a, c = R(a, b, c, d, e, F1, K1, 13, 11, x) + e, b = R(e, a, b, c, d, F1, K1, 12, 8, x) # #31 + # Round 3 + d, a = R(d, e, a, b, c, F2, K2, 11, 3, x) + c, e = R(c, d, e, a, b, F2, K2, 13, 10, x) + b, d = R(b, c, d, e, a, F2, K2, 6, 14, x) + a, c = R(a, b, c, d, e, F2, K2, 7, 4, x) + e, b = R(e, a, b, c, d, F2, K2, 14, 9, x) + d, a = R(d, e, a, b, c, F2, K2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, K2, 13, 8, x) + b, d = R(b, c, d, e, a, F2, K2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, K2, 14, 2, x) + e, b = R(e, a, b, c, d, F2, K2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, K2, 13, 0, x) + c, e = R(c, d, e, a, b, F2, K2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, K2, 5, 13, x) + a, c = R(a, b, c, d, e, F2, K2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, K2, 7, 5, x) + d, a = R(d, e, a, b, c, F2, K2, 5, 12, x) # #47 + # Round 4 + c, e = R(c, d, e, a, b, F3, K3, 11, 1, x) + b, d = R(b, c, d, e, a, F3, K3, 12, 9, x) + a, c = R(a, b, c, d, e, F3, K3, 14, 11, x) + e, b = R(e, a, b, c, d, F3, K3, 15, 10, x) + d, a = R(d, e, a, b, c, F3, K3, 14, 0, x) + c, e = R(c, d, e, a, b, F3, K3, 15, 8, x) + b, d = R(b, c, d, e, a, F3, K3, 9, 12, x) + a, c = R(a, b, c, d, e, F3, K3, 8, 4, x) + e, b = R(e, a, b, c, d, F3, K3, 9, 13, x) + d, a = R(d, e, a, b, c, F3, K3, 14, 3, x) + c, e = R(c, d, e, a, b, F3, K3, 5, 7, x) + b, d = R(b, c, d, e, a, F3, K3, 6, 15, x) + a, c = R(a, b, c, d, e, F3, K3, 8, 14, x) + e, b = R(e, a, b, c, d, F3, K3, 6, 5, x) + d, a = R(d, e, a, b, c, F3, K3, 5, 6, x) + c, e = R(c, d, e, a, b, F3, K3, 12, 2, x) # #63 + # Round 5 + b, d = R(b, c, d, e, a, F4, K4, 9, 4, x) + a, c = R(a, b, c, d, e, F4, K4, 15, 0, x) + e, b = R(e, a, b, c, d, F4, K4, 5, 5, x) + d, a = R(d, e, a, b, c, F4, K4, 11, 9, x) + c, e = R(c, d, e, a, b, F4, K4, 6, 7, x) + b, d = R(b, c, d, e, a, F4, K4, 8, 12, x) + a, c = R(a, b, c, d, e, F4, K4, 13, 2, x) + e, b = R(e, a, b, c, d, F4, K4, 12, 10, x) + d, a = R(d, e, a, b, c, F4, K4, 5, 14, x) + c, e = R(c, d, e, a, b, F4, K4, 12, 1, x) + b, d = R(b, c, d, e, a, F4, K4, 13, 3, x) + a, c = R(a, b, c, d, e, F4, K4, 14, 8, x) + e, b = R(e, a, b, c, d, F4, K4, 11, 11, x) + d, a = R(d, e, a, b, c, F4, K4, 8, 6, x) + c, e = R(c, d, e, a, b, F4, K4, 5, 15, x) + b, d = R(b, c, d, e, a, F4, K4, 6, 13, x) # #79 + + aa = a + bb = b + cc = c + dd = d + ee = e + + a = state[0] + b = state[1] + c = state[2] + d = state[3] + e = state[4] + + # Parallel round 1 + a, c = R(a, b, c, d, e, F4, KK0, 8, 5, x) + e, b = R(e, a, b, c, d, F4, KK0, 9, 14, x) + d, a = R(d, e, a, b, c, F4, KK0, 9, 7, x) + c, e = R(c, d, e, a, b, F4, KK0, 11, 0, x) + b, d = R(b, c, d, e, a, F4, KK0, 13, 9, x) + a, c = R(a, b, c, d, e, F4, KK0, 15, 2, x) + e, b = R(e, a, b, c, d, F4, KK0, 15, 11, x) + d, a = R(d, e, a, b, c, F4, KK0, 5, 4, x) + c, e = R(c, d, e, a, b, F4, KK0, 7, 13, x) + b, d = R(b, c, d, e, a, F4, KK0, 7, 6, x) + a, c = R(a, b, c, d, e, F4, KK0, 8, 15, x) + e, b = R(e, a, b, c, d, F4, KK0, 11, 8, x) + d, a = R(d, e, a, b, c, F4, KK0, 14, 1, x) + c, e = R(c, d, e, a, b, F4, KK0, 14, 10, x) + b, d = R(b, c, d, e, a, F4, KK0, 12, 3, x) + a, c = R(a, b, c, d, e, F4, KK0, 6, 12, x) # #15 + # Parallel round 2 + e, b = R(e, a, b, c, d, F3, KK1, 9, 6, x) + d, a = R(d, e, a, b, c, F3, KK1, 13, 11, x) + c, e = R(c, d, e, a, b, F3, KK1, 15, 3, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 7, x) + a, c = R(a, b, c, d, e, F3, KK1, 12, 0, x) + e, b = R(e, a, b, c, d, F3, KK1, 8, 13, x) + d, a = R(d, e, a, b, c, F3, KK1, 9, 5, x) + c, e = R(c, d, e, a, b, F3, KK1, 11, 10, x) + b, d = R(b, c, d, e, a, F3, KK1, 7, 14, x) + a, c = R(a, b, c, d, e, F3, KK1, 7, 15, x) + e, b = R(e, a, b, c, d, F3, KK1, 12, 8, x) + d, a = R(d, e, a, b, c, F3, KK1, 7, 12, x) + c, e = R(c, d, e, a, b, F3, KK1, 6, 4, x) + b, d = R(b, c, d, e, a, F3, KK1, 15, 9, x) + a, c = R(a, b, c, d, e, F3, KK1, 13, 1, x) + e, b = R(e, a, b, c, d, F3, KK1, 11, 2, x) # #31 + # Parallel round 3 + d, a = R(d, e, a, b, c, F2, KK2, 9, 15, x) + c, e = R(c, d, e, a, b, F2, KK2, 7, 5, x) + b, d = R(b, c, d, e, a, F2, KK2, 15, 1, x) + a, c = R(a, b, c, d, e, F2, KK2, 11, 3, x) + e, b = R(e, a, b, c, d, F2, KK2, 8, 7, x) + d, a = R(d, e, a, b, c, F2, KK2, 6, 14, x) + c, e = R(c, d, e, a, b, F2, KK2, 6, 6, x) + b, d = R(b, c, d, e, a, F2, KK2, 14, 9, x) + a, c = R(a, b, c, d, e, F2, KK2, 12, 11, x) + e, b = R(e, a, b, c, d, F2, KK2, 13, 8, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 12, x) + c, e = R(c, d, e, a, b, F2, KK2, 14, 2, x) + b, d = R(b, c, d, e, a, F2, KK2, 13, 10, x) + a, c = R(a, b, c, d, e, F2, KK2, 13, 0, x) + e, b = R(e, a, b, c, d, F2, KK2, 7, 4, x) + d, a = R(d, e, a, b, c, F2, KK2, 5, 13, x) # #47 + # Parallel round 4 + c, e = R(c, d, e, a, b, F1, KK3, 15, 8, x) + b, d = R(b, c, d, e, a, F1, KK3, 5, 6, x) + a, c = R(a, b, c, d, e, F1, KK3, 8, 4, x) + e, b = R(e, a, b, c, d, F1, KK3, 11, 1, x) + d, a = R(d, e, a, b, c, F1, KK3, 14, 3, x) + c, e = R(c, d, e, a, b, F1, KK3, 14, 11, x) + b, d = R(b, c, d, e, a, F1, KK3, 6, 15, x) + a, c = R(a, b, c, d, e, F1, KK3, 14, 0, x) + e, b = R(e, a, b, c, d, F1, KK3, 6, 5, x) + d, a = R(d, e, a, b, c, F1, KK3, 9, 12, x) + c, e = R(c, d, e, a, b, F1, KK3, 12, 2, x) + b, d = R(b, c, d, e, a, F1, KK3, 9, 13, x) + a, c = R(a, b, c, d, e, F1, KK3, 12, 9, x) + e, b = R(e, a, b, c, d, F1, KK3, 5, 7, x) + d, a = R(d, e, a, b, c, F1, KK3, 15, 10, x) + c, e = R(c, d, e, a, b, F1, KK3, 8, 14, x) # #63 + # Parallel round 5 + b, d = R(b, c, d, e, a, F0, KK4, 8, 12, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 15, x) + e, b = R(e, a, b, c, d, F0, KK4, 12, 10, x) + d, a = R(d, e, a, b, c, F0, KK4, 9, 4, x) + c, e = R(c, d, e, a, b, F0, KK4, 12, 1, x) + b, d = R(b, c, d, e, a, F0, KK4, 5, 5, x) + a, c = R(a, b, c, d, e, F0, KK4, 14, 8, x) + e, b = R(e, a, b, c, d, F0, KK4, 6, 7, x) + d, a = R(d, e, a, b, c, F0, KK4, 8, 6, x) + c, e = R(c, d, e, a, b, F0, KK4, 13, 2, x) + b, d = R(b, c, d, e, a, F0, KK4, 6, 13, x) + a, c = R(a, b, c, d, e, F0, KK4, 5, 14, x) + e, b = R(e, a, b, c, d, F0, KK4, 15, 0, x) + d, a = R(d, e, a, b, c, F0, KK4, 13, 3, x) + c, e = R(c, d, e, a, b, F0, KK4, 11, 9, x) + b, d = R(b, c, d, e, a, F0, KK4, 11, 11, x) # #79 + + t = (state[1] + cc + d) % 0x100000000 + state[1] = (state[2] + dd + e) % 0x100000000 + state[2] = (state[3] + ee + a) % 0x100000000 + state[3] = (state[4] + aa + b) % 0x100000000 + state[4] = (state[0] + bb + c) % 0x100000000 + state[0] = t % 0x100000000 + + +def RMD160Update(ctx, inp, inplen): + if type(inp) == str: + inp = [ord(i)&0xff for i in inp] + + have = int((ctx.count // 8) % 64) + inplen = int(inplen) + need = 64 - have + ctx.count += 8 * inplen + off = 0 + if inplen >= need: + if have: + for i in range(need): + ctx.buffer[have + i] = inp[i] + RMD160Transform(ctx.state, ctx.buffer) + off = need + have = 0 + while off + 64 <= inplen: + RMD160Transform(ctx.state, inp[off:]) #<--- + off += 64 + if off < inplen: + # memcpy(ctx->buffer + have, input+off, len-off) + for i in range(inplen - off): + ctx.buffer[have + i] = inp[off + i] + +def RMD160Final(ctx): + size = struct.pack("= self.n: + return self.jacobian_multiply(a, n % self.n, secret) + half = self.jacobian_multiply(a, n // 2, secret) + half_sq = self.jacobian_double(half) + if secret: + # A constant-time implementation + half_sq_a = self.jacobian_add(half_sq, a) + if n % 2 == 0: + result = half_sq + if n % 2 == 1: + result = half_sq_a + return result + else: + if n % 2 == 0: + return half_sq + return self.jacobian_add(half_sq, a) + + + def jacobian_shamir(self, a, n, b, m): + ab = self.jacobian_add(a, b) + if n < 0 or n >= self.n: + n %= self.n + if m < 0 or m >= self.n: + m %= self.n + res = 0, 0, 1 # point on infinity + for i in range(self.n_length - 1, -1, -1): + res = self.jacobian_double(res) + has_n = n & (1 << i) + has_m = m & (1 << i) + if has_n: + if has_m == 0: + res = self.jacobian_add(res, a) + if has_m != 0: + res = self.jacobian_add(res, ab) + else: + if has_m == 0: + res = self.jacobian_add(res, (0, 0, 1)) # Try not to leak + if has_m != 0: + res = self.jacobian_add(res, b) + return res + + + def fast_multiply(self, a, n, secret=False): + return self.from_jacobian(self.jacobian_multiply(self.to_jacobian(a), n, secret)) + + + def fast_add(self, a, b): + return self.from_jacobian(self.jacobian_add(self.to_jacobian(a), self.to_jacobian(b))) + + + def fast_shamir(self, a, n, b, m): + return self.from_jacobian(self.jacobian_shamir(self.to_jacobian(a), n, self.to_jacobian(b), m)) + + + def is_on_curve(self, a): + x, y = a + # Simple arithmetic check + if (pow(x, 3, self.p) + self.a * x + self.b) % self.p != y * y % self.p: + return False + # nP = point-at-infinity + return self.isinf(self.jacobian_multiply(self.to_jacobian(a), self.n)) diff --git a/src/lib/sslcrypto/fallback/_util.py b/src/lib/sslcrypto/fallback/_util.py new file mode 100644 index 00000000..2236ebee --- /dev/null +++ b/src/lib/sslcrypto/fallback/_util.py @@ -0,0 +1,79 @@ +def int_to_bytes(raw, length): + data = [] + for _ in range(length): + data.append(raw % 256) + raw //= 256 + return bytes(data[::-1]) + + +def bytes_to_int(data): + raw = 0 + for byte in data: + raw = raw * 256 + byte + return raw + + +def legendre(a, p): + res = pow(a, (p - 1) // 2, p) + if res == p - 1: + return -1 + else: + return res + + +def inverse(a, n): + if a == 0: + return 0 + lm, hm = 1, 0 + low, high = a % n, n + while low > 1: + r = high // low + nm, new = hm - lm * r, high - low * r + lm, low, hm, high = nm, new, lm, low + return lm % n + + +def square_root_mod_prime(n, p): + if n == 0: + return 0 + if p == 2: + return n # We should never get here but it might be useful + if legendre(n, p) != 1: + raise ValueError("No square root") + # Optimizations + if p % 4 == 3: + return pow(n, (p + 1) // 4, p) + # 1. By factoring out powers of 2, find Q and S such that p - 1 = + # Q * 2 ** S with Q odd + q = p - 1 + s = 0 + while q % 2 == 0: + q //= 2 + s += 1 + # 2. Search for z in Z/pZ which is a quadratic non-residue + z = 1 + while legendre(z, p) != -1: + z += 1 + m, c, t, r = s, pow(z, q, p), pow(n, q, p), pow(n, (q + 1) // 2, p) + while True: + if t == 0: + return 0 + elif t == 1: + return r + # Use repeated squaring to find the least i, 0 < i < M, such + # that t ** (2 ** i) = 1 + t_sq = t + i = 0 + for i in range(1, m): + t_sq = t_sq * t_sq % p + if t_sq == 1: + break + else: + raise ValueError("Should never get here") + # Let b = c ** (2 ** (m - i - 1)) + b = pow(c, 2 ** (m - i - 1), p) + m = i + c = b * b % p + t = t * b * b % p + r = r * b % p + return r diff --git a/src/lib/sslcrypto/fallback/aes.py b/src/lib/sslcrypto/fallback/aes.py new file mode 100644 index 00000000..e168bf34 --- /dev/null +++ b/src/lib/sslcrypto/fallback/aes.py @@ -0,0 +1,101 @@ +import os +import pyaes +from .._aes import AES + + +__all__ = ["aes"] + +class AESBackend: + def _get_algo_cipher_type(self, algo): + if not algo.startswith("aes-") or algo.count("-") != 2: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + key_length, cipher_type = algo[4:].split("-") + if key_length not in ("128", "192", "256"): + raise ValueError("Unknown cipher algorithm {}".format(algo)) + if cipher_type not in ("cbc", "ctr", "cfb", "ofb"): + raise ValueError("Unknown cipher algorithm {}".format(algo)) + return cipher_type + + + def is_algo_supported(self, algo): + try: + self._get_algo_cipher_type(algo) + return True + except ValueError: + return False + + + def random(self, length): + return os.urandom(length) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + cipher_type = self._get_algo_cipher_type(algo) + + # Generate random IV + iv = os.urandom(16) + + if cipher_type == "cbc": + cipher = pyaes.AESModeOfOperationCBC(key, iv=iv) + elif cipher_type == "ctr": + # The IV is actually a counter, not an IV but it does almost the + # same. Notice: pyaes always uses 1 as initial counter! Make sure + # not to call pyaes directly. + + # We kinda do two conversions here: from byte array to int here, and + # from int to byte array in pyaes internals. It's possible to fix that + # but I didn't notice any performance changes so I'm keeping clean code. + iv_int = 0 + for byte in iv: + iv_int = (iv_int * 256) + byte + counter = pyaes.Counter(iv_int) + cipher = pyaes.AESModeOfOperationCTR(key, counter=counter) + elif cipher_type == "cfb": + # Change segment size from default 8 bytes to 16 bytes for OpenSSL + # compatibility + cipher = pyaes.AESModeOfOperationCFB(key, iv, segment_size=16) + elif cipher_type == "ofb": + cipher = pyaes.AESModeOfOperationOFB(key, iv) + + encrypter = pyaes.Encrypter(cipher) + ciphertext = encrypter.feed(data) + ciphertext += encrypter.feed() + return ciphertext, iv + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + cipher_type = self._get_algo_cipher_type(algo) + + if cipher_type == "cbc": + cipher = pyaes.AESModeOfOperationCBC(key, iv=iv) + elif cipher_type == "ctr": + # The IV is actually a counter, not an IV but it does almost the + # same. Notice: pyaes always uses 1 as initial counter! Make sure + # not to call pyaes directly. + + # We kinda do two conversions here: from byte array to int here, and + # from int to byte array in pyaes internals. It's possible to fix that + # but I didn't notice any performance changes so I'm keeping clean code. + iv_int = 0 + for byte in iv: + iv_int = (iv_int * 256) + byte + counter = pyaes.Counter(iv_int) + cipher = pyaes.AESModeOfOperationCTR(key, counter=counter) + elif cipher_type == "cfb": + # Change segment size from default 8 bytes to 16 bytes for OpenSSL + # compatibility + cipher = pyaes.AESModeOfOperationCFB(key, iv, segment_size=16) + elif cipher_type == "ofb": + cipher = pyaes.AESModeOfOperationOFB(key, iv) + + decrypter = pyaes.Decrypter(cipher) + data = decrypter.feed(ciphertext) + data += decrypter.feed() + return data + + + def get_backend(self): + return "fallback" + + +aes = AES(AESBackend()) diff --git a/src/lib/sslcrypto/fallback/ecc.py b/src/lib/sslcrypto/fallback/ecc.py new file mode 100644 index 00000000..3a438d4d --- /dev/null +++ b/src/lib/sslcrypto/fallback/ecc.py @@ -0,0 +1,371 @@ +import hmac +import os +from ._jacobian import JacobianCurve +from .._ecc import ECC +from .aes import aes +from ._util import int_to_bytes, bytes_to_int, inverse, square_root_mod_prime + + +# pylint: disable=line-too-long +CURVES = { + # nid: (p, n, a, b, (Gx, Gy)), + 704: ( + # secp112r1 + 0xDB7C2ABF62E35E668076BEAD208B, + 0xDB7C2ABF62E35E7628DFAC6561C5, + 0xDB7C2ABF62E35E668076BEAD2088, + 0x659EF8BA043916EEDE8911702B22, + ( + 0x09487239995A5EE76B55F9C2F098, + 0xA89CE5AF8724C0A23E0E0FF77500 + ) + ), + 705: ( + # secp112r2 + 0xDB7C2ABF62E35E668076BEAD208B, + 0x36DF0AAFD8B8D7597CA10520D04B, + 0x6127C24C05F38A0AAAF65C0EF02C, + 0x51DEF1815DB5ED74FCC34C85D709, + ( + 0x4BA30AB5E892B4E1649DD0928643, + 0xADCD46F5882E3747DEF36E956E97 + ) + ), + 706: ( + # secp128r1 + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFE0000000075A30D1B9038A115, + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFC, + 0xE87579C11079F43DD824993C2CEE5ED3, + ( + 0x161FF7528B899B2D0C28607CA52C5B86, + 0xCF5AC8395BAFEB13C02DA292DDED7A83 + ) + ), + 707: ( + # secp128r2 + 0xFFFFFFFDFFFFFFFFFFFFFFFFFFFFFFFF, + 0x3FFFFFFF7FFFFFFFBE0024720613B5A3, + 0xD6031998D1B3BBFEBF59CC9BBFF9AEE1, + 0x5EEEFCA380D02919DC2C6558BB6D8A5D, + ( + 0x7B6AA5D85E572983E6FB32A7CDEBC140, + 0x27B6916A894D3AEE7106FE805FC34B44 + ) + ), + 708: ( + # secp160k1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, + 0x0100000000000000000001B8FA16DFAB9ACA16B6B3, + 0, + 7, + ( + 0x3B4C382CE37AA192A4019E763036F4F5DD4D7EBB, + 0x938CF935318FDCED6BC28286531733C3F03C4FEE + ) + ), + 709: ( + # secp160r1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFF, + 0x0100000000000000000001F4C8F927AED3CA752257, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7FFFFFFC, + 0x001C97BEFC54BD7A8B65ACF89F81D4D4ADC565FA45, + ( + 0x4A96B5688EF573284664698968C38BB913CBFC82, + 0x23A628553168947D59DCC912042351377AC5FB32 + ) + ), + 710: ( + # secp160r2 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC73, + 0x0100000000000000000000351EE786A818F3A1A16B, + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFAC70, + 0x00B4E134D3FB59EB8BAB57274904664D5AF50388BA, + ( + 0x52DCB034293A117E1F4FF11B30F7199D3144CE6D, + 0xFEAFFEF2E331F296E071FA0DF9982CFEA7D43F2E + ) + ), + 711: ( + # secp192k1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFEE37, + 0xFFFFFFFFFFFFFFFFFFFFFFFE26F2FC170F69466A74DEFD8D, + 0, + 3, + ( + 0xDB4FF10EC057E9AE26B07D0280B7F4341DA5D1B1EAE06C7D, + 0x9B2F2F6D9C5628A7844163D015BE86344082AA88D95E2F9D + ) + ), + 409: ( + # prime192v1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFF99DEF836146BC9B1B4D22831, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFC, + 0x64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1, + ( + 0x188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012, + 0x07192B95FFC8DA78631011ED6B24CDD573F977A11E794811 + ) + ), + 712: ( + # secp224k1 + 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFE56D, + 0x010000000000000000000000000001DCE8D2EC6184CAF0A971769FB1F7, + 0, + 5, + ( + 0xA1455B334DF099DF30FC28A169A467E9E47075A90F7E650EB6B7A45C, + 0x7E089FED7FBA344282CAFBD6F7E319F7C0B0BD59E2CA4BDB556D61A5 + ) + ), + 713: ( + # secp224r1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000001, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFF16A2E0B8F03E13DD29455C5C2A3D, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFE, + 0xB4050A850C04B3ABF54132565044B0B7D7BFD8BA270B39432355FFB4, + ( + 0xB70E0CBD6BB4BF7F321390B94A03C1D356C21122343280D6115C1D21, + 0xBD376388B5F723FB4C22DFE6CD4375A05A07476444D5819985007E34 + ) + ), + 714: ( + # secp256k1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141, + 0, + 7, + ( + 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, + 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 + ) + ), + 415: ( + # prime256v1 + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF, + 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551, + 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC, + 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B, + ( + 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296, + 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5 + ) + ), + 715: ( + # secp384r1 + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973, + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC, + 0xB3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF, + ( + 0xAA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7, + 0x3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F + ) + ), + 716: ( + # secp521r1 + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409, + 0x01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC, + 0x0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00, + ( + 0x00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66, + 0x011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650 + ) + ) +} +# pylint: enable=line-too-long + + +class EllipticCurveBackend: + def __init__(self, nid): + self.p, self.n, self.a, self.b, self.g = CURVES[nid] + self.jacobian = JacobianCurve(*CURVES[nid]) + + self.public_key_length = (len(bin(self.p).replace("0b", "")) + 7) // 8 + self.order_bitlength = len(bin(self.n).replace("0b", "")) + + + def _int_to_bytes(self, raw, len=None): + return int_to_bytes(raw, len or self.public_key_length) + + + def decompress_point(self, public_key): + # Parse & load data + x = bytes_to_int(public_key[1:]) + # Calculate Y + y_square = (pow(x, 3, self.p) + self.a * x + self.b) % self.p + try: + y = square_root_mod_prime(y_square, self.p) + except Exception: + raise ValueError("Invalid public key") from None + if y % 2 != public_key[0] - 0x02: + y = self.p - y + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def new_private_key(self): + while True: + private_key = os.urandom(self.public_key_length) + if bytes_to_int(private_key) >= self.n: + continue + return private_key + + + def private_to_public(self, private_key): + raw = bytes_to_int(private_key) + x, y = self.jacobian.fast_multiply(self.g, raw) + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def ecdh(self, private_key, public_key): + x, y = public_key + x, y = bytes_to_int(x), bytes_to_int(y) + private_key = bytes_to_int(private_key) + x, _ = self.jacobian.fast_multiply((x, y), private_key, secret=True) + return self._int_to_bytes(x) + + + def _subject_to_int(self, subject): + return bytes_to_int(subject[:(self.order_bitlength + 7) // 8]) + + + def sign(self, subject, raw_private_key, recoverable, is_compressed, entropy): + z = self._subject_to_int(subject) + private_key = bytes_to_int(raw_private_key) + k = bytes_to_int(entropy) + + # Fix k length to prevent Minerva. Increasing multiplier by a + # multiple of order doesn't break anything. This fix was ported + # from python-ecdsa + ks = k + self.n + kt = ks + self.n + ks_len = len(bin(ks).replace("0b", "")) // 8 + kt_len = len(bin(kt).replace("0b", "")) // 8 + if ks_len == kt_len: + k = kt + else: + k = ks + px, py = self.jacobian.fast_multiply(self.g, k, secret=True) + + r = px % self.n + if r == 0: + # Invalid k + raise ValueError("Invalid k") + + s = (inverse(k, self.n) * (z + (private_key * r))) % self.n + if s == 0: + # Invalid k + raise ValueError("Invalid k") + + inverted = False + if s * 2 >= self.n: + s = self.n - s + inverted = True + rs_buf = self._int_to_bytes(r) + self._int_to_bytes(s) + + if recoverable: + recid = (py % 2) ^ inverted + recid += 2 * int(px // self.n) + if is_compressed: + return bytes([31 + recid]) + rs_buf + else: + if recid >= 4: + raise ValueError("Too big recovery ID, use compressed address instead") + return bytes([27 + recid]) + rs_buf + else: + return rs_buf + + + def recover(self, signature, subject): + z = self._subject_to_int(subject) + + recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 + r = bytes_to_int(signature[1:self.public_key_length + 1]) + s = bytes_to_int(signature[self.public_key_length + 1:]) + + # Verify bounds + if not 0 <= recid < 2 * (self.p // self.n + 1): + raise ValueError("Invalid recovery ID") + if r >= self.n: + raise ValueError("r is out of bounds") + if s >= self.n: + raise ValueError("s is out of bounds") + + rinv = inverse(r, self.n) + u1 = (-z * rinv) % self.n + u2 = (s * rinv) % self.n + + # Recover R + rx = r + (recid // 2) * self.n + if rx >= self.p: + raise ValueError("Rx is out of bounds") + + # Almost copied from decompress_point + ry_square = (pow(rx, 3, self.p) + self.a * rx + self.b) % self.p + try: + ry = square_root_mod_prime(ry_square, self.p) + except Exception: + raise ValueError("Invalid recovered public key") from None + + # Ensure the point is correct + if ry % 2 != recid % 2: + # Fix Ry sign + ry = self.p - ry + + x, y = self.jacobian.fast_shamir(self.g, u1, (rx, ry), u2) + return self._int_to_bytes(x), self._int_to_bytes(y) + + + def verify(self, signature, subject, public_key): + z = self._subject_to_int(subject) + + r = bytes_to_int(signature[:self.public_key_length]) + s = bytes_to_int(signature[self.public_key_length:]) + + # Verify bounds + if r >= self.n: + raise ValueError("r is out of bounds") + if s >= self.n: + raise ValueError("s is out of bounds") + + public_key = [bytes_to_int(c) for c in public_key] + + # Ensure that the public key is correct + if not self.jacobian.is_on_curve(public_key): + raise ValueError("Public key is not on curve") + + sinv = inverse(s, self.n) + u1 = (z * sinv) % self.n + u2 = (r * sinv) % self.n + + x1, _ = self.jacobian.fast_shamir(self.g, u1, public_key, u2) + if r != x1 % self.n: + raise ValueError("Invalid signature") + + return True + + + def derive_child(self, seed, child): + # Round 1 + h = hmac.new(key=b"Bitcoin seed", msg=seed, digestmod="sha512").digest() + private_key1 = h[:32] + x, y = self.private_to_public(private_key1) + public_key1 = bytes([0x02 + (y[-1] % 2)]) + x + private_key1 = bytes_to_int(private_key1) + + # Round 2 + msg = public_key1 + self._int_to_bytes(child, 4) + h = hmac.new(key=h[32:], msg=msg, digestmod="sha512").digest() + private_key2 = bytes_to_int(h[:32]) + + return self._int_to_bytes((private_key1 + private_key2) % self.n) + + + @classmethod + def get_backend(cls): + return "fallback" + + +ecc = ECC(EllipticCurveBackend, aes) diff --git a/src/lib/sslcrypto/fallback/rsa.py b/src/lib/sslcrypto/fallback/rsa.py new file mode 100644 index 00000000..54b8d2cb --- /dev/null +++ b/src/lib/sslcrypto/fallback/rsa.py @@ -0,0 +1,8 @@ +# pylint: disable=too-few-public-methods + +class RSA: + def get_backend(self): + return "fallback" + + +rsa = RSA() diff --git a/src/lib/sslcrypto/openssl/__init__.py b/src/lib/sslcrypto/openssl/__init__.py new file mode 100644 index 00000000..a32ae692 --- /dev/null +++ b/src/lib/sslcrypto/openssl/__init__.py @@ -0,0 +1,3 @@ +from .aes import aes +from .ecc import ecc +from .rsa import rsa diff --git a/src/lib/sslcrypto/openssl/aes.py b/src/lib/sslcrypto/openssl/aes.py new file mode 100644 index 00000000..c58451d5 --- /dev/null +++ b/src/lib/sslcrypto/openssl/aes.py @@ -0,0 +1,156 @@ +import ctypes +import threading +from .._aes import AES +from ..fallback.aes import aes as fallback_aes +from .library import lib, openssl_backend + + +# Initialize functions +try: + lib.EVP_CIPHER_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +except AttributeError: + pass +lib.EVP_get_cipherbyname.restype = ctypes.POINTER(ctypes.c_char) + + +thread_local = threading.local() + + +class Context: + def __init__(self, ptr, do_free): + self.lib = lib + self.ptr = ptr + self.do_free = do_free + + + def __del__(self): + if self.do_free: + self.lib.EVP_CIPHER_CTX_free(self.ptr) + + +class AESBackend: + ALGOS = ( + "aes-128-cbc", "aes-192-cbc", "aes-256-cbc", + "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", + "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", + "aes-128-ofb", "aes-192-ofb", "aes-256-ofb" + ) + + def __init__(self): + self.is_supported_ctx_new = hasattr(lib, "EVP_CIPHER_CTX_new") + self.is_supported_ctx_reset = hasattr(lib, "EVP_CIPHER_CTX_reset") + + + def _get_ctx(self): + if not hasattr(thread_local, "ctx"): + if self.is_supported_ctx_new: + thread_local.ctx = Context(lib.EVP_CIPHER_CTX_new(), True) + else: + # 1 KiB ought to be enough for everybody. We don't know the real + # size of the context buffer because we are unsure about padding and + # pointer size + thread_local.ctx = Context(ctypes.create_string_buffer(1024), False) + return thread_local.ctx.ptr + + + def get_backend(self): + return openssl_backend + + + def _get_cipher(self, algo): + if algo not in self.ALGOS: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + cipher = lib.EVP_get_cipherbyname(algo.encode()) + if not cipher: + raise ValueError("Unknown cipher algorithm {}".format(algo)) + return cipher + + + def is_algo_supported(self, algo): + try: + self._get_cipher(algo) + return True + except ValueError: + return False + + + def random(self, length): + entropy = ctypes.create_string_buffer(length) + lib.RAND_bytes(entropy, length) + return bytes(entropy) + + + def encrypt(self, data, key, algo="aes-256-cbc"): + # Initialize context + ctx = self._get_ctx() + if not self.is_supported_ctx_new: + lib.EVP_CIPHER_CTX_init(ctx) + try: + lib.EVP_EncryptInit_ex(ctx, self._get_cipher(algo), None, None, None) + + # Generate random IV + iv_length = 16 + iv = self.random(iv_length) + + # Set key and IV + lib.EVP_EncryptInit_ex(ctx, None, None, key, iv) + + # Actually encrypt + block_size = 16 + output = ctypes.create_string_buffer((len(data) // block_size + 1) * block_size) + output_len = ctypes.c_int() + + if not lib.EVP_CipherUpdate(ctx, output, ctypes.byref(output_len), data, len(data)): + raise ValueError("Could not feed cipher with data") + + new_output = ctypes.byref(output, output_len.value) + output_len2 = ctypes.c_int() + if not lib.EVP_CipherFinal_ex(ctx, new_output, ctypes.byref(output_len2)): + raise ValueError("Could not finalize cipher") + + ciphertext = output[:output_len.value + output_len2.value] + return ciphertext, iv + finally: + if self.is_supported_ctx_reset: + lib.EVP_CIPHER_CTX_reset(ctx) + else: + lib.EVP_CIPHER_CTX_cleanup(ctx) + + + def decrypt(self, ciphertext, iv, key, algo="aes-256-cbc"): + # Initialize context + ctx = self._get_ctx() + if not self.is_supported_ctx_new: + lib.EVP_CIPHER_CTX_init(ctx) + try: + lib.EVP_DecryptInit_ex(ctx, self._get_cipher(algo), None, None, None) + + # Make sure IV length is correct + iv_length = 16 + if len(iv) != iv_length: + raise ValueError("Expected IV to be {} bytes, got {} bytes".format(iv_length, len(iv))) + + # Set key and IV + lib.EVP_DecryptInit_ex(ctx, None, None, key, iv) + + # Actually decrypt + output = ctypes.create_string_buffer(len(ciphertext)) + output_len = ctypes.c_int() + + if not lib.EVP_DecryptUpdate(ctx, output, ctypes.byref(output_len), ciphertext, len(ciphertext)): + raise ValueError("Could not feed decipher with ciphertext") + + new_output = ctypes.byref(output, output_len.value) + output_len2 = ctypes.c_int() + if not lib.EVP_DecryptFinal_ex(ctx, new_output, ctypes.byref(output_len2)): + raise ValueError("Could not finalize decipher") + + return output[:output_len.value + output_len2.value] + finally: + if self.is_supported_ctx_reset: + lib.EVP_CIPHER_CTX_reset(ctx) + else: + lib.EVP_CIPHER_CTX_cleanup(ctx) + + +aes = AES(AESBackend(), fallback_aes) diff --git a/src/lib/sslcrypto/openssl/discovery.py b/src/lib/sslcrypto/openssl/discovery.py new file mode 100644 index 00000000..0ebb0299 --- /dev/null +++ b/src/lib/sslcrypto/openssl/discovery.py @@ -0,0 +1,3 @@ +# Can be redefined by user +def discover(): + pass \ No newline at end of file diff --git a/src/lib/sslcrypto/openssl/ecc.py b/src/lib/sslcrypto/openssl/ecc.py new file mode 100644 index 00000000..5b5f0bde --- /dev/null +++ b/src/lib/sslcrypto/openssl/ecc.py @@ -0,0 +1,575 @@ +import ctypes +import hmac +import threading +from .._ecc import ECC +from .aes import aes +from .library import lib, openssl_backend + + +# Initialize functions +lib.BN_new.restype = ctypes.POINTER(ctypes.c_char) +lib.BN_bin2bn.restype = ctypes.POINTER(ctypes.c_char) +lib.BN_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_GROUP_new_by_curve_name.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_KEY_new.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_POINT_new.restype = ctypes.POINTER(ctypes.c_char) +lib.EC_KEY_get0_private_key.restype = ctypes.POINTER(ctypes.c_char) +lib.EVP_PKEY_new.restype = ctypes.POINTER(ctypes.c_char) +try: + lib.EVP_PKEY_CTX_new.restype = ctypes.POINTER(ctypes.c_char) +except AttributeError: + pass + + +thread_local = threading.local() + + +# This lock is required to keep ECC thread-safe. Old OpenSSL versions (before +# 1.1.0) use global objects so they aren't thread safe. Fortunately we can check +# the code to find out which functions are thread safe. +# +# For example, EC_GROUP_new_by_curve_name checks global error code to initialize +# the group, so if two errors happen at once or two threads read the error code, +# or the codes are read in the wrong order, the group is initialized in a wrong +# way. +# +# EC_KEY_new_by_curve_name calls EC_GROUP_new_by_curve_name so it's not thread +# safe. We can't use the lock because it would be too slow; instead, we use +# EC_KEY_new and then EC_KEY_set_group which calls EC_GROUP_copy instead which +# is thread safe. +lock = threading.Lock() + + +class BN: + # BN_CTX + class Context: + def __init__(self): + self.ptr = lib.BN_CTX_new() + self.lib = lib # For finalizer + + + def __del__(self): + self.lib.BN_CTX_free(self.ptr) + + + @classmethod + def get(cls): + # Get thread-safe contexf + if not hasattr(thread_local, "bn_ctx"): + thread_local.bn_ctx = cls() + return thread_local.bn_ctx.ptr + + + def __init__(self, value=None, link_only=False): + if link_only: + self.bn = value + self._free = False + else: + if value is None: + self.bn = lib.BN_new() + self._free = True + elif isinstance(value, bytes): + self.bn = lib.BN_bin2bn(value, len(value), None) + self._free = True + else: + self.bn = lib.BN_new() + lib.BN_clear(self.bn) + lib.BN_add_word(self.bn, value) + self._free = True + + + def __del__(self): + if self._free: + lib.BN_free(self.bn) + + + def bytes(self, length=None): + buf = ctypes.create_string_buffer((len(self) + 7) // 8) + lib.BN_bn2bin(self.bn, buf) + buf = bytes(buf) + if length is None: + return buf + else: + if length < len(buf): + raise ValueError("Too little space for BN") + return b"\x00" * (length - len(buf)) + buf + + def __int__(self): + value = 0 + for byte in self.bytes(): + value = value * 256 + byte + return value + + def __len__(self): + return lib.BN_num_bits(self.bn) + + + def inverse(self, modulo): + result = BN() + if not lib.BN_mod_inverse(result.bn, self.bn, modulo.bn, BN.Context.get()): + raise ValueError("Could not compute inverse") + return result + + + def __floordiv__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only divide BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_div(result.bn, None, self.bn, other.bn, BN.Context.get()): + raise ZeroDivisionError("Division by zero") + return result + + def __mod__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only divide BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_div(None, result.bn, self.bn, other.bn, BN.Context.get()): + raise ZeroDivisionError("Division by zero") + return result + + def __add__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only sum BN's, not BN and {}".format(other)) + result = BN() + if not lib.BN_add(result.bn, self.bn, other.bn): + raise ValueError("Could not sum two BN's") + return result + + def __sub__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only subtract BN's, not BN and {}".format(other)) + result = BN() + if not lib.BN_sub(result.bn, self.bn, other.bn): + raise ValueError("Could not subtract BN from BN") + return result + + def __mul__(self, other): + if not isinstance(other, BN): + raise TypeError("Can only multiply BN by BN, not {}".format(other)) + result = BN() + if not lib.BN_mul(result.bn, self.bn, other.bn, BN.Context.get()): + raise ValueError("Could not multiply two BN's") + return result + + def __neg__(self): + return BN(0) - self + + + # A dirty but nice way to update current BN and free old BN at the same time + def __imod__(self, other): + res = self % other + self.bn, res.bn = res.bn, self.bn + return self + def __iadd__(self, other): + res = self + other + self.bn, res.bn = res.bn, self.bn + return self + def __isub__(self, other): + res = self - other + self.bn, res.bn = res.bn, self.bn + return self + def __imul__(self, other): + res = self * other + self.bn, res.bn = res.bn, self.bn + return self + + + def cmp(self, other): + if not isinstance(other, BN): + raise TypeError("Can only compare BN with BN, not {}".format(other)) + return lib.BN_cmp(self.bn, other.bn) + + def __eq__(self, other): + return self.cmp(other) == 0 + def __lt__(self, other): + return self.cmp(other) < 0 + def __gt__(self, other): + return self.cmp(other) > 0 + def __ne__(self, other): + return self.cmp(other) != 0 + def __le__(self, other): + return self.cmp(other) <= 0 + def __ge__(self, other): + return self.cmp(other) >= 0 + + + def __repr__(self): + return "".format(int(self)) + + def __str__(self): + return str(int(self)) + + +class EllipticCurveBackend: + def __init__(self, nid): + self.lib = lib # For finalizer + self.nid = nid + with lock: + # Thread-safety + self.group = lib.EC_GROUP_new_by_curve_name(self.nid) + if not self.group: + raise ValueError("The curve is not supported by OpenSSL") + + self.order = BN() + self.p = BN() + bn_ctx = BN.Context.get() + lib.EC_GROUP_get_order(self.group, self.order.bn, bn_ctx) + lib.EC_GROUP_get_curve_GFp(self.group, self.p.bn, None, None, bn_ctx) + + self.public_key_length = (len(self.p) + 7) // 8 + + self.is_supported_evp_pkey_ctx = hasattr(lib, "EVP_PKEY_CTX_new") + + + def __del__(self): + self.lib.EC_GROUP_free(self.group) + + + def _private_key_to_ec_key(self, private_key): + # Thread-safety + eckey = lib.EC_KEY_new() + lib.EC_KEY_set_group(eckey, self.group) + if not eckey: + raise ValueError("Failed to allocate EC_KEY") + private_key = BN(private_key) + if not lib.EC_KEY_set_private_key(eckey, private_key.bn): + lib.EC_KEY_free(eckey) + raise ValueError("Invalid private key") + return eckey, private_key + + + def _public_key_to_point(self, public_key): + x = BN(public_key[0]) + y = BN(public_key[1]) + # EC_KEY_set_public_key_affine_coordinates is not supported by + # OpenSSL 1.0.0 so we can't use it + point = lib.EC_POINT_new(self.group) + if not lib.EC_POINT_set_affine_coordinates_GFp(self.group, point, x.bn, y.bn, BN.Context.get()): + raise ValueError("Could not set public key affine coordinates") + return point + + + def _public_key_to_ec_key(self, public_key): + # Thread-safety + eckey = lib.EC_KEY_new() + lib.EC_KEY_set_group(eckey, self.group) + if not eckey: + raise ValueError("Failed to allocate EC_KEY") + try: + # EC_KEY_set_public_key_affine_coordinates is not supported by + # OpenSSL 1.0.0 so we can't use it + point = self._public_key_to_point(public_key) + if not lib.EC_KEY_set_public_key(eckey, point): + raise ValueError("Could not set point") + lib.EC_POINT_free(point) + return eckey + except Exception as e: + lib.EC_KEY_free(eckey) + raise e from None + + + def _point_to_affine(self, point): + # Convert to affine coordinates + x = BN() + y = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, point, x.bn, y.bn, BN.Context.get()) != 1: + raise ValueError("Failed to convert public key to affine coordinates") + # Convert to binary + if (len(x) + 7) // 8 > self.public_key_length: + raise ValueError("Public key X coordinate is too large") + if (len(y) + 7) // 8 > self.public_key_length: + raise ValueError("Public key Y coordinate is too large") + return x.bytes(self.public_key_length), y.bytes(self.public_key_length) + + + def decompress_point(self, public_key): + point = lib.EC_POINT_new(self.group) + if not point: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_oct2point(self.group, point, public_key, len(public_key), BN.Context.get()): + raise ValueError("Invalid compressed public key") + return self._point_to_affine(point) + finally: + lib.EC_POINT_free(point) + + + def new_private_key(self): + # Create random key + # Thread-safety + eckey = lib.EC_KEY_new() + lib.EC_KEY_set_group(eckey, self.group) + lib.EC_KEY_generate_key(eckey) + # To big integer + private_key = BN(lib.EC_KEY_get0_private_key(eckey), link_only=True) + # To binary + private_key_buf = private_key.bytes() + # Cleanup + lib.EC_KEY_free(eckey) + return private_key_buf + + + def private_to_public(self, private_key): + eckey, private_key = self._private_key_to_ec_key(private_key) + try: + # Derive public key + point = lib.EC_POINT_new(self.group) + try: + if not lib.EC_POINT_mul(self.group, point, private_key.bn, None, None, BN.Context.get()): + raise ValueError("Failed to derive public key") + return self._point_to_affine(point) + finally: + lib.EC_POINT_free(point) + finally: + lib.EC_KEY_free(eckey) + + + def ecdh(self, private_key, public_key): + if not self.is_supported_evp_pkey_ctx: + # Use ECDH_compute_key instead + # Create EC_KEY from private key + eckey, _ = self._private_key_to_ec_key(private_key) + try: + # Create EC_POINT from public key + point = self._public_key_to_point(public_key) + try: + key = ctypes.create_string_buffer(self.public_key_length) + if lib.ECDH_compute_key(key, self.public_key_length, point, eckey, None) == -1: + raise ValueError("Could not compute shared secret") + return bytes(key) + finally: + lib.EC_POINT_free(point) + finally: + lib.EC_KEY_free(eckey) + + # Private key: + # Create EC_KEY + eckey, _ = self._private_key_to_ec_key(private_key) + try: + # Convert to EVP_PKEY + pkey = lib.EVP_PKEY_new() + if not pkey: + raise ValueError("Could not create private key object") + try: + lib.EVP_PKEY_set1_EC_KEY(pkey, eckey) + + # Public key: + # Create EC_KEY + peer_eckey = self._public_key_to_ec_key(public_key) + try: + # Convert to EVP_PKEY + peer_pkey = lib.EVP_PKEY_new() + if not peer_pkey: + raise ValueError("Could not create public key object") + try: + lib.EVP_PKEY_set1_EC_KEY(peer_pkey, peer_eckey) + + # Create context + ctx = lib.EVP_PKEY_CTX_new(pkey, None) + if not ctx: + raise ValueError("Could not create EVP context") + try: + if lib.EVP_PKEY_derive_init(ctx) != 1: + raise ValueError("Could not initialize key derivation") + if not lib.EVP_PKEY_derive_set_peer(ctx, peer_pkey): + raise ValueError("Could not set peer") + + # Actually derive + key_len = ctypes.c_int(0) + lib.EVP_PKEY_derive(ctx, None, ctypes.byref(key_len)) + key = ctypes.create_string_buffer(key_len.value) + lib.EVP_PKEY_derive(ctx, key, ctypes.byref(key_len)) + + return bytes(key) + finally: + lib.EVP_PKEY_CTX_free(ctx) + finally: + lib.EVP_PKEY_free(peer_pkey) + finally: + lib.EC_KEY_free(peer_eckey) + finally: + lib.EVP_PKEY_free(pkey) + finally: + lib.EC_KEY_free(eckey) + + + def _subject_to_bn(self, subject): + return BN(subject[:(len(self.order) + 7) // 8]) + + + def sign(self, subject, private_key, recoverable, is_compressed, entropy): + z = self._subject_to_bn(subject) + private_key = BN(private_key) + k = BN(entropy) + + rp = lib.EC_POINT_new(self.group) + bn_ctx = BN.Context.get() + try: + # Fix Minerva + k1 = k + self.order + k2 = k1 + self.order + if len(k1) == len(k2): + k = k2 + else: + k = k1 + if not lib.EC_POINT_mul(self.group, rp, k.bn, None, None, bn_ctx): + raise ValueError("Could not generate R") + # Convert to affine coordinates + rx = BN() + ry = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, rp, rx.bn, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to convert R to affine coordinates") + r = rx % self.order + if r == BN(0): + raise ValueError("Invalid k") + # Calculate s = k^-1 * (z + r * private_key) mod n + s = (k.inverse(self.order) * (z + r * private_key)) % self.order + if s == BN(0): + raise ValueError("Invalid k") + + inverted = False + if s * BN(2) >= self.order: + s = self.order - s + inverted = True + + r_buf = r.bytes(self.public_key_length) + s_buf = s.bytes(self.public_key_length) + if recoverable: + # Generate recid + recid = int(ry % BN(2)) ^ inverted + # The line below is highly unlikely to matter in case of + # secp256k1 but might make sense for other curves + recid += 2 * int(rx // self.order) + if is_compressed: + return bytes([31 + recid]) + r_buf + s_buf + else: + if recid >= 4: + raise ValueError("Too big recovery ID, use compressed address instead") + return bytes([27 + recid]) + r_buf + s_buf + else: + return r_buf + s_buf + finally: + lib.EC_POINT_free(rp) + + + def recover(self, signature, subject): + recid = signature[0] - 27 if signature[0] < 31 else signature[0] - 31 + r = BN(signature[1:self.public_key_length + 1]) + s = BN(signature[self.public_key_length + 1:]) + + # Verify bounds + if r >= self.order: + raise ValueError("r is out of bounds") + if s >= self.order: + raise ValueError("s is out of bounds") + + bn_ctx = BN.Context.get() + + z = self._subject_to_bn(subject) + + rinv = r.inverse(self.order) + u1 = (-z * rinv) % self.order + u2 = (s * rinv) % self.order + + # Recover R + rx = r + BN(recid // 2) * self.order + if rx >= self.p: + raise ValueError("Rx is out of bounds") + rp = lib.EC_POINT_new(self.group) + if not rp: + raise ValueError("Could not create R") + try: + init_buf = b"\x02" + rx.bytes(self.public_key_length) + if not lib.EC_POINT_oct2point(self.group, rp, init_buf, len(init_buf), bn_ctx): + raise ValueError("Could not use Rx to initialize point") + ry = BN() + if lib.EC_POINT_get_affine_coordinates_GFp(self.group, rp, None, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to convert R to affine coordinates") + if int(ry % BN(2)) != recid % 2: + # Fix Ry sign + ry = self.p - ry + if lib.EC_POINT_set_affine_coordinates_GFp(self.group, rp, rx.bn, ry.bn, bn_ctx) != 1: + raise ValueError("Failed to update R coordinates") + + # Recover public key + result = lib.EC_POINT_new(self.group) + if not result: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_mul(self.group, result, u1.bn, rp, u2.bn, bn_ctx): + raise ValueError("Could not recover public key") + return self._point_to_affine(result) + finally: + lib.EC_POINT_free(result) + finally: + lib.EC_POINT_free(rp) + + + def verify(self, signature, subject, public_key): + r_raw = signature[:self.public_key_length] + r = BN(r_raw) + s = BN(signature[self.public_key_length:]) + if r >= self.order: + raise ValueError("r is out of bounds") + if s >= self.order: + raise ValueError("s is out of bounds") + + bn_ctx = BN.Context.get() + + z = self._subject_to_bn(subject) + + pub_p = lib.EC_POINT_new(self.group) + if not pub_p: + raise ValueError("Could not create public key point") + try: + init_buf = b"\x04" + public_key[0] + public_key[1] + if not lib.EC_POINT_oct2point(self.group, pub_p, init_buf, len(init_buf), bn_ctx): + raise ValueError("Could initialize point") + + sinv = s.inverse(self.order) + u1 = (z * sinv) % self.order + u2 = (r * sinv) % self.order + + # Recover public key + result = lib.EC_POINT_new(self.group) + if not result: + raise ValueError("Could not create point") + try: + if not lib.EC_POINT_mul(self.group, result, u1.bn, pub_p, u2.bn, bn_ctx): + raise ValueError("Could not recover public key") + if BN(self._point_to_affine(result)[0]) % self.order != r: + raise ValueError("Invalid signature") + return True + finally: + lib.EC_POINT_free(result) + finally: + lib.EC_POINT_free(pub_p) + + + def derive_child(self, seed, child): + # Round 1 + h = hmac.new(key=b"Bitcoin seed", msg=seed, digestmod="sha512").digest() + private_key1 = h[:32] + x, y = self.private_to_public(private_key1) + public_key1 = bytes([0x02 + (y[-1] % 2)]) + x + private_key1 = BN(private_key1) + + # Round 2 + child_bytes = [] + for _ in range(4): + child_bytes.append(child & 255) + child >>= 8 + child_bytes = bytes(child_bytes[::-1]) + msg = public_key1 + child_bytes + h = hmac.new(key=h[32:], msg=msg, digestmod="sha512").digest() + private_key2 = BN(h[:32]) + + return ((private_key1 + private_key2) % self.order).bytes(self.public_key_length) + + + @classmethod + def get_backend(cls): + return openssl_backend + + +ecc = ECC(EllipticCurveBackend, aes) diff --git a/src/lib/sslcrypto/openssl/library.py b/src/lib/sslcrypto/openssl/library.py new file mode 100644 index 00000000..47bedc3a --- /dev/null +++ b/src/lib/sslcrypto/openssl/library.py @@ -0,0 +1,98 @@ +import os +import sys +import ctypes +import ctypes.util +from .discovery import discover as user_discover + + +# Disable false-positive _MEIPASS +# pylint: disable=no-member,protected-access + +# Discover OpenSSL library +def discover_paths(): + # Search local files first + if "win" in sys.platform: + # Windows + names = [ + "libeay32.dll" + ] + openssl_paths = [os.path.abspath(path) for path in names] + if hasattr(sys, "_MEIPASS"): + openssl_paths += [os.path.join(sys._MEIPASS, path) for path in openssl_paths] + openssl_paths.append(ctypes.util.find_library("libeay32")) + elif "darwin" in sys.platform: + # Mac OS + names = [ + "libcrypto.dylib", + "libcrypto.1.1.0.dylib", + "libcrypto.1.0.2.dylib", + "libcrypto.1.0.1.dylib", + "libcrypto.1.0.0.dylib", + "libcrypto.0.9.8.dylib" + ] + openssl_paths = [os.path.abspath(path) for path in names] + openssl_paths += names + openssl_paths += [ + "/usr/local/opt/openssl/lib/libcrypto.dylib" + ] + if hasattr(sys, "_MEIPASS") and "RESOURCEPATH" in os.environ: + openssl_paths += [ + os.path.join(os.environ["RESOURCEPATH"], "..", "Frameworks", name) + for name in names + ] + openssl_paths.append(ctypes.util.find_library("ssl")) + else: + # Linux, BSD and such + names = [ + "libcrypto.so", + "libssl.so", + "libcrypto.so.1.1.0", + "libssl.so.1.1.0", + "libcrypto.so.1.0.2", + "libssl.so.1.0.2", + "libcrypto.so.1.0.1", + "libssl.so.1.0.1", + "libcrypto.so.1.0.0", + "libssl.so.1.0.0", + "libcrypto.so.0.9.8", + "libssl.so.0.9.8" + ] + openssl_paths = [os.path.abspath(path) for path in names] + openssl_paths += names + if hasattr(sys, "_MEIPASS"): + openssl_paths += [os.path.join(sys._MEIPASS, path) for path in names] + openssl_paths.append(ctypes.util.find_library("ssl")) + lst = user_discover() + if isinstance(lst, str): + lst = [lst] + elif not lst: + lst = [] + return lst + openssl_paths + + +def discover_library(): + for path in discover_paths(): + if path: + try: + return ctypes.CDLL(path) + except OSError: + pass + raise OSError("OpenSSL is unavailable") + + +lib = discover_library() + +# Initialize internal state +try: + lib.OPENSSL_add_all_algorithms_conf() +except AttributeError: + pass + +try: + lib.OpenSSL_version.restype = ctypes.c_char_p + openssl_backend = lib.OpenSSL_version(0).decode() +except AttributeError: + lib.SSLeay_version.restype = ctypes.c_char_p + openssl_backend = lib.SSLeay_version(0).decode() + +openssl_backend += " at " + lib._name diff --git a/src/lib/sslcrypto/openssl/rsa.py b/src/lib/sslcrypto/openssl/rsa.py new file mode 100644 index 00000000..afd8b51c --- /dev/null +++ b/src/lib/sslcrypto/openssl/rsa.py @@ -0,0 +1,11 @@ +# pylint: disable=too-few-public-methods + +from .library import openssl_backend + + +class RSA: + def get_backend(self): + return openssl_backend + + +rsa = RSA() diff --git a/src/util/Electrum.py b/src/util/Electrum.py new file mode 100644 index 00000000..112151aa --- /dev/null +++ b/src/util/Electrum.py @@ -0,0 +1,39 @@ +import hashlib +import struct + + +# Electrum, the heck?! + +def bchr(i): + return struct.pack("B", i) + +def encode(val, base, minlen=0): + base, minlen = int(base), int(minlen) + code_string = b"".join([bchr(x) for x in range(256)]) + result = b"" + while val > 0: + index = val % base + result = code_string[index:index + 1] + result + val //= base + return code_string[0:1] * max(minlen - len(result), 0) + result + +def insane_int(x): + x = int(x) + if x < 253: + return bchr(x) + elif x < 65536: + return bchr(253) + encode(x, 256, 2)[::-1] + elif x < 4294967296: + return bchr(254) + encode(x, 256, 4)[::-1] + else: + return bchr(255) + encode(x, 256, 8)[::-1] + + +def magic(message): + return b"\x18Bitcoin Signed Message:\n" + insane_int(len(message)) + message + +def format(message): + return hashlib.sha256(magic(message)).digest() + +def dbl_format(message): + return hashlib.sha256(format(message)).digest() diff --git a/src/util/OpensslFindPatch.py b/src/util/OpensslFindPatch.py index fd09ec6b..5e218d7e 100644 --- a/src/util/OpensslFindPatch.py +++ b/src/util/OpensslFindPatch.py @@ -1,13 +1,11 @@ import logging import os import sys -import ctypes -import ctypes.util +from ctypes.util import find_library +from lib.sslcrypto.openssl import discovery from Config import config -find_library_original = ctypes.util.find_library - def getOpensslPath(): if config.openssl_lib_file: @@ -49,29 +47,11 @@ def getOpensslPath(): logging.debug("OpenSSL lib not found in: %s (%s)" % (path, err)) lib_path = ( - find_library_original('ssl.so') or find_library_original('ssl') or - find_library_original('crypto') or find_library_original('libcrypto') or 'libeay32' + find_library('ssl.so') or find_library('ssl') or + find_library('crypto') or find_library('libcrypto') or 'libeay32' ) return lib_path -def patchCtypesOpensslFindLibrary(): - def findLibraryPatched(name): - if name in ("ssl", "crypto", "libeay32"): - lib_path = getOpensslPath() - return lib_path - else: - return find_library_original(name) - - ctypes.util.find_library = findLibraryPatched - - -patchCtypesOpensslFindLibrary() - - -def openLibrary(): - lib_path = getOpensslPath() - logging.debug("Opening %s..." % lib_path) - ssl_lib = ctypes.CDLL(lib_path, ctypes.RTLD_GLOBAL) - return ssl_lib +discovery.discover = getOpensslPath From 02fd1dc4d04bff0c643e391df2a059c57217c003 Mon Sep 17 00:00:00 2001 From: Ivanq Date: Thu, 5 Mar 2020 22:59:07 +0300 Subject: [PATCH 483/483] Add GitHub Actions workflow --- .github/workflows/tests.yml | 49 +++++++++++++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 50 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..6f2a748e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,49 @@ +name: tests + +on: [push] + +jobs: + test: + + runs-on: ubuntu-16.04 + strategy: + max-parallel: 16 + matrix: + python-version: [3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v1 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: Prepare for installation + run: | + python3 -m pip install setuptools + python3 -m pip install --upgrade pip wheel + python3 -m pip install --upgrade codecov coveralls flake8 mock pytest==4.6.3 pytest-cov selenium + + - name: Install + run: | + python3 -m pip install --upgrade -r requirements.txt + python3 -m pip list + + - name: Prepare for tests + run: | + openssl version -a + echo 0 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6 + + - name: Test + run: | + catchsegv python3 -m pytest src/Test --cov=src --cov-config src/Test/coverage.ini + export ZERONET_LOG_DIR="log/CryptMessage"; catchsegv python3 -m pytest -x plugins/CryptMessage/Test + export ZERONET_LOG_DIR="log/Bigfile"; catchsegv python3 -m pytest -x plugins/Bigfile/Test + export ZERONET_LOG_DIR="log/AnnounceLocal"; catchsegv python3 -m pytest -x plugins/AnnounceLocal/Test + export ZERONET_LOG_DIR="log/OptionalManager"; catchsegv python3 -m pytest -x plugins/OptionalManager/Test + export ZERONET_LOG_DIR="log/Multiuser"; mv plugins/disabled-Multiuser plugins/Multiuser && catchsegv python -m pytest -x plugins/Multiuser/Test + export ZERONET_LOG_DIR="log/Bootstrapper"; mv plugins/disabled-Bootstrapper plugins/Bootstrapper && catchsegv python -m pytest -x plugins/Bootstrapper/Test + find src -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" + find plugins -name "*.json" | xargs -n 1 python3 -c "import json, sys; print(sys.argv[1], end=' '); json.load(open(sys.argv[1])); print('[OK]')" + flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics --exclude=src/lib/pyaes/ diff --git a/.gitignore b/.gitignore index 451a67be..38dd3a34 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ # Hidden files .* +!/.github !/.gitignore !/.travis.yml !/.gitlab-ci.yml