├── licensing ├── __init__.py ├── internal.py ├── models.py └── methods.py ├── setup.cfg ├── Makefile ├── make.bat ├── LICENSE.txt ├── index.rst ├── .gitignore ├── setup.py ├── test.py ├── conf.py ├── README.md └── cryptolens_python2.py /licensing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Inside of setup.cfg 2 | [metadata] 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Cryptolens AB and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | .. cryptolens python documentation master file, created by 2 | sphinx-quickstart on Fri Feb 1 14:18:52 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to cryptolens python's documentation! 7 | ============================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | .. automodule:: licensing 13 | :members: 14 | .. autoclass:: licensing.methods.Key 15 | :members: 16 | .. autoclass:: licensing.methods.AI 17 | :members: 18 | .. autoclass:: licensing.methods.Data 19 | :members: 20 | .. autoclass:: licensing.methods.PaymentForm 21 | :members: 22 | .. autoclass:: licensing.methods.Message 23 | :members: 24 | .. autoclass:: licensing.methods.Customer 25 | :members: 26 | .. autoclass:: licensing.methods.Product 27 | :members: 28 | .. autoclass:: licensing.models.LicenseKey 29 | :members: 30 | .. autoclass:: licensing.methods.Helpers 31 | :members: 32 | .. autoclass:: licensing.models.ActivatedMachine 33 | :members: 34 | .. autoclass:: licensing.models.Response 35 | :members: 36 | 37 | Indices and tables 38 | ================== 39 | 40 | * :ref:`genindex` 41 | * :ref:`modindex` 42 | * :ref:`search` 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | _build 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | licensefile.skm 108 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name = 'licensing', # How you named your package folder (MyLib) 5 | packages = ['licensing'], # Chose the same as "name" 6 | version = '0.53', # Start with a small number and increase it with every change you make 7 | license='MIT', # Chose a license from here: https://help.github.com/articles/licensing-a-repository 8 | #description_file = 'Client library for Cryptolens licensing Web API.', # Give a short description about your library 9 | long_description = 'Client library for Cryptolens Web API (software licensing).', 10 | long_description_content_type="text/markdown", 11 | author = 'Cryptolens AB', # Type in your name 12 | author_email = 'support@cryptolens.io', # Type in your E-Mail 13 | url = 'https://cryptolens.io', # Provide either the link to your github or to your website 14 | download_url = 'https://github.com/Cryptolens/cryptolens-python/archive/v_53.tar.gz', 15 | project_urls={ # optional, shows on PyPI 16 | "Source": "https://github.com/Cryptolens/cryptolens-python", 17 | "Tracker": "https://github.com/Cryptolens/cryptolens-python/issues", 18 | }, 19 | keywords = ['software licensing', 'licensing library', 'cryptolens'], # Keywords that define your package best 20 | classifiers=[ 21 | #'Development Status :: 5 - Stable', # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package 22 | 'Intended Audience :: Developers', # Define that your audience are developers 23 | 'Topic :: Software Development :: Build Tools', 24 | 'Programming Language :: Python :: 3', #Specify which pyhton versions that you want to support 25 | 'Programming Language :: Python :: 3.4', 26 | 'Programming Language :: Python :: 3.5', 27 | 'Programming Language :: Python :: 3.6', 28 | 'Programming Language :: Python :: 3.7', 29 | 'Programming Language :: Python :: 3.8', 30 | 'Programming Language :: Python :: 3.9', 31 | 'Programming Language :: Python :: 3.10', 32 | 'Programming Language :: Python :: 3.11', 33 | 'Programming Language :: Python :: 3.12', 34 | 'Operating System :: OS Independent', 35 | 'Operating System :: POSIX :: Linux', 36 | 'Operating System :: Microsoft :: Windows', 37 | 'Operating System :: MacOS :: MacOS X', 38 | ], 39 | 40 | ) -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Jan 23 09:34:40 2019 4 | 5 | @author: Artem Los 6 | """ 7 | 8 | from licensing.models import * 9 | from licensing.methods import Key, Helpers, Message, Product, Customer, Data, AI 10 | 11 | import socket 12 | 13 | #from cryptolens_python2 import * 14 | pubKey = "sGbvxwdlDbqFXOMlVUnAF5ew0t0WpPW7rFpI5jHQOFkht/326dvh7t74RYeMpjy357NljouhpTLA3a6idnn4j6c3jmPWBkjZndGsPL4Bqm+fwE48nKpGPjkj4q/yzT4tHXBTyvaBjA8bVoCTnu+LiC4XEaLZRThGzIn5KQXKCigg6tQRy0GXE13XYFVz/x1mjFbT9/7dS8p85n8BuwlY5JvuBIQkKhuCNFfrUxBWyu87CFnXWjIupCD2VO/GbxaCvzrRjLZjAngLCMtZbYBALksqGPgTUN7ZM24XbPWyLtKPaXF2i4XRR9u6eTj5BfnLbKAU5PIVfjIS+vNYYogteQ==AQAB" 15 | #HelperMethods.ironpython2730_legacy = True 16 | #HelperMethods.proxy_experimental=True 17 | res = Key.activate(token="WyIyNzMyIiwiYmx6NlJ6ZzdaWjFScmxFVFNCc283YTJyUG5kQURMZ0hucW1YdUZxKyJd",\ 18 | rsa_pub_key=pubKey,\ 19 | product_id=3349, key="ICVLD-VVSZR-ZTICT-YKGXL", machine_code=Helpers.GetMachineCode(v=2),\ 20 | friendly_name=socket.gethostname()) 21 | 22 | if res[0] == None or not Helpers.IsOnRightMachine(res[0]): 23 | print("An error occured: {0}".format(res[1])) 24 | else: 25 | print("Success") 26 | 27 | license_key = res[0] 28 | print("Feature 1: " + str(license_key.f1)) 29 | print("License expires: " + str(license_key.expires)) 30 | 31 | 32 | if res[0] != None: 33 | # saving license file to disk 34 | with open('licensefile.skm', 'w') as f: 35 | f.write(res[0].save_as_string()) 36 | 37 | 38 | # read license file from file 39 | with open('licensefile.skm', 'r') as f: 40 | license_key = LicenseKey.load_from_string(pubKey, f.read()) 41 | 42 | if not Helpers.IsOnRightMachine(license_key): 43 | print("NOTE: This license file does not belong to this machine.") 44 | else: 45 | print("Feature 1: " + str(license_key.f1)) 46 | print("License expires: " + str(license_key.expires)) 47 | 48 | print(Helpers.GetMachineCode()) 49 | 50 | #res = Key.create_trial_key("WyIzODQ0IiwiempTRWs4SnBKTTArYUh3WkwyZ0VwQkVyeTlUVkRWK2ZTOS8wcTBmaCJd", 3941, Helpers.GetMachineCode()) 51 | 52 | 53 | # Read more on how to define custom features here: https://help.cryptolens.io/web-interface/feature-templates 54 | print(Helpers.HasFeature(res[0], "ModuleB")) 55 | print(Helpers.HasFeature(res[0], "ModuleB.Submodule 2")) 56 | print(not(Helpers.HasFeature(res[0], "ModuleC.Submodule 2"))) 57 | print(Helpers.HasFeature(res[0], "ModuleD.ModuleD1.Submodule D1")) -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath('.')) 19 | from licensing import * 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'cryptolens python' 24 | copyright = '2020, Cryptolens AB' 25 | author = 'Cryptolens AB' 26 | 27 | # The short X.Y version 28 | version = '' 29 | # The full version, including alpha/beta/rc tags 30 | release = '0.5' 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.viewcode', 44 | 'sphinx.ext.autodoc' 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | # 62 | # This is also used if you do content translation via gettext catalogs. 63 | # Usually you set "language" from the command line for these cases. 64 | language = None 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path. 69 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 70 | 71 | # The name of the Pygments (syntax highlighting) style to use. 72 | pygments_style = None 73 | 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = 'alabaster' 81 | 82 | # Theme options are theme-specific and customize the look and feel of a theme 83 | # further. For a list of options available for each theme, see the 84 | # documentation. 85 | # 86 | # html_theme_options = {} 87 | 88 | # Add any paths that contain custom static files (such as style sheets) here, 89 | # relative to this directory. They are copied after the builtin static files, 90 | # so a file named "default.css" will overwrite the builtin "default.css". 91 | html_static_path = ['_static'] 92 | 93 | # Custom sidebar templates, must be a dictionary that maps document names 94 | # to template names. 95 | # 96 | # The default sidebars (for documents that don't match any pattern) are 97 | # defined by theme itself. Builtin themes are using these templates by 98 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 99 | # 'searchbox.html']``. 100 | # 101 | # html_sidebars = {} 102 | 103 | 104 | # -- Options for HTMLHelp output --------------------------------------------- 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'cryptolenspythondoc' 108 | 109 | 110 | # -- Options for LaTeX output ------------------------------------------------ 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'cryptolenspython.tex', 'cryptolens python Documentation', 135 | 'Cryptolens AB', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output ------------------------------------------ 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'cryptolenspython', 'cryptolens python Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ---------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'cryptolenspython', 'cryptolens python Documentation', 156 | author, 'cryptolenspython', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] 159 | 160 | 161 | # -- Options for Epub output ------------------------------------------------- 162 | 163 | # Bibliographic Dublin Core info. 164 | epub_title = project 165 | 166 | # The unique identifier of the text. This can be a ISBN number 167 | # or the project homepage. 168 | # 169 | # epub_identifier = '' 170 | 171 | # A unique identification for the text. 172 | # 173 | # epub_uid = '' 174 | 175 | # A list of files that should not be packed into the epub file. 176 | epub_exclude_files = ['search.html'] 177 | 178 | 179 | # -- Extension configuration ------------------------------------------------- 180 | -------------------------------------------------------------------------------- /licensing/internal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Jan 23 10:12:13 2019 4 | 5 | @author: Artem Los 6 | """ 7 | import base64 8 | import urllib.request 9 | import hashlib 10 | import subprocess 11 | import os, os.path 12 | import ssl 13 | 14 | def subprocess_args(include_stdout=True): 15 | if hasattr(subprocess, 'STARTUPINFO'): 16 | si = subprocess.STARTUPINFO() 17 | si.dwFlags |= subprocess.STARTF_USESHOWWINDOW 18 | env = os.environ 19 | else: 20 | si = None 21 | env = None 22 | 23 | if include_stdout: 24 | ret = {'stdout': subprocess.PIPE} 25 | else: 26 | ret = {} 27 | 28 | ret.update({'stdin': subprocess.PIPE, 29 | 'stderr': subprocess.PIPE, 30 | 'startupinfo': si, 31 | 'env': env }) 32 | return ret 33 | 34 | class HelperMethods: 35 | 36 | server_address = "https://api.cryptolens.io/api/" 37 | 38 | verify_SSL = True 39 | 40 | proxy_experimental = False 41 | 42 | @staticmethod 43 | def get_SHA256(string): 44 | """ 45 | Compute the SHA256 signature of a string. 46 | """ 47 | return hashlib.sha256(string.encode("utf-8")).hexdigest() 48 | 49 | @staticmethod 50 | def I2OSP(x, xLen): 51 | if x > (1 << (8 * xLen)): 52 | return None 53 | Xrev = [] 54 | for _ in range(0, xLen): 55 | x, m = divmod(x, 256) 56 | Xrev.append(m) 57 | return bytes(reversed(Xrev)) 58 | 59 | @staticmethod 60 | def OS2IP(X): 61 | import binascii 62 | h = binascii.hexlify(X) 63 | return int(h, 16) 64 | 65 | @staticmethod 66 | def RSAVP1(pair, s): 67 | n, e = pair 68 | if s < 0 or n-1 < s: 69 | return None 70 | return pow(s, e, n) 71 | 72 | @staticmethod 73 | def EMSA_PKCS1_V15_ENCODE(M, emLen, hlen = 256): 74 | import hashlib 75 | h = hashlib.sha256() 76 | if hlen == 512: 77 | h = hashlib.sha512() 78 | h.update(M) 79 | H = h.digest() 80 | 81 | T = bytes([0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20]) + H 82 | if hlen == 512: 83 | T = bytes([0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40]) + H 84 | 85 | tLen = len(T) 86 | if emLen < tLen + 11: 87 | return None 88 | PS = bytes([0xff for _ in range(emLen - tLen - 3)]) 89 | return b"".join([b"\x00\x01", PS, b"\x00", T]) 90 | 91 | @staticmethod 92 | def RSAASSA_PKCS1_V15_VERIFY(pair, M, S, l=256): 93 | n, e = pair 94 | s = HelperMethods.OS2IP(S) 95 | m = HelperMethods.RSAVP1((n,e), s) 96 | if m is None: return False 97 | EM = HelperMethods.I2OSP(m, 256) 98 | if EM is None: return False 99 | EM2 = HelperMethods.EMSA_PKCS1_V15_ENCODE(M, 256, l) 100 | if EM2 is None: return False 101 | 102 | try: 103 | import hmac 104 | return hmac.compare_digest(EM, EM2) 105 | except (ImportError, AttributeError): 106 | return EM == EM2 107 | 108 | @staticmethod 109 | def verify_signature(response, rsaPublicKey): 110 | """ 111 | Verifies a signature from .NET RSACryptoServiceProvider. 112 | """ 113 | 114 | n = HelperMethods.OS2IP(base64.b64decode(rsaPublicKey.modulus)) 115 | e = HelperMethods.OS2IP(base64.b64decode(rsaPublicKey.exponent)) 116 | 117 | m = base64.b64decode(response.license_key) 118 | r = base64.b64decode(response.signature) 119 | 120 | return HelperMethods.RSAASSA_PKCS1_V15_VERIFY((n,e), m, r) 121 | 122 | @staticmethod 123 | def verify_signature_metadata(signature, rsaPublicKey): 124 | 125 | import base64 126 | import json 127 | 128 | n = HelperMethods.OS2IP(base64.b64decode(rsaPublicKey.modulus)) 129 | e = HelperMethods.OS2IP(base64.b64decode(rsaPublicKey.exponent)) 130 | 131 | data = json.loads(base64.b64decode(signature)) 132 | 133 | d = base64.b64decode(data["Data"]) 134 | s = base64.b64decode(data["Signature"]) 135 | 136 | return [HelperMethods.RSAASSA_PKCS1_V15_VERIFY((n,e), d,s, l=512), d] 137 | 138 | 139 | @staticmethod 140 | def int2base64(num): 141 | return base64.b64encode(int.to_bytes(num), byteorder='big') 142 | 143 | @staticmethod 144 | def base642int(string): 145 | return int.from_bytes(base64.b64decode((string)), byteorder='big') 146 | 147 | @staticmethod 148 | def send_request(method, params): 149 | """ 150 | Send a POST request to method in the Web API with the specified 151 | params and return the response string. 152 | 153 | method: the path of the method, eg. key/activate 154 | params: a dictionary of parameters 155 | """ 156 | 157 | if HelperMethods.verify_SSL: 158 | req = urllib.request.Request(HelperMethods.server_address + method, \ 159 | urllib.parse.urlencode(params)\ 160 | .encode("utf-8")) 161 | 162 | if HelperMethods.proxy_experimental == True: 163 | proxies = urllib.request.getproxies() 164 | if HelperMethods.proxy_experimental: 165 | from urllib.parse import urlparse 166 | for scheme, proxy_uri in urllib.request.getproxies().items(): 167 | req.set_proxy(urlparse(proxy_uri).netloc, scheme) 168 | return urllib.request.urlopen(req).read().decode("utf-8") 169 | else: 170 | return urllib.request.urlopen(req).read().decode("utf-8") 171 | 172 | else: 173 | ctx = ssl.create_default_context() 174 | ctx.check_hostname = False 175 | ctx.verify_mode = ssl.CERT_NONE 176 | 177 | return urllib.request.urlopen(HelperMethods.server_address + method, \ 178 | urllib.parse.urlencode(params)\ 179 | .encode("utf-8"), context=ctx).read().decode("utf-8") 180 | 181 | @staticmethod 182 | def start_process_ps_v2(): 183 | ps_args = "-Command (Get-CimInstance -Class Win32_ComputerSystemProduct).UUID" 184 | 185 | cmd = ["powershell", *ps_args.split(" ")] 186 | 187 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 188 | out, err = proc.communicate(timeout=120) 189 | 190 | rawOutput = out.decode('utf-8').strip() 191 | return rawOutput 192 | 193 | 194 | @staticmethod 195 | def start_process(command, v = 1): 196 | 197 | output = subprocess.check_output(command, 198 | **subprocess_args(False)) 199 | 200 | if v == 1: 201 | return output.decode('utf-8') 202 | elif v == 2: 203 | rawOutput = output.decode('utf-8') 204 | return rawOutput[rawOutput.index("UUID")+4:].strip() 205 | else: 206 | raise ValueError("Version can be either 1 or 2.") 207 | 208 | @staticmethod 209 | def get_dbus_machine_id(): 210 | try: 211 | with open("/etc/machine-id") as f: 212 | return f.read().strip() 213 | except: 214 | pass 215 | try: 216 | with open("/var/lib/dbus/machine-id") as f: 217 | return f.read().strip() 218 | except: 219 | pass 220 | return "" 221 | 222 | @staticmethod 223 | def get_inodes(): 224 | import os 225 | files = ["/bin", "/etc", "/lib", "/root", "/sbin", "/usr", "/var"] 226 | inodes = [] 227 | for file in files: 228 | try: 229 | inodes.append(os.stat(file).st_ino) 230 | except: 231 | pass 232 | return "".join([str(x) for x in inodes]) 233 | 234 | @staticmethod 235 | def compute_machine_code(): 236 | return HelperMethods.get_dbus_machine_id() + HelperMethods.get_inodes() 237 | 238 | -------------------------------------------------------------------------------- /licensing/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Jan 23 09:47:26 2019 4 | 5 | @author: Artem Los 6 | """ 7 | import xml.etree.ElementTree 8 | import json 9 | import base64 10 | import datetime 11 | import copy 12 | import time 13 | 14 | from licensing.internal import HelperMethods 15 | 16 | class ActivatedMachine: 17 | def __init__(self, IP, Mid, Time, FriendlyName="", FloatingExpires = ""): 18 | self.IP = IP 19 | self.Mid = Mid 20 | 21 | # TODO: check if time is int, and convert to datetime in this case. 22 | self.Time = Time 23 | self.FriendlyName = FriendlyName 24 | self.FloatingExpires = FloatingExpires 25 | 26 | class Reseller: 27 | 28 | """ 29 | Information about the reseller. 30 | """ 31 | 32 | def __init__(self, Id, InviteId, ResellerUserId, Created, Name, Url, Email, Phone, Description): 33 | self.Id = Id 34 | self.InviteId = InviteId 35 | self.ResellerUserId = ResellerUserId 36 | self.Created = Created 37 | self.Name = Name 38 | self.Url = Url 39 | self.Email = Email 40 | self.Phone = Phone 41 | self.Description = Description 42 | 43 | 44 | class LicenseKey: 45 | 46 | def __init__(self, ProductId, ID, Key, Created, Expires, Period, F1, F2,\ 47 | F3, F4, F5, F6, F7, F8, Notes, Block, GlobalId, Customer, \ 48 | ActivatedMachines, TrialActivation, MaxNoOfMachines, \ 49 | AllowedMachines, DataObjects, SignDate, Reseller, RawResponse): 50 | 51 | self.product_id = ProductId 52 | self.id = ID 53 | self.key = Key 54 | self.created = Created 55 | self.expires = Expires 56 | self.period = Period 57 | self.f1 = F1 58 | self.f2 = F2 59 | self.f3 = F3 60 | self.f4 = F4 61 | self.f5 = F5 62 | self.f6 = F6 63 | self.f7 = F7 64 | self.f8 = F8 65 | self.notes = Notes 66 | self.block = Block 67 | self.global_id = GlobalId 68 | self.customer = Customer 69 | self.activated_machines = ActivatedMachines 70 | self.trial_activation = TrialActivation 71 | self.max_no_of_machines = MaxNoOfMachines 72 | self.allowed_machines = AllowedMachines 73 | self.data_objects = DataObjects 74 | self.sign_date = SignDate 75 | self.reseller = Reseller 76 | self.raw_response = RawResponse 77 | 78 | @staticmethod 79 | def from_response(response): 80 | 81 | if response.result == 1: 82 | raise ValueError("The response did not contain any license key object since it was unsuccessful. Message '{0}'.".format(response.message)) 83 | 84 | obj = json.loads(base64.b64decode(response.license_key).decode('utf-8')) 85 | 86 | reseller = None 87 | 88 | if "Reseller" in obj and obj["Reseller"] != None: 89 | reseller = Reseller(**obj["Reseller"]) 90 | 91 | try: 92 | datetime.datetime.fromtimestamp(obj["Expires"]) 93 | except: 94 | raise ValueError("The expiration date cannot be converted to a datetime object. Please try setting the period to a lower value. Read more: https://github.com/Cryptolens/cryptolens-python/tree/master#the-expiration-date-cannot-be-converted-to-a-datetime-object-please-try-setting-the-period-to-a-lower-value") 95 | 96 | return LicenseKey(obj["ProductId"], obj["ID"], obj["Key"], datetime.datetime.fromtimestamp(obj["Created"]),\ 97 | datetime.datetime.fromtimestamp(obj["Expires"]), obj["Period"], obj["F1"], obj["F2"], \ 98 | obj["F3"], obj["F4"],obj["F5"],obj["F6"], obj["F7"], \ 99 | obj["F8"], obj["Notes"], obj["Block"], obj["GlobalId"],\ 100 | obj["Customer"], LicenseKey.__load_activated_machines(obj["ActivatedMachines"]), obj["TrialActivation"], \ 101 | obj["MaxNoOfMachines"], obj["AllowedMachines"], obj["DataObjects"], \ 102 | datetime.datetime.fromtimestamp(obj["SignDate"]),reseller, response) 103 | 104 | def save_as_string(self): 105 | """ 106 | Save the license as a string that can later be read by load_from_string. 107 | """ 108 | res = copy.copy(self.raw_response.__dict__) 109 | res["licenseKey"] = res["license_key"] 110 | res.pop("license_key", None) 111 | return json.dumps(res) 112 | 113 | @staticmethod 114 | def load_from_string(rsa_pub_key, string, signature_expiration_interval = -1): 115 | """ 116 | Loads a license from a string generated by save_as_string. 117 | Note: if an error occurs, None will be returned. An error can occur 118 | if the license string has been tampered with or if the public key is 119 | incorrectly formatted. 120 | 121 | :param signature_expiration_interval: If the license key was signed, 122 | this method will check so that no more than "signatureExpirationInterval" 123 | days have passed since the last activation. 124 | """ 125 | 126 | response = Response("","","","") 127 | 128 | try: 129 | response = Response.from_string(string) 130 | except Exception as ex: 131 | return None 132 | 133 | if response.result == "1": 134 | return None 135 | else: 136 | try: 137 | pubKey = RSAPublicKey.from_string(rsa_pub_key) 138 | if HelperMethods.verify_signature(response, pubKey): 139 | 140 | licenseKey = LicenseKey.from_response(response) 141 | 142 | sign_date = licenseKey.sign_date 143 | if sign_date.tzinfo is None: 144 | sign_date = sign_date.replace(tzinfo=datetime.timezone.utc) 145 | 146 | if signature_expiration_interval > 0 and \ 147 | (sign_date + datetime.timedelta(days=1*signature_expiration_interval) < datetime.datetime.now(datetime.timezone.utc)): 148 | return None 149 | 150 | return licenseKey 151 | else: 152 | return None 153 | except Exception: 154 | return None 155 | 156 | @staticmethod 157 | def __load_activated_machines(obj): 158 | 159 | if obj == None: 160 | return None 161 | 162 | arr = [] 163 | 164 | for item in obj: 165 | arr.append(ActivatedMachine(**item)) 166 | 167 | return arr 168 | 169 | class Response: 170 | 171 | def __init__(self, license_key, signature, result, message, metadata=None): 172 | self.license_key = license_key 173 | self.signature = signature 174 | self.result = result 175 | self.message = message 176 | self.metadata = metadata 177 | 178 | @staticmethod 179 | def from_string(responseString): 180 | obj = dict((k.lower(),v) for k,v in json.loads(responseString).items()) 181 | 182 | licenseKey = "" 183 | signature = "" 184 | result = 0 185 | message = "" 186 | metadata = None 187 | 188 | if "licensekey" in obj: 189 | licenseKey = obj["licensekey"] 190 | 191 | if "signature" in obj: 192 | signature = obj["signature"] 193 | 194 | if "message" in obj: 195 | message = obj["message"] 196 | 197 | if "result" in obj: 198 | result = obj["result"] 199 | else: 200 | result = 1 201 | 202 | if "metadata" in obj: 203 | metadata = obj["metadata"] 204 | 205 | 206 | 207 | return Response(licenseKey, signature, result, message, metadata) 208 | 209 | class RSAPublicKey: 210 | 211 | def __init__(self, modulus, exponent): 212 | self.modulus = modulus 213 | self.exponent = exponent 214 | 215 | @staticmethod 216 | def from_string(rsaPubKeyString): 217 | """ 218 | The rsaPubKeyString can be found at https://app.cryptolens.io/User/Security. 219 | It should be of the following format: 220 | ...AQAB 221 | """ 222 | rsaKey = xml.etree.ElementTree.fromstring(rsaPubKeyString) 223 | return RSAPublicKey(rsaKey.find('Modulus').text, rsaKey.find('Exponent').text) 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptolens Client API for Python 2 | 3 | [![Downloads](https://pepy.tech/badge/licensing)](https://pepy.tech/project/licensing) 4 | [![Downloads](https://pepy.tech/badge/licensing/month)](https://pepy.tech/project/licensing) 5 | [![Downloads](https://pepy.tech/badge/licensing/week)](https://pepy.tech/project/licensing) 6 | 7 | This library contains helper methods to verify license keys in Python. 8 | 9 | Python docs can be found here: https://help.cryptolens.io/api/python/ 10 | 11 | **Autodesk Maya**: The Python2 version needs to be used, as described [here](https://cryptolens.io/2019/07/autodesk-maya-plugin-software-licensing/). 12 | 13 | **Autodesk Revit / Iron Python 2.7.3**: The Python2 version needs to be used with `HelperMethods.ironpython2730_legacy = True`. 14 | 15 | > Please check out our guide about common errors and how to solve them: https://help.cryptolens.io/faq/index#troubleshooting-api-errors. For Python specific errors, please review [this section](#possible-errors). 16 | 17 | ## Installation 18 | 19 | ### Python 3 20 | ``` 21 | pip install licensing 22 | ``` 23 | 24 | ### Python 2 25 | Please copy `cryptolens_python2.py` file into your project folder. The entire library is contained in that file. 26 | > In the examples below, please disregard the imports and use only the following one: 27 | 28 | ```python 29 | from cryptolens_python2 import * 30 | ``` 31 | 32 | If you create a plugin for Autodesk Revit or use IronPython 2.7.3 or earlier, please also add the line below right after the import: 33 | 34 | ```python 35 | HelperMethods.ironpython2730_legacy = True 36 | ``` 37 | 38 | ## Example 39 | 40 | ### Key verification 41 | 42 | The code below will work exactly as the one explained in the [key verification tutorial](https://help.cryptolens.io/examples/key-verification). 43 | 44 | First, we need to add the namespaces: 45 | 46 | In Python 3: 47 | ```python 48 | from licensing.models import * 49 | from licensing.methods import Key, Helpers 50 | ``` 51 | 52 | In Python 2: 53 | 54 | ```python 55 | from cryptolens_python2 import * 56 | ``` 57 | 58 | Now we can perform the actual key verification: 59 | 60 | ```python 61 | RSAPubKey = "sGbvxwdlDbqFXOMlVUnAF5ew0t0WpPW7rFpI5jHQOFkht/326dvh7t74RYeMpjy357NljouhpTLA3a6idnn4j6c3jmPWBkjZndGsPL4Bqm+fwE48nKpGPjkj4q/yzT4tHXBTyvaBjA8bVoCTnu+LiC4XEaLZRThGzIn5KQXKCigg6tQRy0GXE13XYFVz/x1mjFbT9/7dS8p85n8BuwlY5JvuBIQkKhuCNFfrUxBWyu87CFnXWjIupCD2VO/GbxaCvzrRjLZjAngLCMtZbYBALksqGPgTUN7ZM24XbPWyLtKPaXF2i4XRR9u6eTj5BfnLbKAU5PIVfjIS+vNYYogteQ==AQAB" 62 | auth = "WyIyNTU1IiwiRjdZZTB4RmtuTVcrQlNqcSszbmFMMHB3aWFJTlBsWW1Mbm9raVFyRyJd==" 63 | 64 | result = Key.activate(token=auth,\ 65 | rsa_pub_key=RSAPubKey,\ 66 | product_id=3349, \ 67 | key="ICVLD-VVSZR-ZTICT-YKGXL",\ 68 | machine_code=Helpers.GetMachineCode(v=2)) 69 | 70 | if result[0] == None or not Helpers.IsOnRightMachine(result[0], v=2): 71 | # an error occurred or the key is invalid or it cannot be activated 72 | # (eg. the limit of activated devices was achieved) 73 | print("The license does not work: {0}".format(result[1])) 74 | else: 75 | # everything went fine if we are here! 76 | print("The license is valid!") 77 | license_key = result[0] 78 | print("Feature 1: " + str(license_key.f1)) 79 | print("License expires: " + str(license_key.expires)) 80 | ``` 81 | 82 | * `RSAPubKey` - the RSA public key (can be found [here](https://app.cryptolens.io/docs/api/v3/QuickStart#api-keys), in *API Keys* section). 83 | * `token` - the access token (can be found [here](https://app.cryptolens.io/docs/api/v3/QuickStart#api-keys), in *API Keys* section). 84 | * `product_id` - the id of the product can be found on the product page. 85 | * `key` - the license key to be verified 86 | * `machine_code` - the unique id of the device. 87 | 88 | **Note:** The code above assumes that node-locking is enabled. By default, license keys are created with _Maximum Number of Machines_ set to zero, which deactivates node-locking. As a result, machines will not be registered and the call to `Helpers.IsOnRightMachine(result[0])` will return `False`. You can read more about this behaviour [here](https://help.cryptolens.io/faq/index#maximum-number-of-machines). 89 | 90 | ### Offline activation (saving/loading licenses) 91 | 92 | Assuming the license key verification was successful, we can save the result in a file so that we can use it instead of contacting Cryptolens. 93 | 94 | ```python 95 | # res is obtained from the code above 96 | if result[0] != None: 97 | # saving license file to disk 98 | with open('licensefile.skm', 'w') as f: 99 | f.write(result[0].save_as_string()) 100 | ``` 101 | 102 | When loading it back, we can use the code below: 103 | 104 | ```python 105 | # read license file from file 106 | with open('licensefile.skm', 'r') as f: 107 | license_key = LicenseKey.load_from_string(pubKey, f.read()) 108 | 109 | if license_key == None or not Helpers.IsOnRightMachine(license_key, v=2): 110 | print("NOTE: This license file does not belong to this machine.") 111 | else: 112 | print("Feature 1: " + str(license_key.f1)) 113 | print("License expires: " + str(license_key.expires)) 114 | ``` 115 | 116 | If you want to make sure that the license file is not too old, you can specify the maximum number of days as shown below (after 30 days, this method will return NoneType). 117 | 118 | ```python 119 | # read license file from file 120 | with open('licensefile.skm', 'r') as f: 121 | license_key = LicenseKey.load_from_string(pubKey, f.read(), 30) 122 | 123 | if license_key == None or not Helpers.IsOnRightMachine(license_key, v=2): 124 | print("NOTE: This license file does not belong to this machine.") 125 | else: 126 | print("Feature 1: " + str(license_key.f1)) 127 | print("License expires: " + str(license_key.expires)) 128 | ``` 129 | 130 | ### Floating licenses 131 | [Floating licenses](https://help.cryptolens.io/licensing-models/floating) can be enabled by setting the floatingTimeInterval. Optionally, you can also allow customers to exceed the bound by specifying the maxOverdraft. 132 | 133 | The code below has a floatingTimeInterval of 300 seconds and maxOverdraft set to 1. To support floating licenses with overdraft, the call to `Helpers.IsOnRightMachine(license, true, true)` needs two boolean flags to be set to true. 134 | 135 | ```python 136 | from licensing.models import * 137 | from licensing.methods import Key, Helpers 138 | 139 | RSAPubKey = "sGbvxwdlDbqFXOMlVUnAF5ew0t0WpPW7rFpI5jHQOFkht/326dvh7t74RYeMpjy357NljouhpTLA3a6idnn4j6c3jmPWBkjZndGsPL4Bqm+fwE48nKpGPjkj4q/yzT4tHXBTyvaBjA8bVoCTnu+LiC4XEaLZRThGzIn5KQXKCigg6tQRy0GXE13XYFVz/x1mjFbT9/7dS8p85n8BuwlY5JvuBIQkKhuCNFfrUxBWyu87CFnXWjIupCD2VO/GbxaCvzrRjLZjAngLCMtZbYBALksqGPgTUN7ZM24XbPWyLtKPaXF2i4XRR9u6eTj5BfnLbKAU5PIVfjIS+vNYYogteQ==AQAB" 140 | auth = "WyIyNTU1IiwiRjdZZTB4RmtuTVcrQlNqcSszbmFMMHB3aWFJTlBsWW1Mbm9raVFyRyJd==" 141 | 142 | result = Key.activate(token=auth,\ 143 | rsa_pub_key=RSAPubKey,\ 144 | product_id=3349, \ 145 | key="ICVLD-VVSZR-ZTICT-YKGXL",\ 146 | machine_code=Helpers.GetMachineCode(v=2),\ 147 | floating_time_interval=300,\ 148 | max_overdraft=1) 149 | 150 | if result[0] == None or not Helpers.IsOnRightMachine(result[0], is_floating_license=True, allow_overdraft=True, v=2): 151 | print("An error occurred: {0}".format(result[1])) 152 | else: 153 | print("Success") 154 | 155 | license_key = result[0] 156 | print("Feature 1: " + str(license_key.f1)) 157 | print("License expires: " + str(license_key.expires)) 158 | ``` 159 | 160 | ### Create Trial Key (verified trial) 161 | 162 | #### Idea 163 | 164 | A [trial key](https://help.cryptolens.io/examples/verified-trials) allows your users to evaluate some or all parts of your software for a limited period of time. The goal of trial keys is to set it up in such a way that you don’t need to manually create them, while still keeping everything secure. 165 | 166 | In Cryptolens, all trial keys are bound to the device that requested them, which helps to prevent users from using the trial after reinstalling their device. 167 | 168 | You can define which features should count as trial by [editing feature definitions](https://help.cryptolens.io/web-interface/feature-definitions) on the product page. 169 | 170 | #### Implementation 171 | 172 | The code below shows how to create trial key. If the trial key is successful, `trial_key[0]` will contain the license key string. We then need to call `Key.Activate` (as shown in the earlier examples) with the obtained license key to verify the license. 173 | 174 | ```python 175 | from licensing.models import * 176 | from licensing.methods import Key, Helpers 177 | 178 | trial_key = Key.create_trial_key("WyIzODQ0IiwiempTRWs4SnBKTTArYUh3WkwyZ0VwQkVyeTlUVkRWK2ZTOS8wcTBmaCJd", 3941, Helpers.GetMachineCode(v=2)) 179 | 180 | if trial_key[0] == None: 181 | print("An error occurred: {0}".format(trial_key[1])) 182 | 183 | 184 | RSAPubKey = "sGbvxwdlDbqFXOMlVUnAF5ew0t0WpPW7rFpI5jHQOFkht/326dvh7t74RYeMpjy357NljouhpTLA3a6idnn4j6c3jmPWBkjZndGsPL4Bqm+fwE48nKpGPjkj4q/yzT4tHXBTyvaBjA8bVoCTnu+LiC4XEaLZRThGzIn5KQXKCigg6tQRy0GXE13XYFVz/x1mjFbT9/7dS8p85n8BuwlY5JvuBIQkKhuCNFfrUxBWyu87CFnXWjIupCD2VO/GbxaCvzrRjLZjAngLCMtZbYBALksqGPgTUN7ZM24XbPWyLtKPaXF2i4XRR9u6eTj5BfnLbKAU5PIVfjIS+vNYYogteQ==AQAB" 185 | auth = "WyIyNTU1IiwiRjdZZTB4RmtuTVcrQlNqcSszbmFMMHB3aWFJTlBsWW1Mbm9raVFyRyJd==" 186 | 187 | result = Key.activate(token=auth,\ 188 | rsa_pub_key=RSAPubKey,\ 189 | product_id=3349, \ 190 | key=trial_key[0],\ 191 | machine_code=Helpers.GetMachineCode(v=2)) 192 | 193 | 194 | if result[0] == None or not Helpers.IsOnRightMachine(result[0], v=2): 195 | print("An error occurred: {0}".format(result[1])) 196 | else: 197 | print("Success") 198 | 199 | license_key = result[0] 200 | print("Feature 1: " + str(license_key.f1)) 201 | print("License expires: " + str(license_key.expires)) 202 | ``` 203 | 204 | ### License server or custom endpoint 205 | 206 | To forward requests to a local license server or a different API, you can set it using the `server_address` in HelperMethods, i.e., 207 | 208 | ```python 209 | HelperMethods.server_address = "http://localhost:8080/api/"; 210 | ``` 211 | It is important to include one */* in the end of the address as well as to attach the "api" suffix. 212 | 213 | ### Other settings 214 | 215 | #### Proxy 216 | 217 | If you have customers who want to use a proxy server, we recommend enabling the following setting before calling any other API method, such as Key.Activate. 218 | 219 | ```python 220 | HelperMethods.proxy_experimental = True 221 | ``` 222 | 223 | This will ensure that the underlying HTTP library (urllib) used by Cryptolens Python SDK will use the proxy configured on the OS level. The goal is to make this default behaviour in future versions of the library, once enough feedback is collected. 224 | 225 | #### SSL verification 226 | SSL verification can temporarily be disabled by adding the line below before any call to Key.Activate. 227 | 228 | ```python 229 | HelperMethods.verify_SSL = False 230 | ``` 231 | 232 | The Cryptolens Python SDK will verify that the license information has not changed since it left the server using your RSA Public Key. However, we recommend to keep this value unchanged. 233 | 234 | ### Possible errors 235 | 236 | #### The expiration date cannot be converted to a datetime object. Please try setting the period to a lower value. 237 | This error occurs when the timestamp for the expiration date received from the server exceeds the limit in Python. This typically occurs when the **Period** is set to a excessively large value, often to prevent the license from expiring. 238 | 239 | While Cryptolens requires a period (defaulted to 30) during the creation of a new license, this does not mark the license as time-limited. You can learn more about it [here](https://help.cryptolens.io/web-interface/keys-that-dont-expire). In essence, a license is treated as time-limited by either enforcing this in the Python code (e.g. if F1=true, the license is time-limited and so we check the expiration date against the current date, to see that it is still valid) or on the server side. On the server side, you can, for example, set up a feature that will automatically block expired licenses. You can read more about it [here](https://help.cryptolens.io/faq/index#blocking-expired-licenses). 240 | 241 | In sum, to solve this issue, you can either follow one of the methods described above or set the period to a smaller value. 242 | 243 | #### Could not contact the server. Error message: 244 | 245 | This error is thrown when the urllib library (a built in library in Python that we use to send HTTP requests) is unable to locate the CA files on the client machine. From our experience, this error occurs exclusively on Macs where the Python environment is incorrectly installed. 246 | 247 | To solve this temporarily for **testing purposes**, you could temporary disable SSL verifications as described in [here](#ssl-verification), however, we do not recommend this in a production scenario. Instead, a better solution is to fix the underlying issue preventing the Python environment from finding the CA files. 248 | 249 | This can be accomplished in at least two ways: 250 | 251 | ##### Using certifi 252 | Before calling any of the API methods (e.g. Key.activate), you can add the following code: 253 | 254 | ```python 255 | import certifi 256 | os.environ['SSL_CERT_FILE'] = certifi.where() 257 | ``` 258 | 259 | Please note that this requires `certifi` package to be installed. 260 | 261 | ##### Running a script in the Python environment 262 | An alternative is to run script in their environment that should fix the issue. You can read more about it in this thread: https://github.com/Cryptolens/cryptolens-python/issues/65 263 | 264 | ##### Summary 265 | The key takeaway is that it is better to address the issue with missing CA on the user side, since this issue will typically be user-specific. If that is not possible, you can use the code above to manually set the path to CA files. Although we have mentioned turning off SSL verification temporarily, it should not be used in production. `Key.activate` takes care of signature verification internally, but some other methods do not. 266 | 267 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /cryptolens_python2.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import socket 3 | import json 4 | 5 | import os 6 | 7 | """ 8 | The code below should not be changed. 9 | """ 10 | 11 | # -*- coding: utf-8 -*- 12 | """ 13 | Created on Wed Jan 23 10:12:13 2019 14 | 15 | @author: Artem Los 16 | """ 17 | import base64 18 | import urllib2 19 | import urllib 20 | import hashlib 21 | from subprocess import Popen, PIPE 22 | from urllib2 import URLError, HTTPError 23 | 24 | class HelperMethods: 25 | 26 | server_address = "https://app.cryptolens.io/api/" 27 | ironpython2730_legacy = False 28 | 29 | @staticmethod 30 | def get_SHA256(string): 31 | """ 32 | Compute the SHA256 signature of a string. 33 | """ 34 | return hashlib.sha256(string.encode("utf-8")).hexdigest() 35 | 36 | @staticmethod 37 | def I2OSP(x, xLen): 38 | if x > (1 << (8 * xLen)): 39 | return None 40 | Xrev = [] 41 | for _ in xrange(0, xLen): 42 | x, m = divmod(x, 256) 43 | Xrev.append(chr(m)) 44 | return "".join(reversed(Xrev)) 45 | 46 | @staticmethod 47 | def OS2IP(X): 48 | return int(X.encode("hex"), 16) 49 | 50 | @staticmethod 51 | def _OS2IP(X): 52 | x = 0 53 | a = 1 54 | l = len(X) 55 | for i in xrange(1, l+1): 56 | x += ord(X[l - i])*a 57 | a *= 256 58 | return x 59 | 60 | @staticmethod 61 | def RSAVP1((n,e), s): 62 | if s < 0 or n-1 < s: 63 | return None 64 | return pow(s, e, n) 65 | 66 | @staticmethod 67 | def EMSA_PKCS1_V15_ENCODE(M, emLen): 68 | import hashlib 69 | h = hashlib.sha256() 70 | h.update(M) 71 | H = h.digest() 72 | 73 | T = "".join([chr(x) for x in [0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20]]) + H 74 | tLen = len(T) 75 | if emLen < tLen + 11: 76 | return None 77 | PS = "".join([chr(0xff) for _ in range(emLen - tLen - 3)]) 78 | return "".join([chr(0x0), chr(0x1), PS, chr(0x0), T]) 79 | 80 | @staticmethod 81 | def RSAASSA_PKCS1_V15_VERIFY((n,e), M, S): 82 | s = HelperMethods.OS2IP(S) 83 | m = HelperMethods.RSAVP1((n,e), s) 84 | if m is None: return False 85 | EM = HelperMethods.I2OSP(m, 256) 86 | if EM is None: return False 87 | EM2 = HelperMethods.EMSA_PKCS1_V15_ENCODE(M, 256) 88 | if EM2 is None: return False 89 | 90 | try: 91 | import hmac 92 | return hmac.compare_digest(EM, EM2) 93 | except (ImportError, AttributeError): 94 | return EM == EM2 95 | 96 | 97 | @staticmethod 98 | def verify_signature(response, rsaPublicKey): 99 | """ 100 | Verifies a signature from .NET RSACryptoServiceProvider. 101 | """ 102 | 103 | modulus = base64.b64decode(rsaPublicKey.modulus) 104 | exponent = base64.b64decode(rsaPublicKey.exponent) 105 | message = base64.b64decode(response.license_key) 106 | signature = base64.b64decode(response.signature) 107 | 108 | n = HelperMethods.OS2IP(modulus) 109 | e = HelperMethods.OS2IP(exponent) 110 | 111 | return HelperMethods.RSAASSA_PKCS1_V15_VERIFY((n,e), message, signature) 112 | 113 | @staticmethod 114 | def int2base64(num): 115 | return base64.b64encode(int.to_bytes(num), byteorder='big') 116 | 117 | @staticmethod 118 | def base642int(string): 119 | return int.from_bytes(base64.b64decode((string)), byteorder='big') 120 | 121 | @staticmethod 122 | def send_request(method, params): 123 | """ 124 | Send a POST request to method in the Web API with the specified 125 | params and return the response string. 126 | 127 | method: the path of the method, eg. key/activate 128 | params: a dictionary of parameters 129 | """ 130 | 131 | if HelperMethods.ironpython2730_legacy: 132 | return HelperMethods.send_request_ironpythonlegacy(HelperMethods.server_address + method, \ 133 | urllib.urlencode(params)) 134 | else: 135 | return urllib2.urlopen(HelperMethods.server_address + method, \ 136 | urllib.urlencode(params)).read().decode("utf-8") 137 | 138 | @staticmethod 139 | def send_request_ironpythonlegacy(uri, parameters): 140 | """ 141 | IronPython 2.7.3 and earlier has a built in problem with 142 | urlib2 library when verifying certificates. This code calls a .NET 143 | library instead. 144 | """ 145 | from System.Net import WebRequest 146 | from System.IO import StreamReader 147 | from System.Text import Encoding 148 | 149 | request = WebRequest.Create(uri) 150 | 151 | request.ContentType = "application/x-www-form-urlencoded" 152 | request.Method = "POST" #work for post 153 | bytes = Encoding.ASCII.GetBytes(parameters) 154 | request.ContentLength = bytes.Length 155 | reqStream = request.GetRequestStream() 156 | reqStream.Write(bytes, 0, bytes.Length) 157 | reqStream.Close() 158 | 159 | response = request.GetResponse() 160 | result = StreamReader(response.GetResponseStream()).ReadToEnd() 161 | return result 162 | 163 | 164 | @staticmethod 165 | def start_process(command): 166 | 167 | process = Popen(command, stdout=PIPE) 168 | (output, err) = process.communicate() 169 | exit_code = process.wait() 170 | return output.decode("utf-8") 171 | 172 | @staticmethod 173 | def get_dbus_machine_id(): 174 | try: 175 | with open("/etc/machine-id") as f: 176 | return f.read().strip() 177 | except: 178 | pass 179 | try: 180 | with open("/var/lib/dbus/machine-id") as f: 181 | return f.read().strip() 182 | except: 183 | pass 184 | return "" 185 | 186 | @staticmethod 187 | def get_inodes(): 188 | import os 189 | files = ["/bin", "/etc", "/lib", "/root", "/sbin", "/usr", "/var"] 190 | inodes = [] 191 | for file in files: 192 | try: 193 | inodes.append(os.stat(file).st_ino) 194 | except: 195 | pass 196 | return "".join([str(x) for x in inodes]) 197 | 198 | 199 | @staticmethod 200 | def compute_machine_code(): 201 | return HelperMethods.get_dbus_machine_id() + HelperMethods.get_inodes() 202 | 203 | import platform 204 | import uuid 205 | import sys 206 | import json 207 | 208 | class Key: 209 | 210 | """ 211 | License key related methods. More docs: https://app.cryptolens.io/docs/api/v3/Key. 212 | """ 213 | 214 | @staticmethod 215 | def activate(token, rsa_pub_key, product_id, key, machine_code, fields_to_return = 0,\ 216 | metadata = False, floating_time_interval = 0,\ 217 | max_overdraft = 0, friendly_name=None): 218 | 219 | """ 220 | Calls the Activate method in Web API 3 and returns a tuple containing 221 | (LicenseKey, Message). If an error occurs, LicenseKey will be None. If 222 | everything went well, no message will be returned. 223 | 224 | More docs: https://app.cryptolens.io/docs/api/v3/Activate 225 | """ 226 | 227 | response = Response("","",0,"") 228 | 229 | try: 230 | response = Response.from_string(HelperMethods.send_request("key/activate", {"token":token,\ 231 | "ProductId":product_id,\ 232 | "key":key,\ 233 | "MachineCode":machine_code,\ 234 | "FieldsToReturn":fields_to_return,\ 235 | "metadata":metadata,\ 236 | "FloatingTimeInterval": floating_time_interval,\ 237 | "MaxOverdraft": max_overdraft,\ 238 | "FriendlyName" : friendly_name,\ 239 | "ModelVersion" : 2,\ 240 | "Sign":"True",\ 241 | "SignMethod":1})) 242 | except HTTPError as e: 243 | response = Response.from_string(e.read()) 244 | except URLError as e: 245 | return (None, "Could not contact the server. Error message: " + str(e)) 246 | except Exception: 247 | return (None, "Could not contact the server.") 248 | 249 | pubkey = RSAPublicKey.from_string(rsa_pub_key) 250 | 251 | if response.result == 1: 252 | return (None, response.message) 253 | else: 254 | try: 255 | if HelperMethods.verify_signature(response, pubkey): 256 | return (LicenseKey.from_response(response), response.message) 257 | else: 258 | return (None, "The signature check failed.") 259 | except Exception: 260 | return (None, "The signature check failed.") 261 | 262 | @staticmethod 263 | def get_key(token, rsa_pub_key, product_id, key, fields_to_return = 0,\ 264 | metadata = False, floating_time_interval = 0): 265 | 266 | """ 267 | Calls the GetKey method in Web API 3 and returns a tuple containing 268 | (LicenseKey, Message). If an error occurs, LicenseKey will be None. If 269 | everything went well, no message will be returned. 270 | 271 | More docs: https://app.cryptolens.io/docs/api/v3/GetKey 272 | """ 273 | 274 | response = Response("","",0,"") 275 | 276 | try: 277 | response = Response.from_string(HelperMethods.send_request("key/getkey", {"token":token,\ 278 | "ProductId":product_id,\ 279 | "key":key,\ 280 | "FieldsToReturn":fields_to_return,\ 281 | "metadata":metadata,\ 282 | "FloatingTimeInterval": floating_time_interval,\ 283 | "Sign":"True",\ 284 | "SignMethod":1})) 285 | except HTTPError as e: 286 | response = Response.from_string(e.read()) 287 | except URLError as e: 288 | return (None, "Could not contact the server. Error message: " + str(e)) 289 | except Exception: 290 | return (None, "Could not contact the server.") 291 | 292 | pubkey = RSAPublicKey.from_string(rsa_pub_key) 293 | 294 | if response.result == 1: 295 | return (None, response.message) 296 | else: 297 | try: 298 | if HelperMethods.verify_signature(response, pubkey): 299 | return (LicenseKey.from_response(response), response.message) 300 | else: 301 | return (None, "The signature check failed.") 302 | except Exception: 303 | return (None, "The signature check failed.") 304 | 305 | @staticmethod 306 | def create_trial_key(token, product_id, machine_code): 307 | """ 308 | Calls the CreateTrialKey method in Web API 3 and returns a tuple containing 309 | (LicenseKeyString, Message). If an error occurs, LicenseKeyString will be None. If 310 | everything went well, no message will be returned. 311 | 312 | More docs: https://app.cryptolens.io/docs/api/v3/CreateTrialKey 313 | """ 314 | 315 | response = "" 316 | 317 | try: 318 | response = HelperMethods.send_request("key/createtrialkey", {"token":token,\ 319 | "ProductId":product_id,\ 320 | "MachineCode":machine_code}) 321 | except HTTPError as e: 322 | response = e.read() 323 | except URLError as e: 324 | return (None, "Could not contact the server. Error message: " + str(e)) 325 | except Exception: 326 | return (None, "Could not contact the server.") 327 | 328 | jobj = json.loads(response) 329 | 330 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 331 | if jobj != None: 332 | return (None, jobj["message"]) 333 | else: 334 | return (None, "Could not contact the server.") 335 | 336 | try: 337 | return (jobj["key"], "") 338 | except: 339 | return (None, "An unexpected error occurred") 340 | 341 | @staticmethod 342 | def deactivate(token, product_id, key, machine_code, floating = False): 343 | """ 344 | Calls the Deactivate method in Web API 3 and returns a tuple containing 345 | (Success, Message). If an error occurs, Success will be False. If 346 | everything went well, Sucess is true and no message will be returned. 347 | 348 | More docs: https://app.cryptolens.io/docs/api/v3/Deactivate 349 | """ 350 | 351 | response = "" 352 | 353 | try: 354 | response = HelperMethods.send_request("key/deactivate", {"token":token,\ 355 | "ProductId":product_id,\ 356 | "Key" : key,\ 357 | "Floating" : floating,\ 358 | "MachineCode":machine_code}) 359 | except HTTPError as e: 360 | response = e.read() 361 | except URLError as e: 362 | return (None, "Could not contact the server. Error message: " + str(e)) 363 | except Exception: 364 | return (None, "Could not contact the server.") 365 | 366 | jobj = json.loads(response) 367 | 368 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 369 | if jobj != None: 370 | return (False, jobj["message"]) 371 | else: 372 | return (False, "Could not contact the server.") 373 | 374 | return (True, "") 375 | 376 | 377 | 378 | class Helpers: 379 | 380 | @staticmethod 381 | def GetMachineCode(): 382 | 383 | """ 384 | Get a unique identifier for this device. 385 | """ 386 | 387 | if "windows" in platform.platform().lower(): 388 | return HelperMethods.get_SHA256(HelperMethods.start_process(["cmd.exe", "/C", "wmic","csproduct", "get", "uuid"])) 389 | elif "darwin" in platform.platform().lower(): 390 | res = HelperMethods.start_process(["system_profiler","SPHardwareDataType"]).decode('utf-8') 391 | return HelperMethods.get_SHA256(res[res.index("UUID"):].strip()) 392 | elif "linux" in platform.platform(HelperMethods.compute_machine_code()): 393 | return HelperMethods.get_SHA256(HelperMethods.compute_machine_code()) 394 | else: 395 | return HelperMethods.get_SHA256(HelperMethods.compute_machine_code()) 396 | 397 | @staticmethod 398 | def IsOnRightMachine(license_key, is_floating_license = False, allow_overdraft=False, custom_machine_code = None): 399 | 400 | """ 401 | Check if the device is registered with the license key. 402 | """ 403 | 404 | current_mid = "" 405 | 406 | if custom_machine_code == None: 407 | current_mid = Helpers.GetMachineCode() 408 | else: 409 | current_mid = custom_machine_code 410 | 411 | if license_key.activated_machines == None: 412 | return False 413 | 414 | if is_floating_license: 415 | if len(license_key.activated_machines) == 1 and \ 416 | (license_key.activated_machines[0].Mid[9:] == current_mid or \ 417 | allow_overdraft and license_key.activated_machines[0].Mid[19:] == current_mid): 418 | return True 419 | else: 420 | for act_machine in license_key.activated_machines: 421 | if current_mid == act_machine.Mid: 422 | return True 423 | 424 | return False 425 | 426 | import xml.etree.ElementTree 427 | import json 428 | import base64 429 | import datetime 430 | import copy 431 | import time 432 | 433 | class ActivatedMachine: 434 | def __init__(self, IP, Mid, Time, FriendlyName = ""): 435 | self.IP = IP 436 | self.Mid = Mid 437 | 438 | # TODO: check if time is int, and convert to datetime in this case. 439 | self.Time = Time 440 | self.FriendlyName = FriendlyName 441 | 442 | class LicenseKey: 443 | 444 | def __init__(self, ProductId, ID, Key, Created, Expires, Period, F1, F2,\ 445 | F3, F4, F5, F6, F7, F8, Notes, Block, GlobalId, Customer, \ 446 | ActivatedMachines, TrialActivation, MaxNoOfMachines, \ 447 | AllowedMachines, DataObjects, SignDate, RawResponse): 448 | 449 | self.product_id = ProductId 450 | self.id = ID 451 | self.key = Key 452 | self.created = Created 453 | self.expires = Expires 454 | self.period = Period 455 | self.f1 = F1 456 | self.f2 = F2 457 | self.f3 = F3 458 | self.f4 = F4 459 | self.f5 = F5 460 | self.f6 = F6 461 | self.f7 = F7 462 | self.f8 = F8 463 | self.notes = Notes 464 | self.block = Block 465 | self.global_id = GlobalId 466 | self.customer = Customer 467 | self.activated_machines = ActivatedMachines 468 | self.trial_activation = TrialActivation 469 | self.max_no_of_machines = MaxNoOfMachines 470 | self.allowed_machines = AllowedMachines 471 | self.data_objects = DataObjects 472 | self.sign_date = SignDate 473 | self.raw_response = RawResponse 474 | 475 | @staticmethod 476 | def from_response(response): 477 | 478 | if response.result == "1": 479 | raise ValueError("The response did not contain any license key object since it was unsuccessful. Message '{0}'.".format(response.message)) 480 | 481 | obj = json.loads(base64.b64decode(response.license_key).decode('utf-8')) 482 | 483 | return LicenseKey(obj["ProductId"], obj["ID"], obj["Key"], datetime.datetime.fromtimestamp(obj["Created"]),\ 484 | datetime.datetime.fromtimestamp(obj["Expires"]), obj["Period"], obj["F1"], obj["F2"], \ 485 | obj["F3"], obj["F4"],obj["F5"],obj["F6"], obj["F7"], \ 486 | obj["F8"], obj["Notes"], obj["Block"], obj["GlobalId"],\ 487 | obj["Customer"], LicenseKey.__load_activated_machines(obj["ActivatedMachines"]), obj["TrialActivation"], \ 488 | obj["MaxNoOfMachines"], obj["AllowedMachines"], obj["DataObjects"], \ 489 | datetime.datetime.fromtimestamp(obj["SignDate"]), response) 490 | 491 | def save_as_string(self): 492 | """ 493 | Save the license as a string that can later be read by load_from_string. 494 | """ 495 | res = copy.copy(self.raw_response.__dict__) 496 | res["licenseKey"] = res["license_key"] 497 | res.pop("license_key", None) 498 | return json.dumps(res) 499 | 500 | @staticmethod 501 | def load_from_string(rsa_pub_key, string, signature_expiration_interval = -1): 502 | """ 503 | Loads a license from a string generated by save_as_string. 504 | Note: if an error occurs, None will be returned. An error can occur 505 | if the license string has been tampered with or if the public key is 506 | incorrectly formatted. 507 | 508 | :param signature_expiration_interval: If the license key was signed, 509 | this method will check so that no more than "signatureExpirationInterval" 510 | days have passed since the last activation. 511 | """ 512 | 513 | response = Response("","","","") 514 | 515 | try: 516 | response = Response.from_string(string) 517 | except Exception as ex: 518 | return None 519 | 520 | if response.result == "1": 521 | return None 522 | else: 523 | try: 524 | pubKey = RSAPublicKey.from_string(rsa_pub_key) 525 | if HelperMethods.verify_signature(response, pubKey): 526 | 527 | licenseKey = LicenseKey.from_response(response) 528 | 529 | if signature_expiration_interval > 0 and \ 530 | (licenseKey.sign_date + datetime.timedelta(days=1*signature_expiration_interval) < datetime.datetime.utcnow()): 531 | return None 532 | 533 | return licenseKey 534 | else: 535 | return None 536 | except Exception: 537 | return None 538 | 539 | @staticmethod 540 | def __load_activated_machines(obj): 541 | 542 | if obj == None: 543 | return None 544 | 545 | arr = [] 546 | 547 | for item in obj: 548 | arr.append(ActivatedMachine(**item)) 549 | 550 | return arr 551 | 552 | class Response: 553 | 554 | def __init__(self, license_key, signature, result, message): 555 | self.license_key = license_key 556 | self.signature = signature 557 | self.result = result 558 | self.message = message 559 | 560 | @staticmethod 561 | def from_string(responseString): 562 | obj = json.loads(responseString) 563 | 564 | licenseKey = "" 565 | signature = "" 566 | result = 0 567 | message = "" 568 | 569 | if "licenseKey" in obj: 570 | licenseKey = obj["licenseKey"] 571 | 572 | if "signature" in obj: 573 | signature = obj["signature"] 574 | 575 | if "message" in obj: 576 | message = obj["message"] 577 | 578 | if "result" in obj: 579 | result = obj["result"] 580 | else: 581 | result = 1 582 | 583 | return Response(licenseKey, signature, result, message) 584 | 585 | class RSAPublicKey: 586 | 587 | def __init__(self, modulus, exponent): 588 | self.modulus = modulus 589 | self.exponent = exponent 590 | 591 | @staticmethod 592 | def from_string(rsaPubKeyString): 593 | """ 594 | The rsaPubKeyString can be found at https://app.cryptolens.io/User/Security. 595 | It should be of the following format: 596 | ...AQAB 597 | """ 598 | rsaKey = xml.etree.ElementTree.fromstring(rsaPubKeyString) 599 | return RSAPublicKey(rsaKey.find('Modulus').text, rsaKey.find('Exponent').text) 600 | -------------------------------------------------------------------------------- /licensing/methods.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Jan 24 08:06:39 2019 4 | 5 | @author: Artem Los 6 | """ 7 | 8 | import platform 9 | import uuid 10 | import sys 11 | from licensing.internal import HelperMethods 12 | from licensing.models import * 13 | import json 14 | from urllib.error import URLError, HTTPError 15 | 16 | class Key: 17 | 18 | """ 19 | License key related methods. More docs: https://app.cryptolens.io/docs/api/v3/Key. 20 | """ 21 | 22 | @staticmethod 23 | def activate(token, rsa_pub_key, product_id, key, machine_code, fields_to_return = 0,\ 24 | metadata = False, floating_time_interval = 0,\ 25 | max_overdraft = 0, friendly_name = None): 26 | 27 | """ 28 | Calls the Activate method in Web API 3 and returns a tuple containing 29 | (LicenseKey, Message). If an error occurs, LicenseKey will be None. If 30 | everything went well, no message will be returned. 31 | 32 | More docs: https://app.cryptolens.io/docs/api/v3/Activate 33 | """ 34 | 35 | response = Response("","",0,"") 36 | 37 | try: 38 | response = Response.from_string(HelperMethods.send_request("key/activate", {"token":token,\ 39 | "ProductId":product_id,\ 40 | "key":key,\ 41 | "MachineCode":machine_code,\ 42 | "FieldsToReturn":fields_to_return,\ 43 | "metadata":metadata,\ 44 | "FloatingTimeInterval": floating_time_interval,\ 45 | "MaxOverdraft": max_overdraft,\ 46 | "FriendlyName" : friendly_name,\ 47 | "ModelVersion": 3 ,\ 48 | "Sign":"True",\ 49 | "SignMethod":1})) 50 | except HTTPError as e: 51 | response = Response.from_string(e.read()) 52 | except URLError as e: 53 | return (None, "Could not contact the server. Error message: " + str(e)) 54 | except Exception: 55 | return (None, "Could not contact the server.") 56 | 57 | pubkey = RSAPublicKey.from_string(rsa_pub_key) 58 | 59 | if response.result == 1: 60 | return (None, response.message) 61 | else: 62 | try: 63 | if HelperMethods.verify_signature(response, pubkey): 64 | if metadata: 65 | 66 | try: 67 | metadata_s = HelperMethods.verify_signature_metadata(response.metadata["signature"], pubkey) 68 | 69 | if metadata_s[0]: 70 | return (LicenseKey.from_response(response), response.message, json.loads(metadata_s[1])) 71 | else: 72 | return (LicenseKey.from_response(response), response.message, "Signature check for metadata object failed.") 73 | except: 74 | return (LicenseKey.from_response(response), response.message, "Signature check for metadata object failed.") 75 | 76 | 77 | else: 78 | return (LicenseKey.from_response(response), response.message) 79 | else: 80 | return (None, "The signature check failed.") 81 | except Exception as ex: 82 | return (None, "An error occured: {0}".format(ex)) 83 | 84 | @staticmethod 85 | def get_key(token, rsa_pub_key, product_id, key, fields_to_return = 0,\ 86 | metadata = False, floating_time_interval = 0): 87 | 88 | """ 89 | Calls the GetKey method in Web API 3 and returns a tuple containing 90 | (LicenseKey, Message). If an error occurs, LicenseKey will be None. If 91 | everything went well, no message will be returned. 92 | 93 | More docs: https://app.cryptolens.io/docs/api/v3/GetKey 94 | """ 95 | 96 | response = Response("","",0,"") 97 | 98 | try: 99 | response = Response.from_string(HelperMethods.send_request("key/getkey", {"token":token,\ 100 | "ProductId":product_id,\ 101 | "key":key,\ 102 | "FieldsToReturn":fields_to_return,\ 103 | "metadata":metadata,\ 104 | "FloatingTimeInterval": floating_time_interval,\ 105 | "Sign":"True",\ 106 | "ModelVersion": 3 ,\ 107 | "SignMethod":1})) 108 | except HTTPError as e: 109 | response = Response.from_string(e.read()) 110 | except URLError as e: 111 | return (None, "Could not contact the server. Error message: " + str(e)) 112 | except Exception: 113 | return (None, "Could not contact the server.") 114 | 115 | pubkey = RSAPublicKey.from_string(rsa_pub_key) 116 | 117 | if response.result == 1: 118 | return (None, response.message) 119 | else: 120 | try: 121 | if HelperMethods.verify_signature(response, pubkey): 122 | return (LicenseKey.from_response(response), response.message) 123 | else: 124 | return (None, "The signature check failed.") 125 | except Exception: 126 | return (None, "The signature check failed.") 127 | 128 | @staticmethod 129 | def create_trial_key(token, product_id, machine_code, friendly_name= ""): 130 | """ 131 | Calls the CreateTrialKey method in Web API 3 and returns a tuple containing 132 | (LicenseKeyString, Message). If an error occurs, LicenseKeyString will be None. If 133 | everything went well, no message will be returned. 134 | 135 | More docs: https://app.cryptolens.io/docs/api/v3/CreateTrialKey 136 | """ 137 | 138 | response = "" 139 | 140 | try: 141 | response = HelperMethods.send_request("key/createtrialkey", {"token":token,\ 142 | "ProductId":product_id,\ 143 | "MachineCode":machine_code,\ 144 | "FriendlyName":friendly_name}) 145 | except HTTPError as e: 146 | response = e.read() 147 | except URLError as e: 148 | return (None, "Could not contact the server. Error message: " + str(e)) 149 | except Exception: 150 | return (None, "Could not contact the server.") 151 | 152 | jobj = json.loads(response) 153 | 154 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 155 | if jobj != None: 156 | return (None, jobj["message"]) 157 | else: 158 | return (None, "Could not contact the server.") 159 | 160 | return (jobj["key"], "") 161 | 162 | @staticmethod 163 | def create_key(token, product_id, period = 0,\ 164 | f1=False,\ 165 | f2=False,\ 166 | f3=False,\ 167 | f4=False,\ 168 | f5=False,\ 169 | f6=False,\ 170 | f7=False,\ 171 | f8=False,\ 172 | notes="",\ 173 | block=False,\ 174 | customer_id=0,\ 175 | new_customer=False,\ 176 | add_or_use_existing_customer=False,\ 177 | trial_activation=False,\ 178 | max_no_of_machines=0,\ 179 | no_of_keys=1,\ 180 | name = None,\ 181 | email = None,\ 182 | company_name=None,\ 183 | enable_customer_association = False,\ 184 | allow_activation_management = False ): 185 | """ 186 | This method allows you to create a new license key. The license can 187 | either be standalone or associated to a specific customer. It is also 188 | possible to add a new customer and associate it with the newly created 189 | license using NewCustomer parameter. If you would like to avoid 190 | duplicates based on the email, you can use the AddOrUseExistingCustomer 191 | parameter. 192 | 193 | The parameters "name", "email", "company_name", "enable_customer_association" 194 | and "allow_activation_management" are used to create a new customer (or update an existing one) 195 | and automatically associate it with the newly created license. Please note that you need to use an 196 | access token with both "CreateKey" and "AddCustomer" permissions. Moreover, either 197 | the parameter "new_customer" or "add_or_use_existing_customer" need to be set to True. 198 | 199 | More docs: https://app.cryptolens.io/docs/api/v3/CreateKey/ 200 | """ 201 | 202 | response = "" 203 | 204 | try: 205 | response = HelperMethods.send_request("key/createkey", {"token":token,\ 206 | "ProductId":product_id,\ 207 | "Period":period,\ 208 | "F1": f1,\ 209 | "F2": f2,\ 210 | "F3": f3,\ 211 | "F4": f4,\ 212 | "F5": f5,\ 213 | "F6": f6,\ 214 | "F7": f7,\ 215 | "F8": f8,\ 216 | "Notes": notes,\ 217 | "Block": block,\ 218 | "CustomerId": customer_id,\ 219 | "NewCustomer": new_customer,\ 220 | "AddOrUseExistingCustomer": add_or_use_existing_customer,\ 221 | "TrialActivation": trial_activation,\ 222 | "MaxNoOfMachines": max_no_of_machines,\ 223 | "NoOfKeys":no_of_keys,\ 224 | "Name": name,\ 225 | "Email": email,\ 226 | "CompanyName": company_name,\ 227 | "EnableCustomerAssociation": enable_customer_association,\ 228 | "AllowActivationManagement": allow_activation_management}) 229 | except HTTPError as e: 230 | response = e.read() 231 | except URLError as e: 232 | return (None, "Could not contact the server. Error message: " + str(e)) 233 | except Exception: 234 | return (None, "Could not contact the server.") 235 | 236 | jobj = json.loads(response) 237 | 238 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 239 | if jobj != None: 240 | return (None, jobj["message"]) 241 | else: 242 | return (None, "Could not contact the server.") 243 | 244 | return (jobj, "") 245 | 246 | 247 | @staticmethod 248 | def deactivate(token, product_id, key, machine_code, floating = False): 249 | """ 250 | Calls the Deactivate method in Web API 3 and returns a tuple containing 251 | (Success, Message). If an error occurs, Success will be False. If 252 | everything went well, Sucess is true and no message will be returned. 253 | 254 | More docs: https://app.cryptolens.io/docs/api/v3/Deactivate 255 | """ 256 | 257 | response = "" 258 | 259 | try: 260 | response = HelperMethods.send_request("key/deactivate", {"token":token,\ 261 | "ProductId":product_id,\ 262 | "Key" : key,\ 263 | "Floating" : floating,\ 264 | "MachineCode":machine_code}) 265 | except HTTPError as e: 266 | response = e.read() 267 | except URLError as e: 268 | return (None, "Could not contact the server. Error message: " + str(e)) 269 | except Exception: 270 | return (None, "Could not contact the server.") 271 | 272 | jobj = json.loads(response) 273 | 274 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 275 | if jobj != None: 276 | return (False, jobj["message"]) 277 | else: 278 | return (False, "Could not contact the server.") 279 | 280 | return (True, "") 281 | 282 | 283 | @staticmethod 284 | def extend_license(token, product_id, key, no_of_days): 285 | """ 286 | This method will extend a license by a certain amount of days. 287 | If the key algorithm in the product is SKGL, the key string will 288 | be changed if necessary. Otherwise, if SKM15 is used, the key will 289 | stay the same. More about the way this method works in Remarks. 290 | 291 | More docs: https://app.cryptolens.io/docs/api/v3/ExtendLicense 292 | """ 293 | 294 | response = "" 295 | 296 | try: 297 | response = HelperMethods.send_request("key/ExtendLicense", {"token":token,\ 298 | "ProductId":product_id,\ 299 | "Key" : key,\ 300 | "NoOfDays" : no_of_days}) 301 | except HTTPError as e: 302 | response = e.read() 303 | except URLError as e: 304 | return (None, "Could not contact the server. Error message: " + str(e)) 305 | except Exception: 306 | return (None, "Could not contact the server.") 307 | 308 | jobj = json.loads(response) 309 | 310 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 311 | if jobj != None: 312 | return (False, jobj["message"]) 313 | else: 314 | return (False, "Could not contact the server.") 315 | 316 | return (True, jobj["message"]) 317 | 318 | @staticmethod 319 | def change_customer(token, product_id, key, customer_id): 320 | """ 321 | This method will change the customer associated with a license. 322 | If the customer is not specified (for example, if CustomerId=0) or 323 | the customer with the provided ID does not exist, any customer that 324 | was previously associated with the license will be dissociated. 325 | 326 | More docs: https://app.cryptolens.io/docs/api/v3/ChangeCustomer 327 | """ 328 | 329 | response = "" 330 | 331 | try: 332 | response = HelperMethods.send_request("key/ChangeCustomer", {"token":token,\ 333 | "ProductId":product_id,\ 334 | "Key" : key,\ 335 | "CustomerId" : customer_id}) 336 | except HTTPError as e: 337 | response = e.read() 338 | except URLError as e: 339 | return (None, "Could not contact the server. Error message: " + str(e)) 340 | except Exception: 341 | return (None, "Could not contact the server.") 342 | 343 | jobj = json.loads(response) 344 | 345 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 346 | if jobj != None: 347 | return (False, jobj["message"]) 348 | else: 349 | return (False, "Could not contact the server.") 350 | 351 | return (True, jobj["message"]) 352 | 353 | @staticmethod 354 | def unblock_key(token, product_id, key): 355 | """ 356 | This method will unblock a specific license key to ensure that it can 357 | be accessed by the Key.Activate method. 358 | To do the reverse, you can use the BlockKey method. 359 | 360 | More docs: https://app.cryptolens.io/docs/api/v3/UnblockKey 361 | """ 362 | 363 | response = "" 364 | 365 | try: 366 | response = HelperMethods.send_request("/key/UnblockKey", {"token":token,\ 367 | "ProductId":product_id,\ 368 | "Key" : key}) 369 | except HTTPError as e: 370 | response = e.read() 371 | except URLError as e: 372 | return (None, "Could not contact the server. Error message: " + str(e)) 373 | except Exception: 374 | return (None, "Could not contact the server.") 375 | 376 | jobj = json.loads(response) 377 | 378 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 379 | if jobj != None: 380 | return (False, jobj["message"]) 381 | else: 382 | return (False, "Could not contact the server.") 383 | 384 | return (True, jobj["message"]) 385 | 386 | def block_key(token, product_id, key): 387 | """ 388 | This method will block a specific license key to ensure that it will 389 | no longer be possible to activate it. Note, it will still be possible 390 | to access the license key using the GetKey method. 391 | To do the reverse, you can use the Unblock Key method. 392 | 393 | More docs: https://app.cryptolens.io/docs/api/v3/BlockKey 394 | """ 395 | 396 | response = "" 397 | 398 | try: 399 | response = HelperMethods.send_request("/key/BlockKey", {"token":token,\ 400 | "ProductId":product_id,\ 401 | "Key" : key}) 402 | except HTTPError as e: 403 | response = e.read() 404 | except URLError as e: 405 | return (None, "Could not contact the server. Error message: " + str(e)) 406 | except Exception: 407 | return (None, "Could not contact the server.") 408 | 409 | jobj = json.loads(response) 410 | 411 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 412 | if jobj != None: 413 | return (False, jobj["message"]) 414 | else: 415 | return (False, "Could not contact the server.") 416 | 417 | return (True, jobj["message"]) 418 | 419 | def machine_lock_limit(token, product_id, key, number_of_machines): 420 | """ 421 | This method will change the maximum number of machine codes that 422 | a license key can have. 423 | 424 | More docs: https://app.cryptolens.io/docs/api/v3/MachineLockLimit 425 | """ 426 | 427 | response = "" 428 | 429 | try: 430 | response = HelperMethods.send_request("/key/MachineLockLimit", {"token":token,\ 431 | "ProductId":product_id,\ 432 | "Key" : key,\ 433 | "NumberOfMachines": number_of_machines}) 434 | except HTTPError as e: 435 | response = e.read() 436 | except URLError as e: 437 | return (None, "Could not contact the server. Error message: " + str(e)) 438 | except Exception: 439 | return (None, "Could not contact the server.") 440 | 441 | jobj = json.loads(response) 442 | 443 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 444 | if jobj != None: 445 | return (False, jobj["message"]) 446 | else: 447 | return (False, "Could not contact the server.") 448 | 449 | return (True, jobj["message"]) 450 | 451 | def change_notes(token, product_id, key, notes): 452 | """ 453 | This method will change the content of the notes field of 454 | a given license key. 455 | 456 | More docs: https://app.cryptolens.io/docs/api/v3/ChangeNotes 457 | """ 458 | 459 | response = "" 460 | 461 | try: 462 | response = HelperMethods.send_request("/key/ChangeNotes", {"token":token,\ 463 | "ProductId":product_id,\ 464 | "Key" : key,\ 465 | "Notes": notes}) 466 | except HTTPError as e: 467 | response = e.read() 468 | except URLError as e: 469 | return (None, "Could not contact the server. Error message: " + str(e)) 470 | except Exception: 471 | return (None, "Could not contact the server.") 472 | 473 | jobj = json.loads(response) 474 | 475 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 476 | if jobj != None: 477 | return (False, jobj["message"]) 478 | else: 479 | return (False, "Could not contact the server.") 480 | 481 | return (True, jobj["message"]) 482 | 483 | def change_reseller(token, product_id, key, reseller_id): 484 | """ 485 | This method will change the reseller of a license. If the reseller is 486 | not specified (for example, if ResellerId=0) or the reseller with the 487 | provided ID does not exist, any reseller that was previously associated 488 | with the license will be dissociated. 489 | 490 | More docs: https://app.cryptolens.io/docs/api/v3/ChangeReseller 491 | """ 492 | 493 | response = "" 494 | 495 | try: 496 | response = HelperMethods.send_request("/key/ChangeReseller", {"token":token,\ 497 | "ProductId":product_id,\ 498 | "Key" : key,\ 499 | "ResellerId": reseller_id}) 500 | except HTTPError as e: 501 | response = e.read() 502 | except URLError as e: 503 | return (None, "Could not contact the server. Error message: " + str(e)) 504 | except Exception: 505 | return (None, "Could not contact the server.") 506 | 507 | jobj = json.loads(response) 508 | 509 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 510 | if jobj != None: 511 | return (False, jobj["message"]) 512 | else: 513 | return (False, "Could not contact the server.") 514 | 515 | return (True, jobj["message"]) 516 | 517 | def create_key_from_template(token, license_template_id): 518 | """ 519 | This method will create a license key based on a License Template. 520 | If you want to see all the defined license templates through the API, 521 | this can be accomplished with Get License Templates. An alternative is 522 | to call the Create Key method, which allows you to specify all the 523 | parameters yourself. Note: the "feature lock" field in the access token 524 | can be used to restrict which license tempalte id can be used. 525 | 526 | More docs: https://app.cryptolens.io/docs/api/v3/CreateKeyFromTemplate 527 | """ 528 | 529 | response = "" 530 | 531 | try: 532 | response = HelperMethods.send_request("/key/CreateKeyFromTemplate", {"token":token,\ 533 | "LicenseTemplateId": license_template_id}) 534 | except HTTPError as e: 535 | response = e.read() 536 | except URLError as e: 537 | return (None, "Could not contact the server. Error message: " + str(e)) 538 | except Exception: 539 | return (None, "Could not contact the server.") 540 | 541 | jobj = json.loads(response) 542 | 543 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 544 | if jobj != None: 545 | return (False, jobj["message"]) 546 | else: 547 | return (False, "Could not contact the server.") 548 | 549 | return (jobj["key"], jobj["rawResponse"], jobj["message"]) 550 | 551 | def add_feature(token, product_id, key, feature): 552 | """ 553 | This method will set a certain feature (F1..F8) to true. 554 | If the key algorithm in the product is SKGL, the key string will be 555 | changed if necessary. Otherwise, if SKM15 is used, the key will stay 556 | the same. To do the reverse, please see RemoveFeature. 557 | 558 | More docs: https://app.cryptolens.io/docs/api/v3/AddFeature 559 | """ 560 | 561 | response = "" 562 | 563 | try: 564 | response = HelperMethods.send_request("/key/AddFeature", {"token":token,\ 565 | "ProductId":product_id,\ 566 | "Key" : key,\ 567 | "Feature" : feature}) 568 | except HTTPError as e: 569 | response = e.read() 570 | except URLError as e: 571 | return (None, "Could not contact the server. Error message: " + str(e)) 572 | except Exception: 573 | return (None, "Could not contact the server.") 574 | 575 | jobj = json.loads(response) 576 | 577 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 578 | if jobj != None: 579 | return (False, jobj["message"]) 580 | else: 581 | return (False, "Could not contact the server.") 582 | 583 | return (True, jobj["message"]) 584 | 585 | def remove_feature(token, product_id, key, feature): 586 | """ 587 | This method will set a certain feature (F1..F8) to false. If the key 588 | algorithm in the product is SKGL, the key string will be changed if 589 | necessary. Otherwise, if SKM15 is used, the key will stay the same. 590 | To do the reverse, please see AddFeature. 591 | 592 | More docs: https://app.cryptolens.io/docs/api/v3/RemoveFeature 593 | """ 594 | 595 | response = "" 596 | 597 | try: 598 | response = HelperMethods.send_request("/key/RemoveFeature", {"token":token,\ 599 | "ProductId":product_id,\ 600 | "Key" : key,\ 601 | "Feature" : feature}) 602 | except HTTPError as e: 603 | response = e.read() 604 | except URLError as e: 605 | return (None, "Could not contact the server. Error message: " + str(e)) 606 | except Exception: 607 | return (None, "Could not contact the server.") 608 | 609 | jobj = json.loads(response) 610 | 611 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 612 | if jobj != None: 613 | return (False, jobj["message"]) 614 | else: 615 | return (False, "Could not contact the server.") 616 | 617 | return (True, jobj["message"]) 618 | 619 | class AI: 620 | 621 | @staticmethod 622 | def get_web_api_log(token, product_id = 0, key = "", machine_code="", friendly_name = "",\ 623 | limit = 10, starting_after = 0, ending_before=0, order_by=""): 624 | 625 | """ 626 | This method will retrieve a list of Web API Logs. All events that get 627 | logged are related to a change of a license key or data object, eg. when 628 | license key gets activated or when a property of data object changes. More details 629 | about the method that was called are specified in the State field. 630 | 631 | More docs: https://app.cryptolens.io/docs/api/v3/GetWebAPILog 632 | """ 633 | 634 | response = "" 635 | 636 | try: 637 | response = HelperMethods.send_request("ai/getwebapilog", {"token":token,\ 638 | "ProductId":product_id,\ 639 | "Key":key,\ 640 | "MachineCode":machine_code,\ 641 | "FriendlyName":friendly_name,\ 642 | "Limit": limit,\ 643 | "StartingAfter": starting_after,\ 644 | "OrderBy" : order_by,\ 645 | "EndingBefore": ending_before}) 646 | except HTTPError as e: 647 | response = e.read() 648 | except URLError as e: 649 | return (None, "Could not contact the server. Error message: " + str(e)) 650 | except Exception: 651 | return (None, "Could not contact the server.") 652 | 653 | jobj = json.loads(response) 654 | 655 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 656 | if jobj != None: 657 | return (None, jobj["message"]) 658 | else: 659 | return (None, "Could not contact the server.") 660 | 661 | return (jobj["logs"], "") 662 | 663 | @staticmethod 664 | def get_events(token, limit = 10, starting_after = 0, product_id = 0,\ 665 | key = "", metadata = ""): 666 | 667 | """ 668 | This method will retrieve events that were registered using Register event method. 669 | 670 | More docs: https://app.cryptolens.io/api/ai/GetEvents 671 | """ 672 | 673 | response = "" 674 | 675 | try: 676 | response = HelperMethods.send_request("ai/GetEvents", {"token":token,\ 677 | "ProductId":product_id,\ 678 | "Key" : key,\ 679 | "Limit": limit,\ 680 | "StartingAfter": starting_after}) 681 | except HTTPError as e: 682 | response = e.read() 683 | except URLError as e: 684 | return (None, "Could not contact the server. Error message: " + str(e)) 685 | except Exception: 686 | return (None, "Could not contact the server.") 687 | 688 | jobj = json.loads(response) 689 | 690 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 691 | if jobj != None: 692 | return (None, jobj["message"]) 693 | else: 694 | return (None, "Could not contact the server.") 695 | 696 | return (jobj["events"], "") 697 | 698 | @staticmethod 699 | def register_event(token, product_id=0, key="", machine_code="", feature_name ="",\ 700 | event_name="", value=0, currency="", metadata=""): 701 | """ 702 | This method will register an event that has occured in either 703 | the client app (eg. start of a certain feature or interaction 704 | within a feature) or in a third party provider (eg. a payment 705 | has occured, etc). 706 | 707 | Note: You can either use this method standalone (eg. by only 708 | providing a machine code/device identifier) or together with 709 | Cryptolens Licensing module (which requires productId and 710 | optionally keyid to be set). The more information that is 711 | provided, the better insights can be provided. 712 | 713 | More docs: https://app.cryptolens.io/api/ai/RegisterEvent 714 | """ 715 | 716 | response = "" 717 | 718 | try: 719 | response = HelperMethods.send_request("/ai/RegisterEvent", {"token":token,\ 720 | "ProductId":product_id,\ 721 | "Key" : key,\ 722 | "MachineCode" : machine_code,\ 723 | "FeatureName" : feature_name,\ 724 | "EventName": event_name,\ 725 | "Value" : value,\ 726 | "Currency": currency,\ 727 | "Metadata" : metadata}) 728 | except HTTPError as e: 729 | response = e.read() 730 | except URLError as e: 731 | return (None, "Could not contact the server. Error message: " + str(e)) 732 | except Exception: 733 | return (None, "Could not contact the server.") 734 | 735 | jobj = json.loads(response) 736 | 737 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 738 | if jobj != None: 739 | return (False, jobj["message"]) 740 | else: 741 | return (False, "Could not contact the server.") 742 | 743 | return (True, jobj["message"]) 744 | 745 | class Message: 746 | 747 | @staticmethod 748 | def get_messages(token, channel="", time=0): 749 | 750 | """ 751 | This method will return a list of messages that were broadcasted. 752 | You can create new messages here. Messages can be filtered based on the time and the channel. 753 | 754 | More docs: https://app.cryptolens.io/docs/api/v3/GetMessages 755 | """ 756 | 757 | try: 758 | response = HelperMethods.send_request("/message/getmessages/", {"token":token, "Channel": channel, "Time": time}) 759 | except HTTPError as e: 760 | response = e.read() 761 | except URLError as e: 762 | return (None, "Could not contact the server. Error message: " + str(e)) 763 | except Exception: 764 | return (None, "Could not contact the server.") 765 | 766 | jobj = json.loads(response) 767 | 768 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 769 | if jobj != None: 770 | return (None, jobj["message"]) 771 | else: 772 | return (None, "Could not contact the server.") 773 | 774 | return (jobj["messages"], "") 775 | 776 | 777 | def create_message(token, content="", channel="", time=0): 778 | 779 | """ 780 | This method will create a new message. 781 | This method requires Edit Messages permission. 782 | 783 | More docs: https://app.cryptolens.io/docs/api/v3/CreateMessage 784 | """ 785 | 786 | try: 787 | response = HelperMethods.send_request("/message/CreateMessage/", {"token":token, "Channel": channel,"Content":content, "Time": time}) 788 | except HTTPError as e: 789 | response = e.read() 790 | except URLError as e: 791 | return (None, "Could not contact the server. Error message: " + str(e)) 792 | except Exception: 793 | return (None, "Could not contact the server.") 794 | 795 | jobj = json.loads(response) 796 | 797 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 798 | if jobj != None: 799 | return (None, jobj["message"]) 800 | else: 801 | return (None, "Could not contact the server.") 802 | 803 | return (jobj["messageId"], "") 804 | 805 | def remove_message(token, messageId): 806 | 807 | """ 808 | This method will remove a message that was previously broadcasted. 809 | This method requires Edit Messages permission. 810 | 811 | More docs: https://app.cryptolens.io/docs/api/v3/RemoveMessage 812 | """ 813 | 814 | try: 815 | response = HelperMethods.send_request("/message/RemoveMessage/", {"token":token, "Id": messageId}) 816 | except HTTPError as e: 817 | response = e.read() 818 | except URLError as e: 819 | return (None, "Could not contact the server. Error message: " + str(e)) 820 | except Exception: 821 | return (None, "Could not contact the server.") 822 | 823 | jobj = json.loads(response) 824 | 825 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 826 | if jobj != None: 827 | return (None, jobj["message"]) 828 | else: 829 | return (None, "Could not contact the server.") 830 | 831 | return (True, "") 832 | 833 | 834 | class Product: 835 | 836 | @staticmethod 837 | def get_products(token): 838 | 839 | """ 840 | This method will return the list of products. Each product contains fields such as 841 | the name and description, as well feature definitions and data objects. All the fields 842 | of a product are available here: https://app.cryptolens.io/docs/api/v3/model/Product 843 | 844 | More docs: https://app.cryptolens.io/docs/api/v3/GetProducts 845 | """ 846 | 847 | try: 848 | response = HelperMethods.send_request("/product/getproducts/", {"token":token}) 849 | except HTTPError as e: 850 | response = e.read() 851 | except URLError as e: 852 | return (None, "Could not contact the server. Error message: " + str(e)) 853 | except Exception: 854 | return (None, "Could not contact the server.") 855 | 856 | jobj = json.loads(response) 857 | 858 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 859 | if jobj != None: 860 | return (None, jobj["message"]) 861 | else: 862 | return (None, "Could not contact the server.") 863 | 864 | return (jobj["products"], "") 865 | 866 | @staticmethod 867 | def get_keys(token, product_id, page = 1, order_by="ID ascending", search_query=""): 868 | 869 | """ 870 | This method will return a list of keys for a given product. 871 | Please keep in mind that although each license key will be 872 | of the License Key type, the fields related to signing operations 873 | will be left empty. Instead, if you want to get a signed license key 874 | (for example, to achieve offline key activation), please use the 875 | Activation method instead. 876 | 877 | More docs: https://app.cryptolens.io/docs/api/v3/GetKeys 878 | """ 879 | 880 | try: 881 | response = HelperMethods.send_request("/product/getkeys/",\ 882 | {"token":token,\ 883 | "ProductId" : product_id,\ 884 | "Page" : page,\ 885 | "OrderBy" : order_by,\ 886 | "SearchQuery" : search_query\ 887 | }) 888 | except HTTPError as e: 889 | response = e.read() 890 | except URLError as e: 891 | return (None, "Could not contact the server. Error message: " + str(e)) 892 | except Exception: 893 | return (None, "Could not contact the server.") 894 | 895 | jobj = json.loads(response) 896 | 897 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 898 | if jobj != None: 899 | return (None, jobj["message"]) 900 | else: 901 | return (None, "Could not contact the server.") 902 | 903 | return (jobj["licenseKeys"], "", {"returned":jobj["returned"], "total":jobj["total"], "pageCount":jobj["pageCount"]}) 904 | 905 | 906 | class Customer: 907 | 908 | @staticmethod 909 | def add_customer(token, name = "", email = "", company_name="",\ 910 | enable_customer_association = False,\ 911 | allow_activation_management = False ): 912 | 913 | """ 914 | This method will add new customer. 915 | 916 | More docs: https://app.cryptolens.io/docs/api/v3/AddCustomer 917 | """ 918 | 919 | try: 920 | response = HelperMethods.send_request("/customer/addcustomer/",\ 921 | {"token":token,\ 922 | "Name": name,\ 923 | "Email": email,\ 924 | "CompanyName": company_name,\ 925 | "EnableCustomerAssociation": enable_customer_association,\ 926 | "AllowActivationManagement": allow_activation_management 927 | }) 928 | except HTTPError as e: 929 | response = e.read() 930 | except URLError as e: 931 | return (None, "Could not contact the server. Error message: " + str(e)) 932 | except Exception: 933 | return (None, "Could not contact the server.") 934 | 935 | jobj = json.loads(response) 936 | 937 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 938 | if jobj != None: 939 | return (None, jobj["message"]) 940 | else: 941 | return (None, "Could not contact the server.") 942 | 943 | return (jobj, "") 944 | 945 | @staticmethod 946 | def get_customer_licenses(token, customer_id, detailed=False, metadata=False): 947 | 948 | """ 949 | This method will return a list of license keys that belong to a certain customer. 950 | 951 | More docs: https://app.cryptolens.io/docs/api/v3/GetCustomerLicenses 952 | """ 953 | 954 | try: 955 | response = HelperMethods.send_request("/customer/GetCustomerLicenses/",\ 956 | {"token":token,\ 957 | "customerId" : customer_id,\ 958 | "detailed" : detailed,\ 959 | "metadata" : metadata 960 | }) 961 | except HTTPError as e: 962 | response = e.read() 963 | except URLError as e: 964 | return (None, "Could not contact the server. Error message: " + str(e)) 965 | except Exception: 966 | return (None, "Could not contact the server.") 967 | 968 | jobj = json.loads(response) 969 | 970 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 971 | if jobj != None: 972 | return (None, jobj["message"]) 973 | else: 974 | return (None, "Could not contact the server.") 975 | 976 | return (jobj, "") 977 | 978 | @staticmethod 979 | def get_customer_licenses_by_secret(token, secret, detailed=False, metadata=False): 980 | 981 | """ 982 | This method will return a list of license keys that belong to a certain customer. 983 | 984 | More docs: https://app.cryptolens.io/docs/api/v3/GetCustomerLicenses 985 | """ 986 | 987 | try: 988 | response = HelperMethods.send_request("/customer/GetCustomerLicensesBySecret/",\ 989 | {"token":token,\ 990 | "secret" : secret,\ 991 | "detailed" : detailed,\ 992 | "metadata" : metadata 993 | }) 994 | except HTTPError as e: 995 | response = e.read() 996 | except URLError as e: 997 | return (None, "Could not contact the server. Error message: " + str(e)) 998 | except Exception: 999 | return (None, "Could not contact the server.") 1000 | 1001 | jobj = json.loads(response) 1002 | 1003 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1004 | if jobj != None: 1005 | return (None, jobj["message"]) 1006 | else: 1007 | return (None, "Could not contact the server.") 1008 | 1009 | return (jobj, "") 1010 | 1011 | 1012 | class Data: 1013 | 1014 | """ 1015 | Data object related methods 1016 | """ 1017 | 1018 | @staticmethod 1019 | def increment_int_value_to_key(token, product_id, key, object_id=0, name = "",\ 1020 | int_value=0, enable_bound=False, bound=0): 1021 | 1022 | """ 1023 | This method will increment the int value of a data object associated with a license key. 1024 | 1025 | When creating an access token to this method, remember to include "IncrementIntValue" permission and 1026 | set the "Lock to key" value to -1. 1027 | 1028 | Note: either an object_id or name (provided there are no duplicates) is required. 1029 | 1030 | More docs: https://app.cryptolens.io/docs/api/v3/IncrementIntValue (see parameters under Method 2) 1031 | """ 1032 | 1033 | try: 1034 | response = HelperMethods.send_request("/data/IncrementIntValueToKey/",\ 1035 | {"token":token,\ 1036 | "ProductId" : product_id,\ 1037 | "Key" : key,\ 1038 | "Id" : object_id,\ 1039 | "Name" : name,\ 1040 | "IntValue": int_value ,\ 1041 | "EnableBound": str(enable_bound),\ 1042 | "Bound" : bound 1043 | }) 1044 | except HTTPError as e: 1045 | response = e.read() 1046 | except URLError as e: 1047 | return (None, "Could not contact the server. Error message: " + str(e)) 1048 | except Exception: 1049 | return (None, "Could not contact the server.") 1050 | 1051 | jobj = json.loads(response) 1052 | 1053 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1054 | if jobj != None: 1055 | return (None, jobj["message"]) 1056 | else: 1057 | return (None, "Could not contact the server.") 1058 | 1059 | return (jobj, "") 1060 | 1061 | @staticmethod 1062 | def decrement_int_value_to_key(token, product_id, key, object_id=0, name="",\ 1063 | int_value=0, enable_bound=False, bound=0): 1064 | 1065 | """ 1066 | This method will decrement the int value of a data object associated with a license key. 1067 | 1068 | When creating an access token to this method, remember to include "DecrementIntValue" permission and 1069 | set the "Lock to key" value to -1. 1070 | 1071 | Note: either an object_id or name (provided there are no duplicates) is required. 1072 | 1073 | More docs: https://app.cryptolens.io/docs/api/v3/DecrementIntValue (see parameters under Method 2) 1074 | """ 1075 | 1076 | try: 1077 | response = HelperMethods.send_request("/data/DecrementIntValueToKey/",\ 1078 | {"token":token,\ 1079 | "ProductId" : product_id,\ 1080 | "Key" : key,\ 1081 | "Id" : object_id,\ 1082 | "Name" : name,\ 1083 | "IntValue": int_value ,\ 1084 | "EnableBound": str(enable_bound),\ 1085 | "Bound" : bound 1086 | }) 1087 | except HTTPError as e: 1088 | response = e.read() 1089 | except URLError as e: 1090 | return (None, "Could not contact the server. Error message: " + str(e)) 1091 | except Exception: 1092 | return (None, "Could not contact the server.") 1093 | 1094 | jobj = json.loads(response) 1095 | 1096 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1097 | if jobj != None: 1098 | return (None, jobj["message"]) 1099 | else: 1100 | return (None, "Could not contact the server.") 1101 | 1102 | return (jobj, "") 1103 | 1104 | @staticmethod 1105 | def add_data_object_to_key(token, product_id, key, name = "", string_value="",\ 1106 | int_value=0, check_for_duplicates=False): 1107 | 1108 | """ 1109 | This method will add a new Data Object to a license key. 1110 | 1111 | More docs: https://app.cryptolens.io/docs/api/v3/AddDataObject (see parameters under Method 2) 1112 | """ 1113 | 1114 | try: 1115 | response = HelperMethods.send_request("/data/AddDataObjectToKey/",\ 1116 | {"token":token,\ 1117 | "ProductId" : product_id,\ 1118 | "Key" : key,\ 1119 | "Name" : name,\ 1120 | "IntValue": int_value ,\ 1121 | "StringValue": string_value ,\ 1122 | "CheckForDuplicates" : str(check_for_duplicates) 1123 | }) 1124 | except HTTPError as e: 1125 | response = e.read() 1126 | except URLError as e: 1127 | return (None, "Could not contact the server. Error message: " + str(e)) 1128 | except Exception: 1129 | return (None, "Could not contact the server.") 1130 | 1131 | jobj = json.loads(response) 1132 | 1133 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1134 | if jobj != None: 1135 | return (None, jobj["message"]) 1136 | else: 1137 | return (None, "Could not contact the server.") 1138 | 1139 | return (jobj, "") 1140 | 1141 | @staticmethod 1142 | def remove_data_object_to_key(token, product_id, key, object_id=0, name = ""): 1143 | 1144 | """ 1145 | This method will add a new Data Object to a license key. 1146 | 1147 | Note: either an object_id or name (provided there are no duplicates) is required. 1148 | 1149 | More docs: https://app.cryptolens.io/docs/api/v3/RemoveDataObject (see parameters under Method 2) 1150 | """ 1151 | 1152 | try: 1153 | response = HelperMethods.send_request("/data/RemoveDataObjectToKey/",\ 1154 | {"token":token,\ 1155 | "ProductId" : product_id,\ 1156 | "Key" : key,\ 1157 | "Name" : name,\ 1158 | "Id": object_id }) 1159 | except HTTPError as e: 1160 | response = e.read() 1161 | except URLError as e: 1162 | return (None, "Could not contact the server. Error message: " + str(e)) 1163 | except Exception: 1164 | return (None, "Could not contact the server.") 1165 | 1166 | jobj = json.loads(response) 1167 | 1168 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1169 | if jobj != None: 1170 | return (None, jobj["message"]) 1171 | else: 1172 | return (None, "Could not contact the server.") 1173 | 1174 | return (jobj, "") 1175 | 1176 | @staticmethod 1177 | def add_data_object_to_machine(token, product_id, key, machine_code, name = "", string_value="",\ 1178 | int_value=0, check_for_duplicates=False): 1179 | 1180 | """ 1181 | This method will add a new Data Object to Machine. 1182 | 1183 | More docs: https://app.cryptolens.io/docs/api/v3/AddDataObject (see parameters under Method 3) 1184 | """ 1185 | 1186 | try: 1187 | response = HelperMethods.send_request("/data/AddDataObjectToMachineCode/",\ 1188 | {"token":token,\ 1189 | "ProductId" : product_id,\ 1190 | "Key" : key,\ 1191 | "Name" : name,\ 1192 | "IntValue": int_value ,\ 1193 | "StringValue": string_value ,\ 1194 | "CheckForDuplicates" : str(check_for_duplicates), \ 1195 | "MachineCode": machine_code 1196 | }) 1197 | except HTTPError as e: 1198 | response = e.read() 1199 | except URLError as e: 1200 | return (None, "Could not contact the server. Error message: " + str(e)) 1201 | except Exception: 1202 | return (None, "Could not contact the server.") 1203 | 1204 | jobj = json.loads(response) 1205 | 1206 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1207 | if jobj != None: 1208 | return (None, jobj["message"]) 1209 | else: 1210 | return (None, "Could not contact the server.") 1211 | 1212 | return (jobj, "") 1213 | 1214 | @staticmethod 1215 | def remove_data_object_to_machine(token, product_id, key, machine_code, object_id=0, name = ""): 1216 | 1217 | """ 1218 | This method will remove existing Data Object from Machine Code. 1219 | 1220 | More docs: https://app.cryptolens.io/docs/api/v3/RemoveDataObject (see parameters under Method 3) 1221 | """ 1222 | 1223 | try: 1224 | response = HelperMethods.send_request("/data/RemoveDataObjectToMachineCode/",\ 1225 | {"token":token,\ 1226 | "ProductId" : product_id,\ 1227 | "Key" : key,\ 1228 | "MachineCode": machine_code,\ 1229 | "Name" : name,\ 1230 | "Id": object_id }) 1231 | except HTTPError as e: 1232 | response = e.read() 1233 | except URLError as e: 1234 | return (None, "Could not contact the server. Error message: " + str(e)) 1235 | except Exception: 1236 | return (None, "Could not contact the server.") 1237 | 1238 | jobj = json.loads(response) 1239 | 1240 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1241 | if jobj != None: 1242 | return (None, jobj["message"]) 1243 | else: 1244 | return (None, "Could not contact the server.") 1245 | 1246 | return (jobj, "") 1247 | 1248 | @staticmethod 1249 | def list_machine_data_objects(token, product_id, key, machine_code, \ 1250 | name_contains=""): 1251 | 1252 | """ 1253 | This method will list Data Objects for Machine. 1254 | 1255 | More docs: https://app.cryptolens.io/docs/api/v3/ListDataObjects (see parameters under Method 3) 1256 | """ 1257 | 1258 | try: 1259 | response = HelperMethods.send_request("/data/ListDataObjectsToMachineCode/",\ 1260 | {"token":token,\ 1261 | "ProductId" : product_id,\ 1262 | "Key" : key,\ 1263 | "MachineCode": machine_code,\ 1264 | "Contains": name_contains 1265 | }) 1266 | except HTTPError as e: 1267 | response = e.read() 1268 | except URLError as e: 1269 | return (None, "Could not contact the server. Error message: " + str(e)) 1270 | except Exception: 1271 | return (None, "Could not contact the server.") 1272 | 1273 | jobj = json.loads(response) 1274 | 1275 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1276 | if jobj != None: 1277 | return (None, jobj["message"]) 1278 | else: 1279 | return (None, "Could not contact the server.") 1280 | 1281 | return (jobj, "") 1282 | 1283 | @staticmethod 1284 | def list_key_data_objects(token, product_id, key, \ 1285 | name_contains=""): 1286 | 1287 | """ 1288 | This method will list Data Objects for License Key. 1289 | 1290 | More docs: https://app.cryptolens.io/docs/api/v3/ListDataObjects (see parameters under Method 2) 1291 | """ 1292 | 1293 | try: 1294 | response = HelperMethods.send_request("/data/ListDataObjectsToKey/",\ 1295 | {"token":token,\ 1296 | "ProductId" : product_id,\ 1297 | "Key" : key,\ 1298 | "Contains": name_contains 1299 | }) 1300 | except HTTPError as e: 1301 | response = e.read() 1302 | except URLError as e: 1303 | return (None, "Could not contact the server. Error message: " + str(e)) 1304 | except Exception: 1305 | return (None, "Could not contact the server.") 1306 | 1307 | jobj = json.loads(response) 1308 | 1309 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1310 | if jobj != None: 1311 | return (None, jobj["message"]) 1312 | else: 1313 | return (None, "Could not contact the server.") 1314 | 1315 | return (jobj, "") 1316 | 1317 | 1318 | class PaymentForm: 1319 | 1320 | @staticmethod 1321 | def create_session(token, payment_form_id, currency, expires, price=None,\ 1322 | heading = None, product_name = None, custom_field='',\ 1323 | metadata = None): 1324 | 1325 | 1326 | """ 1327 | This method will create a new session for a Payment Form. 1328 | It allows you to customize appearance of the form (such as price, heading, etc). 1329 | You should only create new sessions from a server side (i.e. never directly from your application). 1330 | Note, session will only work once and it will eventually expire depending on Expires parameter. 1331 | 1332 | More docs: https://app.cryptolens.io/docs/api/v3/PFCreateSession 1333 | """ 1334 | 1335 | try: 1336 | response = HelperMethods.send_request("/paymentform/CreateSession/",\ 1337 | {"token":token,\ 1338 | "PaymentFormId" : payment_form_id,\ 1339 | "Price" : price,\ 1340 | "Currency" : currency,\ 1341 | "Heading": heading ,\ 1342 | "ProductName": product_name,\ 1343 | "CustomField" : custom_field,\ 1344 | "Metadata" : metadata,\ 1345 | "Expires" : expires,\ 1346 | }) 1347 | except HTTPError as e: 1348 | response = e.read() 1349 | except URLError as e: 1350 | return (None, "Could not contact the server. Error message: " + str(e)) 1351 | except Exception: 1352 | return (None, "Could not contact the server.") 1353 | 1354 | jobj = json.loads(response) 1355 | 1356 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1357 | if jobj != None: 1358 | return (None, jobj["message"]) 1359 | else: 1360 | return (None, "Could not contact the server.") 1361 | 1362 | return (jobj, "") 1363 | 1364 | class Helpers: 1365 | 1366 | 1367 | def __read_registry_value(key, subkey, value_name): 1368 | 1369 | import winreg 1370 | 1371 | """ 1372 | Reads a value from the Windows Registry. 1373 | 1374 | Parameters: 1375 | key (int): The registry root key (e.g., winreg.HKEY_LOCAL_MACHINE). 1376 | subkey (str): The path to the subkey. 1377 | value_name (str): The name of the value to read. 1378 | 1379 | Returns: 1380 | str: The value read from the registry, or an error message if not found. 1381 | """ 1382 | try: 1383 | # Open the registry key 1384 | registry_key = winreg.OpenKey(key, subkey, 0, winreg.KEY_READ) 1385 | 1386 | # Query the value 1387 | value, reg_type = winreg.QueryValueEx(registry_key, value_name) 1388 | 1389 | # Close the registry key 1390 | winreg.CloseKey(registry_key) 1391 | 1392 | return value 1393 | 1394 | except FileNotFoundError: 1395 | return None 1396 | except Exception as e: 1397 | return None #str(e) 1398 | 1399 | 1400 | @staticmethod 1401 | def GetMachineCode(v=1): 1402 | 1403 | """ 1404 | Get a unique identifier for this device. If you want the machine code to be the same in .NET on Windows, you 1405 | can set v=2. More information is available here: https://help.cryptolens.io/faq/index#machine-code-generation 1406 | 1407 | Note: if we are unable to compute the machine code, None will be returned. Please make sure 1408 | to check this in production code. 1409 | """ 1410 | 1411 | if "windows" in platform.platform().lower(): 1412 | 1413 | import winreg 1414 | 1415 | seed = "" 1416 | 1417 | if v==2: 1418 | seed = HelperMethods.start_process_ps_v2() 1419 | else: 1420 | seed = HelperMethods.start_process(["cmd.exe", "/C", "wmic","csproduct", "get", "uuid"],v) 1421 | 1422 | if seed == "" or seed == None: 1423 | machineGUID = Helpers.__read_registry_value(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography", "MachineGuid") 1424 | 1425 | if machineGUID != None and machineGUID != "": 1426 | return HelperMethods.get_SHA256(machineGUID) 1427 | return None 1428 | else: 1429 | return HelperMethods.get_SHA256(seed) 1430 | 1431 | 1432 | elif "mac" in platform.platform().lower() or "darwin" in platform.platform().lower(): 1433 | res = HelperMethods.start_process(["system_profiler","SPHardwareDataType"]) 1434 | seed = res[res.index("UUID"):].strip() 1435 | 1436 | if seed == "": 1437 | return None 1438 | else: 1439 | return HelperMethods.get_SHA256(seed) 1440 | 1441 | elif "linux" in platform.platform().lower() : 1442 | seed = HelperMethods.compute_machine_code() 1443 | if seed == "": 1444 | return None 1445 | else: 1446 | return HelperMethods.get_SHA256(seed) 1447 | else: 1448 | seed = HelperMethods.compute_machine_code() 1449 | if seed == "": 1450 | return None 1451 | else: 1452 | return HelperMethods.get_SHA256(seed) 1453 | 1454 | @staticmethod 1455 | def IsOnRightMachine(license_key, is_floating_license = False, allow_overdraft=False, v = 1, custom_machine_code = None): 1456 | 1457 | """ 1458 | Check if the device is registered with the license key. 1459 | The version parameter is related to the one in GetMachineCode method. 1460 | """ 1461 | 1462 | current_mid = "" 1463 | 1464 | if custom_machine_code == None: 1465 | current_mid = Helpers.GetMachineCode(v) 1466 | else: 1467 | current_mid = custom_machine_code 1468 | 1469 | if license_key.activated_machines == None: 1470 | return False 1471 | 1472 | if is_floating_license: 1473 | for act_machine in license_key.activated_machines: 1474 | if act_machine.Mid[9:] == current_mid or\ 1475 | allow_overdraft and act_machine.Mid[19:] == current_mid: 1476 | return True 1477 | else: 1478 | for act_machine in license_key.activated_machines: 1479 | if current_mid == act_machine.Mid: 1480 | return True 1481 | 1482 | return False 1483 | 1484 | 1485 | @staticmethod 1486 | def GetMACAddress(): 1487 | 1488 | """ 1489 | An alternative way to compute the machine code (device identifier). 1490 | This method is especially useful if you plan to target multiple platforms. 1491 | """ 1492 | 1493 | import uuid 1494 | 1495 | return ':'.join(['{:02x}'.format((uuid.getnode() >> ele) & 0xff) for ele in range(0,8*6,8)][::-1]) 1496 | 1497 | def HasNotExpired(license_key, allow_usage_on_expiry_date=True): 1498 | 1499 | """ 1500 | Checks that the license key has not expired. Cryptolens offers automatic blocking of licenses 1501 | on the server side, and it is recommended to set it up instead of relying on the client side (unless 1502 | your application is running in offline mode). For more details, please review 1503 | https://help.cryptolens.io/web-interface/keys-that-dont-expire 1504 | 1505 | Parameters: 1506 | @license_key The license key object. 1507 | @allow_usage_on_expiry_date If set to true, the license will be considered valid on the day it expires. 1508 | """ 1509 | 1510 | import datetime 1511 | 1512 | if license_key == None: 1513 | return False 1514 | 1515 | diff = license_key.expires.replace(tzinfo=datetime.timezone.utc) - datetime.datetime.now(datetime.timezone.utc) 1516 | 1517 | if allow_usage_on_expiry_date and diff >= datetime.timedelta(0) or not(allow_usage_on_expiry_date) and diff > 0: 1518 | return True 1519 | 1520 | return False 1521 | 1522 | def HasFeature(license_key, feature_name): 1523 | 1524 | """ 1525 | Uses a special data object associated with the license key to determine if a certain feature exists (instead of the 8 feature flags). 1526 | Formatting: The name of the data object should be 'cryptolens_features' and it should be structured as a JSON array. 1527 | 1528 | For example,
["f1", "f2"]

means f1 and f2 are true. You can also have feature bundling, eg.

["f1", ["f2",["voice","image"]]]
1529 | which means that f1 and f2 are true, as well as f2.voice and f2.image. You can set any depth, eg. you can have 1530 |
["f1", ["f2",[["voice",["all"]], "image"]]]
means f2.voice.all is true as well as f2.voice and f2. 1531 | The dots symbol is used to specify the "sub-features". 1532 | 1533 | Read more here: https://help.cryptolens.io/web-interface/feature-templates 1534 | 1535 | Parameters: 1536 | @license_key The license key object. 1537 | @feature_name For example, "f2.voice.all". 1538 | 1539 | """ 1540 | 1541 | if license_key.data_objects == None: 1542 | return False 1543 | 1544 | features = None 1545 | 1546 | for dobj in license_key.data_objects: 1547 | 1548 | if dobj["Name"] == 'cryptolens_features': 1549 | features = dobj["StringValue"] 1550 | break 1551 | 1552 | if features == None or features.strip() == "": 1553 | return False 1554 | 1555 | array = json.loads(features) 1556 | 1557 | feature_path = feature_name.split(".") 1558 | 1559 | found = False 1560 | 1561 | for i in range(len(feature_path)): 1562 | 1563 | found = False 1564 | index = -1 1565 | 1566 | for j in range(len(array)): 1567 | 1568 | if not(isinstance(array[j], list)) and array[j] == feature_path[i]: 1569 | found = True 1570 | break 1571 | elif isinstance(array[j], list) and array[j][0] == feature_path[i]: 1572 | found = True 1573 | index = j 1574 | 1575 | if not(found): 1576 | return False 1577 | 1578 | if i+1 < len(feature_path) and index != -1: 1579 | array = array[index][1] 1580 | 1581 | if not(found): 1582 | return False 1583 | 1584 | return True 1585 | 1586 | class Subscription: 1587 | """ 1588 | Subscription related methods 1589 | """ 1590 | 1591 | @staticmethod 1592 | def record_usage_to_stripe(token, product_id, key, amount=""): 1593 | 1594 | """ 1595 | This method records uses Stripe's metered billing to record usage for a certain subscription. In order to use this method, 1596 | you need to have set up recurring billing. A record will be created using Stripe's API with action set to 'increment' 1597 | 1598 | More docs: https://app.cryptolens.io/docs/api/v3/RecordUsage 1599 | """ 1600 | 1601 | try: 1602 | response = HelperMethods.send_request("/subscription/RecordUsage/",\ 1603 | {"token":token,\ 1604 | "ProductId" : product_id,\ 1605 | "Key" : key,\ 1606 | "Amount" : amount, 1607 | }) 1608 | except HTTPError as e: 1609 | response = e.read() 1610 | except URLError as e: 1611 | return (None, "Could not contact the server. Error message: " + str(e)) 1612 | except Exception: 1613 | return (None, "Could not contact the server.") 1614 | 1615 | jobj = json.loads(response) 1616 | 1617 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1618 | if jobj != None: 1619 | return (None, jobj["message"]) 1620 | else: 1621 | return (None, "Could not contact the server.") 1622 | 1623 | return (jobj, "") 1624 | 1625 | class User: 1626 | 1627 | """ 1628 | The idea behind user authentication is to allow you to authenticate 1629 | users using their crendntials (i.e. username and password) to verify their 1630 | license. You can use their username and password to retrieve their 1631 | licenses instead of asking for a license key. 1632 | 1633 | This is similar to obtaining all licenses assigned to a customer 1634 | using customer secret, with the difference that the user can pick both 1635 | the username and password, as well as restore a forgotten password. 1636 | 1637 | For more information, please see 1638 | https://help.cryptolens.io/examples/user-verification and 1639 | https://app.cryptolens.io/docs/api/v3/UserAuth 1640 | """ 1641 | 1642 | @staticmethod 1643 | def login(token, username, password): 1644 | 1645 | """ 1646 | This method will return all licenses that belong to the user. 1647 | This method can be called with an access token that has UserAuthNormal 1648 | and UserAuthAdmin permission. 1649 | 1650 | More docs: https://app.cryptolens.io/docs/api/v3/Login 1651 | """ 1652 | 1653 | try: 1654 | response = HelperMethods.send_request("/userauth/login/", {"token":token, "username":username, "password":password}) 1655 | except HTTPError as e: 1656 | response = e.read() 1657 | except URLError as e: 1658 | return (None, "Could not contact the server. Error message: " + str(e)) 1659 | except Exception: 1660 | return (None, "Could not contact the server.") 1661 | 1662 | jobj = json.loads(response) 1663 | 1664 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1665 | if jobj != None: 1666 | return (None, jobj["message"]) 1667 | else: 1668 | return (None, "Could not contact the server.") 1669 | 1670 | return (jobj["licenseKeys"], "") 1671 | 1672 | """ 1673 | The idea behind user authentication is to allow you to authenticate 1674 | users using their crendntials (i.e. username and password) to verify their 1675 | license. You can use their username and password to retrieve their 1676 | licenses instead of asking for a license key. 1677 | 1678 | This is similar to obtaining all licenses assigned to a customer 1679 | using customer secret, with the difference that the user can pick both 1680 | the username and password, as well as restore a forgotten password. 1681 | 1682 | For more information, please see 1683 | https://help.cryptolens.io/examples/user-verification and 1684 | https://app.cryptolens.io/docs/api/v3/UserAuth 1685 | """ 1686 | 1687 | @staticmethod 1688 | def register(token, username, password, email = "", customerId = 0): 1689 | 1690 | """ 1691 | This method will register a new user. Please note that calling this 1692 | method requires a UserAuthAdmin token. 1693 | 1694 | More docs: https://app.cryptolens.io/docs/api/v3/Register 1695 | """ 1696 | 1697 | try: 1698 | response = HelperMethods.send_request("/userauth/Register/",\ 1699 | {"token":token,\ 1700 | "username":username,\ 1701 | "password":password,\ 1702 | "email":email,\ 1703 | "customerid":customerId}) 1704 | except HTTPError as e: 1705 | response = e.read() 1706 | except URLError as e: 1707 | return (None, "Could not contact the server. Error message: " + str(e)) 1708 | except Exception: 1709 | return (None, "Could not contact the server.") 1710 | 1711 | jobj = json.loads(response) 1712 | 1713 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1714 | if jobj != None: 1715 | return (None, jobj["message"]) 1716 | else: 1717 | return (None, "Could not contact the server.") 1718 | 1719 | return (jobj, "") 1720 | 1721 | @staticmethod 1722 | def associate(token, username, customer_id=0): 1723 | 1724 | """ 1725 | Associates a user with a customer object. Please note that calling 1726 | this method requires a UserAuthAdmin token. 1727 | 1728 | More docs: https://app.cryptolens.io/docs/api/v3/Associate 1729 | """ 1730 | 1731 | try: 1732 | response = HelperMethods.send_request("/userauth/Associate/",\ 1733 | {"token":token,\ 1734 | "username":username,\ 1735 | "customerid":customer_id}) 1736 | except HTTPError as e: 1737 | response = e.read() 1738 | except URLError as e: 1739 | return (None, "Could not contact the server. Error message: " + str(e)) 1740 | except Exception: 1741 | return (None, "Could not contact the server.") 1742 | 1743 | jobj = json.loads(response) 1744 | 1745 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1746 | if jobj != None: 1747 | return (None, jobj["message"]) 1748 | else: 1749 | return (None, "Could not contact the server.") 1750 | 1751 | return (jobj, "") 1752 | 1753 | @staticmethod 1754 | def dissociate(token, username): 1755 | 1756 | """ 1757 | Dissociates a user from a customer customer object. Please note that 1758 | calling this method requires a UserAuthAdmin token. 1759 | 1760 | More docs: https://app.cryptolens.io/docs/api/v3/Dissociate 1761 | """ 1762 | 1763 | try: 1764 | response = HelperMethods.send_request("/userauth/Dissociate/",\ 1765 | {"token":token,\ 1766 | "username":username}) 1767 | except HTTPError as e: 1768 | response = e.read() 1769 | except URLError as e: 1770 | return (None, "Could not contact the server. Error message: " + str(e)) 1771 | except Exception: 1772 | return (None, "Could not contact the server.") 1773 | 1774 | jobj = json.loads(response) 1775 | 1776 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1777 | if jobj != None: 1778 | return (None, jobj["message"]) 1779 | else: 1780 | return (None, "Could not contact the server.") 1781 | 1782 | return (jobj, "") 1783 | 1784 | @staticmethod 1785 | def get_users(token, customer_id = 0): 1786 | 1787 | """ 1788 | List all registered users. Please note that calling this method 1789 | requires a UserAuthAdmin token. 1790 | 1791 | More docs: https://app.cryptolens.io/docs/api/v3/GetUsers 1792 | """ 1793 | 1794 | try: 1795 | response = HelperMethods.send_request("/userauth/GetUsers/",\ 1796 | {"token":token,\ 1797 | "customerid":customer_id}) 1798 | except HTTPError as e: 1799 | response = e.read() 1800 | except URLError as e: 1801 | return (None, "Could not contact the server. Error message: " + str(e)) 1802 | except Exception: 1803 | return (None, "Could not contact the server.") 1804 | 1805 | jobj = json.loads(response) 1806 | 1807 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1808 | if jobj != None: 1809 | return (None, jobj["message"]) 1810 | else: 1811 | return (None, "Could not contact the server.") 1812 | 1813 | return (jobj["users"], "") 1814 | 1815 | @staticmethod 1816 | def change_password(token, username, new_password, old_password="", password_reset_token="", admin_mode=False): 1817 | 1818 | """ 1819 | This method will change the password of a user. It supports 3 modes of 1820 | operation. With an access token that has UserAuthNormal permission 1821 | (i.e. without admin permission), the password can either be changed by 1822 | providing the old password or a password reset token, which can be 1823 | generated using Reset Password Token method. Finally, if you call this 1824 | method with an access token that has UserAuthAdmin permission, it will 1825 | allow you to set AdminMode to True and only provide the NewPassword. 1826 | 1827 | More docs: https://app.cryptolens.io/docs/api/v3/ChangePassword 1828 | """ 1829 | 1830 | try: 1831 | response = HelperMethods.send_request("/userauth/ChangePassword/",\ 1832 | {"token":token,\ 1833 | "username":username,\ 1834 | "OldPassword": old_password,\ 1835 | "NewPassword":new_password,\ 1836 | "PasswordResetToken": password_reset_token,\ 1837 | "AdminMode":admin_mode}) 1838 | except HTTPError as e: 1839 | response = e.read() 1840 | except URLError as e: 1841 | return (None, "Could not contact the server. Error message: " + str(e)) 1842 | except Exception: 1843 | return (None, "Could not contact the server.") 1844 | 1845 | jobj = json.loads(response) 1846 | 1847 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1848 | if jobj != None: 1849 | return (None, jobj["message"]) 1850 | else: 1851 | return (None, "Could not contact the server.") 1852 | 1853 | return (jobj, "") 1854 | 1855 | 1856 | @staticmethod 1857 | def reset_password_token(token, username): 1858 | 1859 | """ 1860 | This method allows you to retrive the password reset token that you 1861 | can use when calling Change Password method. Please note that calling 1862 | this method requires a UserAuthAdmin token. 1863 | 1864 | More docs: https://app.cryptolens.io/docs/api/v3/ResetPasswordToken 1865 | """ 1866 | 1867 | try: 1868 | response = HelperMethods.send_request("/userauth/ResetPasswordToken/",\ 1869 | {"token":token,\ 1870 | "username":username}) 1871 | except HTTPError as e: 1872 | response = e.read() 1873 | except URLError as e: 1874 | return (None, "Could not contact the server. Error message: " + str(e)) 1875 | except Exception: 1876 | return (None, "Could not contact the server.") 1877 | 1878 | jobj = json.loads(response) 1879 | 1880 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1881 | if jobj != None: 1882 | return (None, jobj["message"]) 1883 | else: 1884 | return (None, "Could not contact the server.") 1885 | 1886 | return (jobj["passwordResetToken"], "") 1887 | 1888 | 1889 | @staticmethod 1890 | def remove_user(token, username): 1891 | 1892 | """ 1893 | This method removes a user. Please note that calling this method 1894 | requires a UserAuthAdmin token. 1895 | 1896 | More docs: https://app.cryptolens.io/docs/api/v3/RemoveUser 1897 | """ 1898 | 1899 | try: 1900 | response = HelperMethods.send_request("/userauth/RemoveUser/",\ 1901 | {"token":token,\ 1902 | "username":username}) 1903 | except HTTPError as e: 1904 | response = e.read() 1905 | except URLError as e: 1906 | return (None, "Could not contact the server. Error message: " + str(e)) 1907 | except Exception: 1908 | return (None, "Could not contact the server.") 1909 | 1910 | jobj = json.loads(response) 1911 | 1912 | if jobj == None or not("result" in jobj) or jobj["result"] == 1: 1913 | if jobj != None: 1914 | return (None, jobj["message"]) 1915 | else: 1916 | return (None, "Could not contact the server.") 1917 | 1918 | return (jobj, "") --------------------------------------------------------------------------------