├── .gitignore ├── .readthedocs.yaml ├── README.md └── docs ├── Makefile ├── _common_definitions.txt ├── conf.py ├── how-to ├── ci.rst ├── obfuscation.rst ├── packing.rst ├── protection.rst ├── register.rst ├── security.rst ├── third-party.rst └── wheel.rst ├── index.rst ├── licenses.rst ├── part-1.rst ├── part-2.rst ├── part-3.rst ├── part-4.rst ├── questions.rst ├── reference ├── concepts.rst ├── environments.rst ├── errors.rst ├── man.rst └── solutions.rst ├── requirements.txt ├── topic ├── bccmode.rst ├── localization.rst ├── obfuscated-script.rst ├── obfuscation.rst ├── performance.rst ├── repack.rst └── rftmode.rst └── tutorial ├── advanced.rst ├── customization.rst ├── getting-started.rst ├── installation.rst └── obfuscation.rst /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.7" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | 15 | formats: 16 | - pdf 17 | - epub 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyarmor 中文文档库 2 | 3 | 中文文档发布地址 https://pyarmor.readthedocs.io/zh/latest 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PyArmor.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PyArmor.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PyArmor" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyArmor" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/_common_definitions.txt: -------------------------------------------------------------------------------- 1 | .. _Pyarmor: https://pypi.python.org/pypi/pyarmor/ 2 | .. _问题报告: https://github.com/dashingsoft/pyarmor/issues/ 3 | .. _讨论区: https://github.com/dashingsoft/pyarmor/discussions/ 4 | .. _Pyarmor 在线文档: https://pyarmor.readthedocs.io/zh/stable/ 5 | .. _Pyarmor 7.x 在线文档: https://pyarmor.readthedocs.io/zh/v7.x/ 6 | .. _Pyarmor 最终用户许可协议: https://github.com/dashingsoft/pyarmor/blob/master/LICENSE-ZH 7 | .. _MyCommerce Shopping: https://order.mycommerce.com/product?vendorid=200089125&productid=301044051 8 | .. _Pyarmor Shopping Cart: https://jondy.github.io/paypal/index.html 9 | 10 | .. |pyarmor| replace:: :doc:`pyarmor ` 11 | .. |Pyarmor| replace:: :term:`Pyarmor` 12 | .. |Home| replace:: https://github.com/dashingsoft/pyarmor/ 13 | .. |Website| replace:: https://pyarmor.dashingsoft.com/index-zh.html 14 | .. |Manual| replace:: https://pyarmor.readthedocs.io/zh/stable/ 15 | .. |Author| replace:: 赵俊德 16 | .. |Contact| replace:: pyarmor@163.com 17 | 18 | .. |FAQ| replace:: :doc:`常见问题 ` 19 | 20 | .. |Python| replace:: :term:`Python` 21 | 22 | .. _Python: https://www.python.org/ 23 | .. _PyPI: https://pypi.python.org/pypi/ 24 | .. _PyInstaller: https://www.pyinstaller.org/ 25 | .. _Cython: https://cython.org/ 26 | .. _NTP: http://www.ntp.org 27 | .. _Themida: https://www.themida.com 28 | 29 | .. _bytecode: https://docs.python.org/3.11/glossary.html#term-bytecode 30 | .. _extension module: https://docs.python.org/3.11/glossary.html#term-extension-module 31 | .. _Packaging binary extensions: https://packaging.python.org/en/latest/guides/packaging-binary-extensions/ 32 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # PyArmor documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Dec 1 18:07:32 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.todo', 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = 'Pyarmor' 52 | copyright = '2018 - 2024 德新科技.' 53 | author = '赵俊德' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '9.0' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '9.0.8' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = 'zh_CN' 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | # If true, `todo` and `todoList` produce output, else they produce nothing. 106 | todo_include_todos = True 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'sphinx_rtd_theme' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | #html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | #html_logo = None 133 | 134 | # The name of an image file (relative to this directory) to use as a favicon of 135 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | #html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | #html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | #html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | #html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | #html_sidebars = {} 159 | 160 | # Additional templates that should be rendered to pages, maps page names to 161 | # template names. 162 | #html_additional_pages = {} 163 | 164 | # If false, no module index is generated. 165 | #html_domain_indices = True 166 | 167 | # If false, no index is generated. 168 | #html_use_index = True 169 | 170 | # If true, the index is split into individual pages for each letter. 171 | #html_split_index = False 172 | 173 | # If true, links to the reST sources are added to the pages. 174 | #html_show_sourcelink = True 175 | 176 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 177 | #html_show_sphinx = True 178 | 179 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 180 | #html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages will 183 | # contain a tag referring to it. The value of this option must be the 184 | # base URL from which the finished HTML is served. 185 | #html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | #html_file_suffix = None 189 | 190 | # Language to be used for generating the HTML full-text search index. 191 | # Sphinx supports the following languages: 192 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 193 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 194 | #html_search_language = 'en' 195 | 196 | # A dictionary with options for the search language support, empty by default. 197 | # Now only 'ja' uses this config value 198 | #html_search_options = {'type': 'default'} 199 | 200 | # The name of a javascript file (relative to the configuration directory) that 201 | # implements a search results scorer. If empty, the default will be used. 202 | #html_search_scorer = 'scorer.js' 203 | 204 | # Output file base name for HTML help builder. 205 | htmlhelp_basename = 'Pyarmordoc' 206 | 207 | # -- Options for LaTeX output --------------------------------------------- 208 | 209 | # -- Fix PDF builder fail issue ------------------------------------------- 210 | # https://docs.readthedocs.io/en/stable/guides/pdf-non-ascii-languages.html 211 | latex_engine = 'xelatex' 212 | latex_use_xindy = False 213 | 214 | latex_elements = { 215 | # The paper size ('letterpaper' or 'a4paper'). 216 | #'papersize': 'letterpaper', 217 | 218 | # The font size ('10pt', '11pt' or '12pt'). 219 | #'pointsize': '10pt', 220 | 221 | # Additional stuff for the LaTeX preamble. 222 | #'preamble': '', 223 | 'preamble': '\\usepackage[UTF8]{ctex}\n', 224 | 225 | # Latex figure (float) alignment 226 | #'figure_align': 'htbp', 227 | } 228 | 229 | # Grouping the document tree into LaTeX files. List of tuples 230 | # (source start file, target name, title, 231 | # author, documentclass [howto, manual, or own class]). 232 | latex_documents = [ 233 | (master_doc, 'Pyarmor.tex', 'Pyarmor Documentation', 234 | 'Jondy Zhao', 'manual'), 235 | ] 236 | 237 | # The name of an image file (relative to this directory) to place at the top of 238 | # the title page. 239 | #latex_logo = None 240 | 241 | # For "manual" documents, if this is true, then toplevel headings are parts, 242 | # not chapters. 243 | #latex_use_parts = False 244 | 245 | # If true, show page references after internal links. 246 | #latex_show_pagerefs = False 247 | 248 | # If true, show URL addresses after external links. 249 | #latex_show_urls = False 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #latex_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #latex_domain_indices = True 256 | 257 | # -- Options for manual page output --------------------------------------- 258 | 259 | # One entry per manual page. List of tuples 260 | # (source start file, name, description, authors, manual section). 261 | man_pages = [ 262 | (master_doc, 'pyarmor', 'Pyarmor Documentation', 263 | [author], 1) 264 | ] 265 | 266 | # If true, show URL addresses after external links. 267 | #man_show_urls = False 268 | 269 | 270 | # -- Options for Texinfo output ------------------------------------------- 271 | 272 | # Grouping the document tree into Texinfo files. List of tuples 273 | # (source start file, target name, title, author, 274 | # dir menu entry, description, category) 275 | texinfo_documents = [ 276 | (master_doc, 'Pyarmor', 'Pyarmor Documentation', 277 | author, 'Pyarmor', 'Power tool to obfuscate Python scripts.', 278 | 'Miscellaneous'), 279 | ] 280 | 281 | # Documents to append as an appendix to all manuals. 282 | #texinfo_appendices = [] 283 | 284 | # If false, no module index is generated. 285 | #texinfo_domain_indices = True 286 | 287 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 288 | #texinfo_show_urls = 'footnote' 289 | 290 | # If true, do not generate a @detailmenu in the "Top" node's menu. 291 | #texinfo_no_detailmenu = False 292 | -------------------------------------------------------------------------------- /docs/how-to/ci.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: console 2 | 3 | ============================= 4 | 在 CI/CD 管线中使用 Pyarmor 5 | ============================= 6 | 7 | :ref:`直接使用方式` 基本不改变原来 CI/CD 的管线流程,适用于 8 | 9 | - 试用版 10 | - :term:`Pyarmor 基础版` 11 | - :term:`Pyarmor 管线版` 12 | 13 | :ref:`间接使用方式` 需要改变原来的 CI/CD 管线流程,任何许可证都可以使用 14 | 15 | 直接使用方式 16 | ============ 17 | 18 | **试用版** 可以直接在 CI/CD 管线中使用,只需要增加一个步骤:: 19 | 20 | pip install pyarmor 21 | 22 | :term:`Pyarmor 基础版` 和 :term:`Pyarmor 管线版` 许可证 23 | 24 | - 首先完成 :ref:`initial registration` ,得到许可证 :term:`注册文件` ``pyarmor-regfile-xxxx.zip`` 25 | - 其次在本地联网设备上申请 :term:`管线注册文件`:: 26 | 27 | $ pyarmor reg -C pyarmor-regfile-xxxx.zip 28 | 29 | 申请一次即可,该命令执行成功之后会在当前目录创建 :term:`管线注册文件` ``pyarmor-ci-xxxx.zip`` 30 | 31 | 在本地设备上面查看管线许可证的相关信息:: 32 | 33 | $ pyarmor --home temp reg pyarmor-ci-xxxx.zip 34 | 35 | - 在管线中增加如下命令:: 36 | 37 | # 请替换 9.X.Y 为当前 Pyarmor 的版本 38 | pip install pyarmor==9.X.Y 39 | parmor reg pyarmor-ci-xxxx.zip 40 | 41 | 在管线中查看注册信息:: 42 | 43 | pyarmor -v 44 | 45 | 注意事项 46 | 47 | - 管线注册文件有效期为一年,可以用于在 CI/CD 中注册 Pyarmor 48 | - 管线注册文件在当前 Pyarmor 的版本有效,但是不一定在升级之后的 Pyarmor 中有效 49 | - 每一个许可证最多可以申请 100 个管线注册文件,超过申请次数之后就无法在申请 50 | - 请勿在 CI/CD 管线中申请管线注册文件,因为请求次数有一定限制 51 | 52 | .. important:: 53 | 54 | 每次管线运行都会向 Pyarmor 许可证发送验证请求,过多的请求或者超过正常使用的请求可能会被拒绝 55 | 56 | 只允许在开发设备上安装和注册 Pyarmor,不允许在需要发送给客户的 Docker 镜像中安装和注册 Pyarmor 57 | 58 | .. note:: 59 | 60 | 在 GitHub Action 中需要额外的一个操作步骤,否则会报错 `CI license only works in CI/CD pipeline` 61 | 62 | 1. 对于 ubuntu ,增加操作步骤:: 63 | 64 | - run: sudo mv /dev/disk /dev/disk-none 65 | 66 | 2. 对于 darwin ,增加操作步骤:: 67 | 68 | - run: sudo mv /dev/rdisk0 /dev/rdisk0-none 69 | 70 | 请参考这里 `Error when using CI license in CI pipeline `_ 71 | 72 | 间接使用方式 73 | ============ 74 | 75 | 因为加密后的脚本等价于原来的脚本外加运行辅助包,所以可以首先在本地设备对脚本进行加密,然后把加密后的脚本保存到一个单独的分支,CI/CD 管线流程直接从加密分支中获取数据,这种方式适用于任何许可证 76 | 77 | 下面我们用一个示例来进行简单说明 78 | 79 | 假设我们的测试项目位于 `https://github.com/dashingsoft/test-project` ,其目录结构如下:: 80 | 81 | $ tree test-project 82 | 83 | test-project 84 | └── src 85 | ├── main.py 86 | ├── utils.py 87 | └── parent 88 | ├── child 89 | │ └── __init__.py 90 | └── __init__.py 91 | 92 | 可以首先在本地加密脚本,并保存到分支 `master-obf` ,下面是示例脚本: 93 | 94 | .. code-block:: bash 95 | 96 | $ git clone https://github.com/dashingsoft/test-project 97 | 98 | $ pip install pyarmor 99 | $ pyarmor reg /path/to/pyarmor-regfile-5068.zip 100 | 101 | # 创建新分支 102 | $ git checkout -B master-obf 103 | 104 | # 创建输出目录 dist ,链接到项目目录 105 | $ ln -s test-project dist 106 | 107 | # 加密所有的脚本到输出目录 "dist" ,因为它等同于 "test-project" 108 | # 所以加密后的脚本 "dist/src/main.py" 会覆盖原来的脚本 "test-project/src/main.py" 109 | $ pyarmor gen -O dist -r --platform windows.x86_64,linux.x86_64,darwin.x86_64 test_project/src 110 | 111 | # 增加运行辅助包到这个分支 112 | $ git add -f test_project/pyarmor_runtime_5068/* 113 | 114 | # 提交加密结果和运行辅助包 115 | $ git commit -m'Build obfuscated scripts' . 116 | 117 | # 创建远程分支 118 | $ git push -u origin master-obf 119 | 120 | 对于 CI/CD 管线来说,只需要使用分支 `master-obf` 就基本可以和以前的方式一样不受限制的运行 121 | 122 | .. include:: ../_common_definitions.txt 123 | -------------------------------------------------------------------------------- /docs/how-to/obfuscation.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: console 2 | .. program:: pyarmor gen 3 | 4 | ========================== 5 | 打包脚本的时候保护系统库 6 | ========================== 7 | 8 | .. versionadded:: 8.2 9 | .. versionchanged:: 8.2.2 10 | 不在支持使用选项 :option:`--restrict` 组合 :option:`--pack` 来保护系统库。 11 | 12 | Pyarmor 对使用 PyInstaller 打包进行发布的方式提供了特别的保护,这种模式下面可以对系统库进行加密和保护。其实现的思路就是把所有的依赖包也列出了进行加密。 13 | 14 | 下面是一个示例,说明如何加密一个脚本 ``foo.py`` 并同时保护系统依赖库. 15 | 16 | 我们需要使用 PyInstaller 提供的功能来列出 ``foo.py`` 所有的依赖库。 17 | 18 | 首先生成 ``foo.spec``:: 19 | 20 | $ pyi-makespec foo.py 21 | 22 | 然后修改这个 ``foo.spec``: 23 | 24 | .. code-block:: python 25 | 26 | a = Analysis( 27 | ... 28 | ) 29 | 30 | # Patched by Pyarmor to generate file.list 31 | _filelist = [] 32 | _package = None 33 | for _src in sort([_src for _name, _src, _type in a.pure]): 34 | if _src.endswith('__init__.py'): 35 | _package = _src.replace('__init__.py', '') 36 | _filelist.append(_package) 37 | elif _package is None: 38 | _filelist.append(_src) 39 | elif not _src.startswith(_package): 40 | _package = None 41 | _filelist.append(_src) 42 | with open('file.list', 'w') as _file: 43 | _file.write('\n'.join(_filelist)) 44 | # End of patch 45 | 46 | 接下来使用这个修改后的文件打包 ``foo.py`` ,同时生成包含所有依赖库的文件 :file:`file.list`:: 47 | 48 | $ pyinstaller foo.py 49 | 50 | 最后使用下面的选项加密脚本并重新打包:: 51 | 52 | $ pyarmor gen --assert-call --assert-import --pack dist/foo/foo foo.py @file.list 53 | 54 | 这个例子只是说明了基本的实现方法和步骤,请根据自己的实际情况编写自己的补丁脚本和使用必要的加密选项。如有必要,还可以人工修改生成的依赖库文件 :file:`file.list` 55 | 56 | ======================== 57 | 解决加密过程中编码错误 58 | ======================== 59 | 60 | 默认的脚本编码为 ``utf-8`` ,当加密脚本的时候出现编码错误,可以使用配置项指定正确的文件编码。例如下面的命令可以设置加密脚本使用的编码为 ``gbk``:: 61 | 62 | $ pyarmor cfg encoding=gbk 63 | 64 | 同样也可以为定制的错误消息文件 ``messages.cfg`` 指定编码。例如,使用下面的命令可以设置定制的错误消息文件使用的编码为 ``gbk``:: 65 | 66 | $ pyarmor cfg messages=messages.cfg:gbk 67 | 68 | ====================== 69 | 删除脚本中 Docstring 70 | ====================== 71 | 72 | 使用下面的配置可以删除加密脚本中 DocString:: 73 | 74 | $ pyarmor cfg optimize 2 75 | 76 | 配置项 ``optimize`` 可用值和作用请参考 `compile`__ 77 | 78 | __ https://docs.python.org/3.11/library/functions.html#compile 79 | 80 | .. include:: ../_common_definitions.txt 81 | -------------------------------------------------------------------------------- /docs/how-to/packing.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: console 2 | 3 | ============================ 4 | 打包使用外部密钥的加密脚本 5 | ============================ 6 | 7 | 下面说明如何加密并打包一个应用程序 ``src/myapp.py`` 并使用 :term:`外部密钥` 8 | 9 | 首先使用 PyInstaller 打包:: 10 | 11 | $ pyinstaller myapp.py 12 | 13 | 接着加密脚本并替换生成的可执行文件,同时指定使用外部密钥:: 14 | 15 | $ pyarmor gen --outer --pack dist/myapp/myapp myapp.py 16 | 17 | 然后生成外部密钥:: 18 | 19 | $ pyarmor gen key -O keylist -e 30 20 | 21 | 对于打包成为单个目录的模式来说,一般存放运行密钥到运行辅助包的目录,例如:: 22 | 23 | $ cp keylist/pyarmor.rkey dist/myapp/pyarmor_runtime_000000/ 24 | 25 | 这样就可以在任何位置运行可执行文件 ``dist/myapp/myapp`` 。例如:: 26 | 27 | $ dist/myapp/myapp 28 | 29 | 如果是打包成为单个可执行文件,一般把外部密钥和可执行文件存放在相同目录下面,并且重命名为 ``可执行文件名称.密钥名称`` 。例如:: 30 | 31 | $ pyinstaller --onefile myapp.py 32 | $ pyarmor gen --outer --pack dist/myapp myapp.py 33 | $ pyarmor gen key -O keylist -e 30 34 | $ cp keylist/pyarmor.rkey dist/myapp.pyarmor.rkey 35 | 36 | 然后这样就可以在任意目录运行可执行文件,例如:: 37 | 38 | $ dist/myapp 39 | 40 | 还有一种情况是把运行密钥存放在一个固定目录下面,然后使用环境变量来指定这个目录。例如:: 41 | 42 | $ export PYARMOR_RKEY=/opt/pyarmor/runtime_data 43 | $ mkdir -p /opt/pyarmor/runtime_data 44 | $ cp keylist/pyarmor.rkey /opt/pyarmor/runtime_data/ 45 | $ dist/foo 46 | 47 | .. include:: ../_common_definitions.txt 48 | -------------------------------------------------------------------------------- /docs/how-to/protection.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | 保护运行时刻的数据安全性 3 | ========================== 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | .. program:: pyarmor gen 13 | 14 | Pyarmor 的核心功能是保护 Python 脚本无法被反编译,通过多种不可逆加密模式的实现,已经能够实现 Python 脚本无法使用任何方式完全反编译出来。但是对于内存数据,包括运行时刻的数据保护,Pyarmor 没有进行太多的保护。如果保护的重点是运行时刻的数据,那么就需要额外的工具来进行保护。 15 | 16 | Pyarmor 可以确保各种使用 Python 自身提供的机制无法非法获取运行时刻的数据,但是前提是运行加密脚本的 Python 的解释器和扩展模块 pyarmor_runtime 不能被替换或者修改,尤其是在运行时刻使用调试器直接修改内存代码段,这些就是需要额外的方法和工具来提供保护。常见的保护方式有 17 | 18 | - 使用操作系统提供的签名验证方式确保可执行文件和动态库没有被替换和修改 19 | - 使用第三方的可执行文件的保护工具例如 VMProtect 等保护 Python 以及 pyarmor_runtime.pyd/.so 20 | - Pyarmor 提供了一些保护选项,能够绑定脚本到解释器,发现调试器(弱)就退出等功能 21 | - Pyarmor 提供了脚本补丁,可以用来添加自定义的函数,进行检测调试器等各种自定义的保护,这种需要专业的能力,但是能够提供足够高的安全性,没有人知道用户自定义的保护代码,哪怕是简单的保护,对任何一个黑客来说都需要花费时间去破解。而对于那些公共的保护技术,往往有成熟的工具可以直接绕过 22 | 23 | .. 24 | - 对于 Windows 用户,pyarmor 提供了选项 ``--enable-themida`` 可以有效保护动态库实现反调试 25 | 26 | **基本的实现步骤** 27 | 28 | 需要明白的一点是,如果运行 Python 脚本的解释器可以被定制,那么运行时刻的 Python 数据就没有秘密可言,所以必须要保证运行加密脚本的解释器不能被替换。 29 | 30 | 能实现这一点的加密模式目前 Pyarmor 只提供使用选项 :option:`--pack` 的约束模式,然后使用外部工具保证生成的动态库不能被替换和修改,这样才能够确保无法直接通过 Python 自身的机制来获取运行时刻的数据 [#]_ 。 31 | 32 | 首先配置下列选项 [#]_:: 33 | 34 | $ pyarmor cfg check_debugger=1 check_interp=1 35 | 36 | 接着使用下面的命令加密脚本 [#]_:: 37 | 38 | $ pyarmor gen --mix-str --assert-call --assert-import --private --pack onedir foo.py 39 | 40 | 然后使用其他方式来保护 :file:`dist/foo/` 目录下面所有的可执行文件和动态库,外部工具要确保动态库不能被替换以及在运行时候的内存代码不能被修改。 41 | 42 | 典型的外部工具有 codesign,VMProtect 等。 43 | 44 | .. rubric:: 备注 45 | 46 | .. [#] 为了保护主脚本的属性不会被外部进程读取,需要把真正代码移动到模块中,参考 :option:`--private` 中的说明 47 | 48 | .. [#] 不要在 Intel i686 系列的平台上使用配置项 ``check_interp`` ,这个选项无法在这些平台工作。 49 | 50 | .. [#] 如果使用选项 ``onefile`` 打包成为单个可执行文件,是不可能实现真正的保护效果呢。因为这个文件在执行的时候会解压到一个临时目录下面执行,真正的动态库等相关文件都在这个解压后的目录下面,需要保护的是这些解压后的动态库。 51 | 52 | **脚本补丁** 53 | 54 | 为了进一步提高安全性,还可以使用脚本补丁来检查 PyInstaller 的装载代码,确保其没有被替换。 55 | 56 | 下面的例子只是演示如何实现,请不要直接在项目中使用,并且在不同 PyInstaller 版本中可能会出错,请根据自己的具体情况,参考这个示例编写自己的私有补丁。 57 | 58 | .. code-block:: python 59 | :linenos: 60 | :emphasize-lines: 12-14 61 | 62 | # Hook script ".pyarmor/hooks/foo.py" 63 | 64 | def protect_self(): 65 | from sys import modules 66 | 67 | def check_module(name, checklist): 68 | m = modules[name] 69 | for attr, value in checklist.items(): 70 | if value != sum(getattr(m, attr).__code__.co_code): 71 | raise RuntimeError('unexpected %s' % m) 72 | 73 | checklist__frozen_importlib = {} 74 | checklist__frozen_importlib_external = {} 75 | checklist_pyimod03_importers = {} 76 | 77 | check_module('_frozen_importlib', checklist__frozen_importlib) 78 | check_module('_frozen_importlib_external', checklist__frozen_importlib_external) 79 | check_module('pyimod03_importers', checklist_pyimod03_importers) 80 | 81 | protect_self() 82 | 83 | 目前脚本里面的检查点为空(高亮的行),为了得到真正的检查点,需要先使用下面的一个假函数替换真正的 ``check_module`` 84 | 85 | .. code-block:: python 86 | 87 | def check_module(name, checklist): 88 | m = modules[name] 89 | refs = {} 90 | for attr in dir(m): 91 | value = getattr(m, attr) 92 | if hasattr(value, '__code__'): 93 | refs[attr] = sum(value.__code__.co_code) 94 | print(' checklist_%s = %s' % (name, refs)) 95 | 96 | 97 | 运行下面的命令以得到真正的检查点,代码行会打印在控制台:: 98 | 99 | $ pyinstaller foo.py 100 | $ pyarmor gen --pack dist/foo/foo foo.py 101 | 102 | ... 103 | checklist__frozen_importlib = {'__import__': 9800, ...} 104 | checklist__frozen_importlib_external = {'_calc_mode': 2511, ...} 105 | checklist_pyimod03_importers = {'imp_lock': 183, 'imp_unlock': 183, ...} 106 | 107 | 编辑脚本补丁,恢复原来的函数 ``check_module`` 并使用生成的代码替换空的检查点。 108 | 109 | 最后使用真正的补丁脚本来生成最终的包:: 110 | 111 | $ pyinstaller foo.py 112 | $ pyarmor gen --pack dist/foo/foo foo.py 113 | 114 | **启动补丁** 115 | 116 | .. versionadded:: 8.3 117 | 118 | 用户还可以编写自己的代码去检查调试器和其他任何反调试代码,代码可以使用 Python 实现,在扩展模块 pyarmor_runtime 被装载的时候自动调用。 119 | 120 | 基本配置方式是创建一个脚本 :file:`.pyarmor/hooks/pyarmor_runtime.py` ,定义一个函数 :func:`bootstrap` 来进行额外的检查。例如: 121 | 122 | .. code-block:: python 123 | 124 | def bootstrap(user_data): 125 | from ctypes import windll 126 | if windll.kernel32.IsDebuggerPresent(): 127 | print('found debugger') 128 | return False 129 | 130 | .. 131 | **无法自动打包的处理** 132 | 133 | 有些 PyInstaller 的版本打包的文件 Pyarmor 无法识别,所以无法自动打包,这种情况下需要手动打包 134 | 135 | 首先下载 `pyinstxtractor.py`__ 136 | 137 | 解压打包好的文件:: 138 | 139 | $ python pyinstxtractor.py dist/foo/foo 140 | 141 | 把解压的 pyz 目录也进行加密:: 142 | 143 | $ pyarmor cfg check_debugger=1 check_interp=1 144 | $ pyarmor gen --mix-str --assert-call --assert-import --restrict --pack dist/foo/foo foo.py foo_extracted/PYZ-00.pyz 145 | 146 | __ https://github.com/extremecoders-re/pyinstxtractor/blob/master/pyinstxtractor.py 147 | 148 | .. include:: ../_common_definitions.txt 149 | -------------------------------------------------------------------------------- /docs/how-to/register.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | 使用许可证 3 | ============ 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | .. program:: pyarmor reg 13 | 14 | 前提条件 15 | ======== 16 | 17 | 在注册任何许可证之前,需要满足下列条件 18 | 19 | 1. 一个许可证的 :term:`激活文件` ,参考 :doc:`../licenses` 购买合适的许可证 20 | 2. 安装 Pyarmor 9.0 以上的版本的设备 21 | 3. 当前设备需要能够访问互联网 22 | 4. 确定许可证需要绑定的产品名称 23 | 24 | .. _initial registration: 25 | 26 | 初始登记 27 | ======== 28 | 29 | 任何许可证都必须进行初始登记,指定许可证绑定的产品名称,登记完成之后 Pyarmor 许可证服务器会返回该许可证的 :term:`注册文件` ,其后的所有注册都使用该 :term:`注册文件` 完成 30 | 31 | 初始登记一般只执行一次,在第一次注册的时候,使用 :option:`-p` 指定许可证绑定的产品名称,如果在非商业化产品中的使用 Pyarmor,那么产品名称设定为 ``non-profits`` 32 | 33 | 假设许可证绑定的产品名称为 ``EkeCloud`` ,那么使用下面的命令进行初始登记注册:: 34 | 35 | $ pyarmor reg -p "EkeCloud" pyarmor-regcode-xxxx.txt 36 | 37 | 登记成功之后会同时生成一个相应的 :term:`注册文件` ``pyarmor-regfile-xxxx.zip`` ,这个文件被用来在其他设备上进行注册。 38 | 39 | 请不要再次使用 :term:`激活文件` 进行后续的注册,在初始登记之后这个文件失效,后续在任何设备上注册请使用 :term:`注册文件` 。 40 | 41 | 请妥善保管并备份注册文件,一旦丢失,Pyarmor 并不提供找回服务。 42 | 43 | 一旦登记成功,许可证绑定的产品名称就不能在进行修改。 44 | 45 | 绑定许可证到正在开发的产品 46 | -------------------------- 47 | 48 | 如果产品还在开发中,没有正式开始销售,允许初始化登记的时候设定产品的名称为 ``TBD`` 。例如:: 49 | 50 | $ pyarmor reg -p "TBD" pyarmor-regcode-xxxx.txt 51 | 52 | 绑定的产品名称为 ``TBD`` 的许可证,必须在六个月之内,使用下面的命令设置为正确的产品名称:: 53 | 54 | $ pyarmor reg -p "XXX" pyarmor-regcode-xxxx.txt 55 | 56 | 如果六个月之内还没有进行修改,那么产品名称会被自动设定为 ``non-profits`` ,并且不能被修改。 57 | 58 | 使用基础版和专家版许可证 59 | ======================== 60 | 61 | 基本使用步骤: 62 | 63 | 1. 使用 :term:`激活文件` 进行 :ref:`initial registration` ,生成相应的 :term:`注册文件` 64 | 2. 在其他设备使用 :term:`注册文件` 进行注册 65 | 66 | 拷贝 :term:`注册文件` ``pyarmor-regfile-xxxx.zip`` 到其他需要注册的设备,然后运行下面的命令进行注册:: 67 | 68 | $ pyarmor reg pyarmor-regfile-xxxx.zip 69 | 70 | 查看注册信息:: 71 | 72 | $ pyarmor -v 73 | 74 | 注册成功之后所有的加密操作自动应用当前许可证,每一次加密操作需要联网验证许可证。 75 | 76 | 最多可以在 100 台设备上注册使用 Pyarmor 77 | 78 | 在每台设备上面运行一次注册命令就可以了,不要每次加密之前都运行注册命令 79 | 80 | 请勿在 Docker 容器或者 CI/CD 中直接使用该文件,每运行一次就相当于使用了一台新设备 81 | 82 | .. seealso:: :doc:`ci` 83 | 84 | .. _using ci license: 85 | 86 | 使用管线版许可证 87 | ================ 88 | 89 | .. versionadded:: 9.0 90 | 91 | 基本使用步骤: 92 | 93 | 1. 使用 :term:`激活文件` 进行 :ref:`initial registration` ,生成相应的 :term:`注册文件` 94 | 2. 使用 :term:`注册文件` ``pyarmor-regfile-xxxx.zip`` 在本地设备使用下面的命令向服务器请求管线注册文件:: 95 | 96 | $ pyarmor reg -C pyarmor-regfile-xxxx.zip 97 | 98 | 请求成功之后,会生成管线注册文件 ``pyarmor-ci-xxxx.zip`` 99 | 100 | 在本地设备上面查看管线许可证的相关信息:: 101 | 102 | $ pyarmor --home temp reg pyarmor-ci-xxxx.zip 103 | 104 | 请勿在 CI/CD 管线中申请管线注册文件,因为请求次数有一定限制 105 | 106 | 2. 使用管线注册文件在 CI/CD 管线中注册 Pyarmor:: 107 | 108 | # 请替换 "9.X.Y" 为当前使用的 Pyarmor 的版本 109 | pip install pyarmor==9.X.Y 110 | pyarmor reg pyarmor-ci-xxxx.zip 111 | 112 | 在管线中查看注册信息:: 113 | 114 | pyarmor -v 115 | 116 | 使用管线注册文件的注意事项 117 | 118 | - 管线注册文件的最长有效期为 1 年,过期之后需要重新申请 119 | - 管线注册文件可能在升级后的 Pyarmor 中失效,失效之后也需要重新申请 120 | - 请求管线注册文件的次数最多为 100 次,超过之后将无法申请新的管线注册文件 121 | 122 | .. important:: 123 | 124 | 管线许可证只能用于管线中,不能在本地设备中使用 125 | 126 | 如果 CI/CD 管线中 runner 有自己的硬盘系统,CI 许可证可能无法工作 127 | 128 | 一般情况下,如果 runner 不是 Docker 容器,而是物理设备,直接使用专家版许可证 129 | 130 | .. seealso:: :doc:`ci` 131 | 132 | .. 133 | 在 Docker 或者 CI 管线 中注册 Pyarmor 基础版/专家版许可证的基本方法同上,但是在一天之内运行的 Pyarmor 的 Docker 数量有限制,不能超过 100 个。如果需要在一天之内运行超过 100 个的 Docker 容器,请使用集团版许可证。并且同时运行的 Docker 容器或者 Runner 的数量不能超过 3 个,如果超过这个数量,最好每隔半分钟之后启动一个 Docker 容器或者 Runner,同时运行太多的 `pyarmor reg` 命令会导致许可证服务器返回 HTTP 500 的错误并导致注册失败。 134 | 135 | .. _check device for group license: 136 | 137 | 适用集团版许可证的设备 138 | ====================== 139 | 140 | 集团版许可证只能用于设备硬件信息保持不变的系统,使用下面的方法检查一个设备是否适用集团版许可证 141 | 142 | * 在设备上安装 Pyarmor 8.4.0+ 的试用版本 143 | * 运行下面的命令得到机器标识符:: 144 | 145 | $ pyarmor reg -g 1 146 | ... 147 | INFO current machine id is "mc92c9f22c732b482fb485aad31d789f1" 148 | INFO device file has been generated successfully 149 | 150 | * 重新启动设备,重复上面的命令查看机器标示符 151 | * 如果每一次重启之后机器标识符都保持不变,那么该设备可以使用集团版许可证,否则无法使用基本版许可证 152 | 153 | 对于 Docker 容器来说,只需要按照上面的方法检查 Docker Host。如果 Docker Host 可以使用集团版许可证,那么就可以在其上运行不受限制的 Docker 容器,具体使用方法请参考 :doc:`how-to/register` 中的 ``运行不受限制的 Docker 容器`` 154 | 155 | **如果 Docker Host 的机器标识符每次重启都会发生变化,那么集团版许可证无法运行任何 Docker 容器** 156 | 157 | 大多数的物理设备,云服务器以及使用相同磁盘映像的虚拟机(qemu,virtualbox,vmware)可以使用集团版许可证,集团版许可证不可用于 CI/CD 管线中 158 | 159 | .. _using group license: 160 | 161 | 使用集团版许可证 162 | ================ 163 | 164 | .. versionadded:: 8.2 165 | 166 | 每一个集团版许可证称为一个组,一个组最多可以有 100 个离线设备,每一个设备有一个编号,依次从 1 到 100。 167 | 168 | 必须依次为离线设备进行编号,例如第一台设备编号为 1 ,第二台设备为 2 等等。 169 | 170 | 离线设备可以为物理机,虚拟机,云服务器等各种形式,只要其 设备ID 每次重新启动能保持一致即可。大多数的物理设备,云服务器以及使用相同磁盘映像的虚拟机(Qemu,VirtualBox,Vmware)可以使用集团版许可证。 171 | 172 | 一旦设备注册,将永久占用该设备号。如果已经注册的设备重装系统,或者更换硬件系统,那么需要为其重新分配新的设备号。 173 | 174 | 基本使用步骤: 175 | 176 | 1. 使用 :term:`激活文件` 进行初始登记,设定许可证绑定的产品名称,初始登记完成之后会生成相应的 :term:`注册文件` 177 | 2. 在离线设备上生成相应的组设备文件 178 | 3. 在联网设备上面使用 :term:`注册文件` 和组设备文件生成离线注册文件 179 | 4. 在离线设备上使用离线注册文件进行注册,离线注册文件只能在绑定的设备上使用 180 | 181 | 初始登记 182 | -------- 183 | 184 | 购买集团版许可证之后,一个 :file:`pyarmor-regcode-xxxx.txt` 的 :term:`激活文件` 会发送到注册邮箱中,这个文件用于第一次的注册登记。 185 | 186 | 集团版许可证的初始登记也需要在有网络的设备上进行,初始登记必须指定许可证绑定的产品名称,这里不允许使用 ``TBD`` 。假设产品名称是 ``XXX`` ,运行下面的命令进行初始登记:: 187 | 188 | $ pyarmor reg -p XXX pyarmor-regcode-xxxx.txt 189 | 190 | 登记成功之后会生成相应的 :term:`注册文件` ``pyarmor-regfile-xxxx.zip`` ,这个注册文件可用于后续的注册命令。 191 | 192 | 生成组设备文件 193 | -------------- 194 | 195 | 为了在离线设备上注册 Pyarmor,首先需要生成设备对应的组设备文件 ``pyarmor-group-device.X`` ,其中 ``X`` 是设备编号。 196 | 197 | 在每一台离线设备上安装 Pyarmor ,使用选项 :option:`-g` 指定设备编号,生成相应的设备文件。 198 | 199 | 例如,在第一台离线设备上,运行下面的命令生成组设备文件 ``pyarmor-group-device.1``:: 200 | 201 | $ pyarmor reg -g 1 202 | 203 | INFO Python 3.12.0 204 | INFO Pyarmor 8.4.7 (trial), 000000, non-profits 205 | INFO Platform darwin.x86_64 206 | INFO generating device file ".pyarmor/group/pyarmor-group-device.1" 207 | INFO current machine id is "mc92c9f22c732b482fb485aad31d789f1" 208 | INFO device file has been generated successfully 209 | 210 | 日志中的这一行显示的就是这台设备的标识符:: 211 | 212 | INFO current machine id is "mc92c9f22c732b482fb485aad31d789f1" 213 | 214 | 检查离线设备是否可以使用集团版许可证,可以重新启动系统,然后再次运行该命令,比较一下设备的标识符是否发生改变:: 215 | 216 | $ pyarmor reg -g 1 217 | 218 | ... 219 | INFO current machine id is "mc92c9f22c732b482fb485aad31d789f1" 220 | ... 221 | 222 | 因为集团版许可证是绑定到设备的,所有只用当设备标识符没有改变,那么这台离线设备才可以使用集团版许可证,否则无法使用集团版许可证。 223 | 224 | 对于虚拟机,WSL(Windows Subsystem Linux)以及其他任何系统,如果其设备标识符重启之后发生变化,请参考相应的文档配置系统网络的 Mac 地址以及硬盘的设备序列号保持不变,然后在检查设备标识符是否能够保持不变。如果设备标识符始终发生变化,那么集团版许可证无法在该系统使用。 225 | 226 | 生成离线设备注册文件 227 | -------------------- 228 | 229 | 生成离线设备注册文件需要 230 | 231 | - 安装 Pyarmor 8.2+ 并且可以访问互联网的设备 232 | - 完成初始登记,并且已经生成 :term:`注册文件` ``pyarmor-regfile-xxxx.zip`` 233 | - 离线设备的组设备文件 234 | 235 | 把离线设备的组设备文件拷贝到进行初始登记的机器上(或者任何有互联网连接的设备),并存放在当前目录下面的子目录 ``.pyarmor/group/`` ,然后使用下面的命令生成离线注册文件 ``pyarmor-device-regfile-xxxx.1.zip``:: 236 | 237 | $ mkdir -p .pyarmor/group 238 | $ cp pyarmor-group-file.1 .pyarmor/group/ 239 | 240 | $ pyarmor reg -g 1 /path/to/pyarmor-regfile-xxxx.zip 241 | 242 | 这条命令生成的离线注册文件只可以在第一台设备上使用,因为离线设备注册文件 ``pyarmor-device-regfile-xxxx.1.zip`` 包含设备标识符的信息。 243 | 244 | 离线设备注册 245 | ------------ 246 | 247 | 一旦生成离线设备注册文件之后,就可以拷贝到离线设备上面进行注册。例如,在第一台离线设备上,运行下面的命令:: 248 | 249 | $ pyarmor reg /path/to/pyarmor-device-regfile-xxxx.1.zip 250 | 251 | INFO Python 3.12.0 252 | INFO Pyarmor 8.4.7 (trial), 000000, non-profits 253 | INFO Platform darwin.x86_64 254 | INFO register "/path/to/pyarmor-device-regfile-xxxx.1.zip" 255 | INFO machine id in group license: mc92c9f22c732b482fb485aad31d789f1 256 | INFO got machine id: mc92c9f22c732b482fb485aad31d789f1 257 | INFO this machine id matchs group license 258 | INFO This license registration information: 259 | 260 | License Type : pyarmor-group 261 | License No. : pyarmor-vax-006000 262 | License To : Tester 263 | License Product : btarmor 264 | 265 | BCC Mode : Yes 266 | RFT Mode : Yes 267 | 268 | Notes 269 | * Offline obfuscation 270 | 271 | 注意查看这两行日志:: 272 | 273 | INFO machine id in group license: mc92c9f22c732b482fb485aad31d789f1 274 | INFO got machine id: mc92c9f22c732b482fb485aad31d789f1 275 | 276 | 其中第一行显示的是注册文件中的设备标识符,下面显示的当前设备的标识符,两者只有匹配才能注册成功,如果不匹配,那么需要重新为当前设备生成设备标识符文件和离线注册文件。 277 | 278 | 查看注册信息:: 279 | 280 | $ pyarmor -v 281 | 282 | 注册成功之后所有的加密操作自动应用集团版许可证,注册和加密都不需要联网验证。 283 | 284 | .. important:: 285 | 286 | 如果需要重新为当前设备生成注册文件,必须使用下一个设备编号,原来的设备编号已经被占用而无法使用。例如,应该使用 `pyarmor reg -g 2` ,而不能还依旧使用 `pyarmor reg -g 1` 287 | 288 | 运行不受限制的 Docker 容器 289 | -------------------------- 290 | 291 | .. versionadded:: 8.3 292 | 293 | 集团版许可证支持运行不受限制的 Docker 容器,每一个 Docker 容器都使用当前离线设备的注册文件。这些容器需要使用默认的 Bridge 网络接口,并且没有进行高度定制而导致 Pyarmor 无法识别。 294 | 295 | **基本使用方法** 296 | 297 | 1. 每一个 Docker 主机被当作一个离线设备,并且必须能够注册集团版许可证 298 | 299 | 2. 然后在 Docker 主机上启动一个认证服务,来接受 Docker 容器的认证请求 300 | 301 | 3. 在 Docker 容器中运行 Pyarmor 的时候,会发送认证请求到 Docker 主机,并验证返回的结果 302 | 303 | 4. Docker 主机和 Docker 容器必须在同一个网段,否则 Docker 主机无法接受到 Docker 容器的认证请求。 304 | 305 | **Linux Docker Host** 306 | 307 | 下面是使用集团版许可证的实践 308 | 309 | - Docker 主机, Ubuntu x86_64, Python 3.8 310 | - Docker 容器, Ubuntu x86_64, Python 3.11 311 | 312 | 为了运行不受限制的容器,Docker 主机需要 313 | 314 | - 离线设备注册文件 ``pyarmor-device-regfile-xxxx.1.zip`` ,参考上面的方法生成 315 | - 安装 Pyarmor 8.4.1+ 316 | 317 | 首先拷贝下面的文件到 Docker 主机: 318 | 319 | - pyarmor-8.4.1.tar.gz 320 | - pyarmor.cli.core-5.4.1-cp38-none-manylinux1_x86_64.whl 321 | - pyarmor.cli.core-5.4.1-cp311-none-manylinux1_x86_64.whl 322 | - pyarmor-device-regfile-6000.1.zip 323 | 324 | 然后在 Docker 主机上运行下面的命令:: 325 | 326 | $ python3 --version 327 | Python 3.8.10 328 | 329 | $ pip install pyarmor.cli.core-5.4.1-cp38-none-manylinux1_x86_64.whl 330 | $ pip install pyarmor-8.4.1.tar.bgz 331 | 332 | 接着启动 ``pyarmor-auth`` 来侦听来自 Docker 容器的认证请求:: 333 | 334 | $ pyarmor-auth pyarmor-device-regfile-6000.1.zip 335 | 336 | 2023-06-24 09:43:14,939: work path: /root/.pyarmor/docker 337 | 2023-06-24 09:43:14,940: register "pyarmor-device-regfile-6000.1.zip" 338 | 2023-06-24 09:43:15,016: listen container auth request on 0.0.0.0:29092 339 | 340 | 不要关闭这个命令窗口,另外打开一个命令窗口运行容器。 341 | 342 | 运行 Linux 系统的容器时候需要使用额外参数 ``--add-host=host.docker.internal:host-gateway`` (运行 Windows 和 Darwin 容器不需要):: 343 | 344 | $ docker run -it --add-host=host.docker.internal:host-gateway python bash 345 | 346 | root@86b180b28a50:/# python --version 347 | Python 3.11.4 348 | root@86b180b28a50:/# 349 | 350 | 在 Docker 主机上打开第三个控制台拷贝文件到容器:: 351 | 352 | $ docker cp pyarmor-8.4.1.tar.gz 86b180b28a50:/ 353 | $ docker cp pyarmor.cli.core-5.4.1-cp311-none-manylinux1_x86_64.whl 86b180b28a50:/ 354 | $ docker cp pyarmor-device-regfile-6000.1.zip 86b180b28a50:/ 355 | 356 | 最后在 Docker 容器中,注册 Pyarmor 并进行加密脚本:: 357 | 358 | root@86b180b28a50:/# pip install pyarmor.cli.core-5.4.1-cp311-none-manylinux1_x86_64.whl 359 | root@86b180b28a50:/# pip install pyarmor-8.4.1.tar.gz 360 | root@86b180b28a50:/# pyarmor reg pyarmor-device-regfile-6000.1.zip 361 | root@86b180b28a50:/# pyarmor -v 362 | 363 | 如果配置正确,应该显示许可证的相关信息。 364 | 365 | 下面进行简单的测试:: 366 | 367 | root@86b180b28a50:/# echo "print('hello world')" > foo.py 368 | root@86b180b28a50:/# pyarmor gen --enable-rft foo.py 369 | 370 | 当需要验证许可证的时候,Docker 容器会发送请求到主机。如果 Docker 主机的 `pyarmor-auth` 对应的控制台没有收到任何请求,请检查 Docker 网络配置,确保主机和容器的 IPv4 地址在同一个网段。 371 | 372 | **MacOS Docker Host** 373 | 374 | 如果 Docker 主机是 MacOS 的时候,和 Linux 主机稍微有一些不同。因为 Docker 容器是运行在 Linux 虚拟机中,而不是直接运行在 MacOS 上面。 375 | 376 | 一个解决方案是直接在 Linux VM 上运行 `pyarmor-auth` ,在这种情况下,必须把 Linux VM 作为一个离线设备进行注册,而不是把 MacOS 作为离线设备进行注册,然后使用下面的额外参数启动 Docker 容器:: 377 | 378 | $ docker run --add-host=host.docker.internal:172.17.0.1 ... 379 | 380 | 在这种情况下,也许需要对 Linux VM 进行额外的配置以确保其设备标识符重启之后保持不变。 381 | 382 | 参考 `issue 1542`__ 了解更多信息。 383 | 384 | __ https://github.com/dashingsoft/pyarmor/issues/1542 385 | 386 | **Windows Docker Host** 387 | 388 | 对于 Windows Docker 主机,首先检查网络配置:: 389 | 390 | C:> ipconfig 391 | 392 | Ethernet adapter vEthernet (WSL): 393 | 394 | Connection-specific DNS Suffix . : 395 | Link-local IPv6 Address . . . . . : fe80::8984:457:2335:588e%28 396 | IPv4 Address. . . . . . . . . . . : 172.22.32.1 397 | Subnet Mask . . . . . . . . . . . : 255.255.240.0 398 | Default Gateway . . . . . . . . . : 399 | 400 | 如果它有一个 IPv4 地址,例如 ``172.22.32.1`` 是和 Docker 容器在同一个网段,那么事情就简单了。只需要把 Windows 作为离线设备进行注册,然后在上面运行 `pyarmor-auth` ,并且使用下面的额外选项启动 Docker 容器:: 401 | 402 | $ docker run --add-host=host.docker.internal:172.22.32.1 ... 403 | 404 | 总之, `pyarmor-auth` 侦听的 IPv4 地址必须和 Docker 容器的 IPv4 地址在同一个网段,只要能达到这个目的,怎么进行配置都可以。 405 | 406 | 如果 Windows 没有任何 IPv4 地址和 Docker 容器在同一个网段,那么另外一种解决方案就是把 WSL (Windows Subsystem Linux)作为离线设备进行注册,然后运行 `pyarmor-auth` 在 WSL 里面。在这种情况下,也需要对 WSL 进行额外的配置以确保其设备标识符保持不变,配置方式请参考 WSL 的文档。 407 | 408 | 支持多设备的离线注册文件 409 | ------------------------ 410 | 411 | .. versionadded:: 8.x 412 | 该功能尚未实现 413 | 414 | 如果需要为所有设备生成一个集成的离线注册文件,首先把所有组设备文件拷贝到指定目录,然后使用特殊组号 ``0`` 生成所有设备可用的离线注册文件 ``pyarmor-device-regfile-xxxx.zip``:: 415 | 416 | $ cp pyarmor-group-file.1 pyarmor-group-file.2 pyarmor-group-file.3 .pyarmor/group/ 417 | 418 | $ pyarmor reg -g 0 /path/to/pyarmor-regfile-xxxx.zip 419 | 420 | 然后拷贝到每一台离线设备,使用 :option:`-g` 指定设备编号进行注册:: 421 | 422 | $ pyarmor reg -g 1 pyarmor-device-regfile-xxxx.zip 423 | 424 | 同一台设备使用多个许可证 425 | ======================== 426 | 427 | 使用默认选项的时候,注册文件都是保存在 Pyarmor 的 :term:`根目录` ,通常情况下,就是 :file:`~/.pyarmor` ,这也意味着 428 | 429 | - 不同的 Python 的虚拟环境会共享注册文件 430 | - 如果需要在同一台设备上使用不同的许可证,那么会存在问题 431 | 432 | 当需要在一台设备上注册多个 :term:`Pyarmor 许可证` 的时候,可以为每一个许可证设置一个 :term:`根目录` 。例如:: 433 | 434 | $ pyarmor --home ~/.pyarmor1 reg pyarmor-regfile-2051.zip 435 | $ pyarmor --home ~/.pyarmor2 reg pyarmor-regfile-2052.zip 436 | 437 | $ pyarmor --home ~/.pyarmor1 gen project1/foo.py 438 | $ pyarmor --home ~/.pyarmor2 gen project2/foo.py 439 | 440 | .. _pyarmor: 441 | 442 | 升级 Pyarmor 之后的许可证兼容性 443 | =============================== 444 | 445 | 一般情况下,升级 Pyarmor 之后不需要重新注册,原来的注册信息依旧有效。 446 | 447 | 但是下面列出的版本是例外 448 | 449 | - **Pyarmor 8.0** 450 | 451 | 老版本许可证无法在 Pyarmor 8.0 之后的版本中直接使用 452 | 453 | - 部分老版本许可证可以免费升级到基础版许可证,具体升级步骤请参考 :ref:`upgrading old license` 454 | - 老版本许可证无法升级为专家版或者集团版许可证 455 | 456 | - **Pyarmor 9.0** 457 | 458 | 主要是对在 CI/CD 管线环境的使用 Pyarmor 许可证进行了大的变更 459 | 460 | - 基础版许可证 461 | 462 | - 可以直接 :ref:`upgrade to pyarmor 9` 463 | - 如果在 CI/CD 管线中使用,还需要为管线生成专门的注册文件,请参考 :doc:`ci` 464 | 465 | - 专家版许可证 466 | 467 | - 如果没有在 CI/CD 管线中使用,可以直接 :ref:`upgrade to pyarmor 9` 468 | - 如果需要使用在 CI/CD 管线中,那么两种方案 469 | 470 | - 按照原来的协议继续使用任意 Pyarmor 8.x 的版本 471 | - 升级到 Pyarmor 9,同时需要购买新的 CI 许可证 472 | 473 | - 集团版许可证需要重新生成设备注册文件,参考 :ref:`upgrade to pyarmor 9` 474 | 475 | .. _upgrading old license: 476 | 477 | 升级老版本许可证 478 | ---------------- 479 | 480 | 不是所有的老版本的许可证都可以升级为新的许可证。 481 | 482 | 符合下列条件的老版本许可证可以免费升级到 Pyarmor 基础版许可证: 483 | 484 | * 遵循新的 `Pyarmor 最终用户许可协议`_ 485 | * 原来的许可证编号是以 ``pyarmor-vax-`` 开头的 486 | * 原来许可证的注册文件 ``pyarmor-regcode-xxxx.txt`` 存在且不能被使用超过 100 次 487 | * 原来的许可证的购买日期在 2023年6月1日之前,原则上在 Pyarmor 8 发布之后依旧购买的老许可证不支持升级。 488 | 489 | 如果无法免费升级,请购买新的许可证。 490 | 491 | 老版本的许可证不支持升级到专家版和集团版。 492 | 493 | **免费升级到基础版** 494 | 495 | 首先找到原来的许可证激活文件 ``pyarmor-regcode-xxxx.txt`` 496 | 497 | 然后安装 Pyarmor 8.2+ 498 | 499 | 按照新的 `Pyarmor 最终用户许可协议`_ ,需要为每一个许可证指定产品名称。这也意味着,如果老的许可证是被用于多种产品的话,升级之后就只能用于其中的一个,其他产品还需要购买新的许可证。 500 | 501 | 假定使用许可证的产品名称是 ``XXX`` ,那么使用下面的命令进行升级:: 502 | 503 | $ pyarmor reg -u -p "XXX" pyarmor-regcode-xxxx.txt 504 | 505 | 升级成功之后会生成新的 :term:`注册文件` 506 | 507 | 在其他设备直接使用新的 :term:`注册文件` 进行注册:: 508 | 509 | $ pyarmor reg pyarmor-regfile-xxxx.zip 510 | 511 | 运行下面的命令检查升级后的许可证:: 512 | 513 | $ pyarmor -v 514 | 515 | 注册成功之后所有的加密操作自动应用当前许可证,每一次加密操作需要联网验证许可证。 516 | 517 | .. _upgrade to pyarmor 9: 518 | 519 | 升级到 Pyarmor 9 520 | ---------------- 521 | 522 | 1. 基础版和专家版许可证的升级步骤 523 | 524 | **如果当前设备已经注册 Pyarmor** 525 | 526 | - 首先升级到 Pyarmor 9:: 527 | 528 | $ pip install -U pyarmor 529 | 530 | - 升级之后在第一次加密的时候会提示:: 531 | 532 | $ pyarmor gen foo.py 533 | 534 | ... 535 | Pyarmor 9 has big change on CI/CD pipeline 536 | If not using Pyarmor License in CI/CD pipeline 537 | Press "c" to continue 538 | Otherwise press "h" to check Pyarmor 9.0 Upgrade Notes 539 | 540 | Continue (c), Help (h), Quit (q): 541 | 542 | - 直接按下 :kbd:`c` 并回车即可,第二次加密的时候就不会再提示了 543 | 544 | **如果当前设备还没有注册 Pyarmor** 545 | 546 | - 在 Pyarmor 9 使用 :term:`激活文件` 重新生成许可证的 :term:`注册文件`:: 547 | 548 | $ pip install -U pyarmor 549 | 550 | # 请替换 XXX 为原来的绑定的产品名称 551 | $ pyarmor reg -p XXX pyarmor-regcode-xxxx.txt 552 | 553 | - 保存新生成的 :term:`注册文件` ``pyarmor-regfile-xxxx.zip`` 554 | 555 | - 使用新的注册文件在其他新的设备注册 Pyarmor:: 556 | 557 | $ pyarmor reg pyarmor-regfile-xxxx.zip 558 | $ pyarmor -v 559 | 560 | 如果 :term:`激活文件` 因为使用次数过多而不可用,请首先安装 Pyarmor 8,注册成功之后在升级到 Pyarmor 9 561 | 562 | 2. 集团版许可证的升级步骤 563 | 564 | 使用 Pyarmor 9.0 之前版本生成的设备注册文件在 Pyarmor 9.0+ 版本中无效 565 | 566 | - 在联网设备上面安装升级 Pyarmor 到 9.0+:: 567 | 568 | $ pip install -U pyarmor 569 | 570 | - 使用集团版注册文件为设备重新生成注册文件,方法和第一次生成设备注册文件是一样的 571 | 572 | 例如,为编号为 1 的离线设备重新生成设备注册文件:: 573 | 574 | $ pyarmor reg -g 1 /path/to/pyarmor-regfile-6000.zip 575 | 576 | - 使用新生成的设备注册文件在离线设备注册 Pyarmor:: 577 | 578 | $ pyarmor reg pyarmor-device-regfile-6000.1.zip 579 | 580 | .. include:: ../_common_definitions.txt 581 | -------------------------------------------------------------------------------- /docs/how-to/security.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | 最高安全性和最快性能 3 | ====================== 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | .. program:: pyarmor gen 13 | 14 | 如何生成最高安全性的脚本 15 | ======================== 16 | 17 | 下面的选项都可以用来增加安全性 18 | 19 | * :option:`--enable-rft` 几乎不影响性能,是推荐选项 20 | * :option:`--enable-bcc` 能增加函数执行速度,但是可能需要的内存会多一些 21 | * :option:`--enable-jit` 可以防止静态反编译 22 | * :option:`--enable-themida` 可以防止动态调试器,但是对性能影响比较大,并且仅在 Windwos 可用 23 | * :option:`--mix-str` 保护脚本的所有字符串常量 24 | * :option:`--obf-code` ``2`` 能够同时增加反编译 Bytecode 的难度 25 | * ``pyarmor cfg mix_argnames=1`` 保护函数参数,但是可能导致 annotations 不可用 26 | 27 | 下面的选项可以隐藏加密模块的属性 28 | 29 | * :option:`--private` 30 | * :option:`--restrict` 甚至不允许外部脚本直接导入和使用加密脚本 31 | 32 | 下面的选项可以防止加密脚本和加密脚本中函数被其他人替换成为自己的函数 33 | 34 | * :option:`--assert-call` 35 | * :option:`--assert-import` 36 | 37 | 如何生成最快的加密脚本 38 | ====================== 39 | 40 | 使用下面的选项可以生成性能最高的加密脚本,其安全性相当于 `.pyc` 41 | 42 | * :option:`--obf-code` ``0`` 43 | * :option:`--obf-module` ``0`` 44 | * ``pyarmor cfg restrict_module=0`` 45 | 46 | 如果需要提高安全性,但是对性能又不要影响太大,最好的选择是同时启用 :term:`RFT 模式` 47 | 48 | * :option:`--enable-rft` 49 | 50 | 如果有敏感字符串,那么使用 :option:`--mix-str` 同时设置过滤条件,仅仅加密这些敏感字符串。如果没有过滤条件,所有的字符串常量都会加密,可能对性能会造成一点影响 51 | 52 | * ``pyarmor cfg mix.str:includes "/regular expression/"`` 53 | * :option:`--mix-str` 54 | 55 | 不同类型应用的推荐选项 56 | ====================== 57 | 58 | **对于服务网络请求类型的应用** 59 | 60 | 如果 :term:`RFT 模式` 已经足够安全(通过检查转换后的脚本来确定是否足够安全),那么使用下面的选项进行加密 61 | 62 | * :option:`--enable-rft` 63 | * :option:`--obf-code` ``0`` 64 | * :option:`--obf-module` ``0`` 65 | * :option:`--mix-str` 和过滤条件,如果不设置过滤条件能满足性能要求,也可以不设置 66 | 67 | 如果单独的 :term:`RFT 模式` 不能满足安全需求,那么使用下面的选项 68 | 69 | * :option:`--enable-rft` 70 | * :option:`--no-wrap` 71 | * :option:`--mix-str` 和过滤条件 72 | 73 | **对于其他大多数的应用** 74 | 75 | 如果 :term:`RFT 模式` 和 :term:`BCC 模式` 可用,那么使用下面的选项 76 | 77 | * :option:`--enable-rft` 78 | * :option:`--enable-bcc` 79 | * :option:`--mix-str` 和过滤条件 80 | * :option:`--assert-import` 81 | 82 | 如果 :term:`RFT 模式` 和 :term:`BCC 模式` 不可用,使用下面的选项 83 | 84 | * :option:`--enable-jit` 85 | * :option:`--private` 或者 :option:`--restrict` 限制外部脚本访问加密脚本 86 | * :option:`--mix-str` 和过滤条件 87 | * :option:`--assert-import` 88 | * :option:`--obf-code` ``2`` 89 | 90 | 如果需要保护关键函数避免被其他人替换成为没有加密的函数,使用下面的选项 91 | 92 | * :option:`--assert-call` ,同时检查跟踪日志,确保关键函数被保护,必要的时候使用运行 :term:`脚本补丁` 对这些函数进行保护 93 | 94 | 如果性能允许的话,启用选项 :option:`--enable-themida` ,这个选项还是能很大程度的防止调试器的攻击,但就是对性能影响大一些 95 | 96 | 97 | 重构脚本增加安全性 98 | ================== 99 | 100 | **重构主脚本** 101 | 102 | Pyarmor 在装载完模块之后,会把模块级别的代码进行清理,清理之后即使通过注入方式也无法在获取代码。 103 | 104 | 但是对于主模块,因为它永远不会执行完,所以模块级别的代码不会被清理。通过把主模块中非必要的代码,移到其他模块,然后在导入这些代码,这样能够提高安全性。 105 | 106 | .. include:: ../_common_definitions.txt 107 | -------------------------------------------------------------------------------- /docs/how-to/third-party.rst: -------------------------------------------------------------------------------- 1 | ======================================== 2 | 如何解决第三方库调用加密脚本存在的问题 3 | ======================================== 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | .. program:: pyarmor gen 13 | 14 | Pyarmor 提供了丰富的选项就是为了解决不同情况下遇到的问题,当使用一个复杂的第三方包调用加密脚本出现问题的时候,请首先花费一点时间看看 :doc:`../reference/man` ,去了解所有的选项和作用,有一些选项就是专门针对不同的应用情况设计的。 **Pyarmor 开发组不会去告诉应该使用那个选项就可以解决你遇到的问题** ,而是你需要去学习和了解 Pyarmor,并找到适合自己需要的选项。 15 | 16 | Python 在各个领域得到广泛的应用,有很多包我甚至从来都没有用过。对于我来说,不会去学习每一个包,然后确保其能够和 Pyarmor 兼容。通常的处理方式是用户报告相关的异常,根据异常的行号给出附近的源代码,我可以帮助分析哪些地方可能和 Pyarmor 发生冲突并给出相应的解决方案。 17 | 18 | 对于属于 Pyarmor 的问题,Pyarmor 开发组会尽快解决,但是有一些问题是 Pyarmor 自身无法解决的。 19 | 20 | 通常情况下,使用第三方包主要导致的问题来源有: 21 | 22 | * 使用 ``sys._getframe`` 去访问函数的局部变量,或者其他运行框架的信息,但是加密脚本的运行框架和普通脚本是不一样的 23 | * 使用 :mod:`inspect` 或者其他方式直接去访问函数的源代码(Byte code)等相关属性,它们所访问的属性正好就是 Pyarmor 所保护的 24 | * 使用 :mod:`pickle` 或者类似功能序列号加密函数,然后把加密函数传递给其他进程或者线程执行,但是加密函数是无法使用普通方法序列化的 25 | 26 | 这些问题都来源于第三方包使用到了加密脚本修改过的底层对象,更多的不同之处请参考 :ref:`the differences of obfuscated scripts` 。如果使用被修改的特性,就会出现不兼容的问题。特别是使用 :term:`BCC 模式` 进行加密的脚本,改变的内容更多。 27 | 28 | 对于这些问题,有一些常用的解决方案,可以首先尝试下面的方式 29 | 30 | - 使用 :term:`RFT 模式` 和选项 :option:`--obf-code` ``0`` 31 | 32 | :term:`RFT 模式` 几乎不改变原来脚本的内部结构,所以一般不会导致不兼容性。使用 :option:`--obf-code` 禁用代码对象的加密也是为了保持内部对象的结构不改变。这是一种推荐的解决方案:: 33 | 34 | $ pyarmor gen --enable-rft --obf-code 0 /path/to/myapp 35 | 36 | 首先使用最少的选项确保其能工作,然后在尝试更多的选项去增加安全性。例如:: 37 | 38 | $ pyarmor gen --enable-rft --obf-code 0 --mix-str /path/to/myapp 39 | $ pyarmor gen --enable-rft --obf-code 0 --mix-str --assert-call /path/to/myapp 40 | 41 | - 忽略有问题的模块 42 | 43 | 在一个复杂的应用中,如果只是个别模块存在问题,可以不加密这些模块。例如,如果只有模块 ``config.py`` 不能正常工作,使用模块私有配置的方式把这个模块忽略掉:: 44 | 45 | $ pyarmor cfg -p myapp.config obf_code=0 46 | $ pyarmor gen [other options] /path/to/myapp 47 | 48 | 也可以按照原来的方法加密,只是加密之后使用原来的脚本直接把加密脚本替换:: 49 | 50 | $ pyarmor gen [other options] /path/to/myapp 51 | $ cp /path/to/myapp/config.py dist/myapp/config.py 52 | 53 | - 修改第三方库 54 | 55 | 这是一个实例,如果使用别名 "myapi" 去调用,抛出 404 错误,但是不使用别名工作正常。 56 | 57 | .. code-block:: python 58 | 59 | @cherrypy.expose(alias='myapi') 60 | @cherrypy.tools.json_out() 61 | # pylint: disable=no-member 62 | @cherrypy.tools.authenticate() 63 | @cherrypy.tools.validateOptOut() 64 | @cherrypy.tools.validateHttpVerbs(allowedVerbs=['POST']) 65 | # pylint: enable=no-member 66 | def abc_xyz(self, arg1, arg2): 67 | """ 68 | This is the doc string 69 | """ 70 | 71 | 导致问题出现的原因在于 ``cherrypy.expose`` 使用下面的语句 72 | 73 | .. code-block:: python 74 | 75 | parents = sys._getframe(1).f_locals 76 | 77 | 因为 ``sys._getframe(1)`` 在加密脚本返回的不是期望的执行框架,把它修改成为下面的语句就可以在加密脚本正常使用: 78 | 79 | .. code-block:: python 80 | 81 | parents = sys._getframe(2).f_locals 82 | 83 | .. note:: 84 | 85 | 如果第三方包 :mod:`cheerypy` 也被其他程序使用,请为加密脚本创建一个私有的包 86 | 87 | 常用的第三方库 88 | ============== 89 | 90 | 这里列出了一些常用到的第三方库以及可能的解决方案,欢迎大家分享自己的使用经验,提交 Pull request 增加新的第三方库。 91 | 92 | .. list-table:: 表-1. 常用第三方库列表 93 | :header-rows: 1 94 | 95 | * - 库 96 | - 状态 97 | - 备注 98 | * - cherrypy 99 | - 打补丁 [#patch]_ 100 | - 问题原因是使用了 sys._getframe 101 | * - `pandas`_ 102 | - 打补丁 [#patch]_ 103 | - 问题原因是使用了 sys._getframe 104 | * - playwright 105 | - 打补丁应该可以工作 [#RFT]_ 106 | - 尚未验证 107 | * - `nuitka`_ 108 | - 使用 restrict_module = 0 之后应该可以工作 109 | - 尚未验证 110 | * - `Cython`_ 111 | - 使用 restrict_module = 0 之后可以工作 112 | - 113 | 114 | .. rubric:: 说明 115 | 116 | .. [#patch] 通过打补丁的方式可以使用加密脚本 117 | .. [#RFT] 可以使用 :term:`RFT 模式` 加密的脚本 118 | .. [#obfcode0] 只有使用 ``--obf-code 0`` 加密的脚本可以工作 119 | .. [#not] 任何方式都无法使用加密脚本 120 | 121 | pandas 122 | ------ 123 | 124 | 另外一个实例是 :mod:`pandas` 125 | 126 | .. code-block:: python 127 | 128 | import pandas as pd 129 | 130 | class Sample: 131 | def __init__(self): 132 | self.df = pd.DataFrame( 133 | data={'name': ['Alice', 'Bob', 'Dave'], 134 | 'age': [11, 15, 8], 135 | 'point': [0.9, 0.1, 0.4]} 136 | ) 137 | 138 | def func(self, val: float = 0.5) -> None: 139 | print(self.df.query('point > @val')) 140 | 141 | sampler = Sample() 142 | sampler.func(0.3) 143 | 144 | 加密之后运行报错:: 145 | 146 | pandas.core.computation.ops.UndefinedVariableError: local variable 'val' is not defined 147 | 148 | 同样需要对其打补丁,把 pandas ``scope.py`` 里面的 ``sys._getframe(self.level)`` 修改成为 ``sys._getframe(self.level+1)`` , ``sys._getframe(self.level+2)`` 或者 ``sys._getframe(self.level+3)`` 149 | 150 | nuitka 151 | ------ 152 | 153 | 把加密脚本作为普通脚本一样使用 Nuitka 进行处理,但是需要禁用约束模式:: 154 | 155 | $ pyarmor cfg restrict_module=0 156 | 157 | 如果不禁用约束模式,会导致校验错误 ``RuntimeError: unauthorized use of script`` 158 | 159 | 使用默认选项进行加密:: 160 | 161 | $ pyarmor gen foo.py 162 | 163 | 然后在尝试使用更多选项,但是约束相关的这些选项,例如 :option:`--private` , :option:`--restrict` , :option:`--assert-call` , :option:`--assert-import` 可能无法使用。 164 | 165 | .. note:: 166 | 167 | 可能需要升级到 v9.0.8 之后的非试用版本。因为 Nuitka 会把包 `pyarmor_runtime_000000/__init__.py` 转换成为模块 `pyarmor_runtime_000000_init_.py` ,运行时候通用会报错 ``RuntimeError: unauthorized use of script`` ,在 v9.0.8 中解决了这个问题 168 | 169 | streamlit 170 | --------- 171 | 172 | 需要禁用下列选项,然后在进行加密:: 173 | 174 | $ pyarmor cfg restrict_module=0 175 | $ pyarmor cfg clear_module_co=0 176 | 177 | $ pyarmor gen foo.py 178 | 179 | 不禁用第一项可能会报错 `RuntimeError: unauthorized use of script (1:1102)` 180 | 181 | 不禁用第二项可能会报错 `RuntimeError: the format of obfuscated script is incorrect (1:1082)` 182 | 183 | **不过 Streamlit 依旧可能无法直接使用加密脚本,因为它是直接访问甚至修改 code object** 184 | 185 | Cython 186 | ------ 187 | 188 | 下面是一个简单的例子,来说明如何把一个脚本 :file:`foo.py` 加密之后在使用 Cython 把它转换成为扩展模块,脚本的内容如下:: 189 | 190 | print('Hello Cython') 191 | 192 | 首先加密脚本,需要禁用下列选项,然后在进行加密:: 193 | 194 | $ pyarmor cfg restrict_module=0 195 | $ pyarmor gen foo.py 196 | $ ls dist/ 197 | 198 | 接下来使用 `cythonize` 把 `foo.py` 转换成为 `foo.c` 文件:: 199 | 200 | $ cd dist 201 | $ cythonize -3 foo.py 202 | 203 | 最后把 `foo.c` 编译成为扩展模块(有的平台需要把额外的编译选项 ``-fPIC`` 加入到命令行)。例如:: 204 | 205 | $ gcc -shared $(python-config --cflags) $(python-config --ldflags) \ 206 | -o foo$(python-config --extension-suffix) foo.c 207 | 208 | 最后测试一下扩展模块,把 `dist/foo.py` 文件删除了,然后导入加密后的脚本:: 209 | 210 | $ rm foo.py 211 | $ python -c 'import foo' 212 | 213 | 像预想的那样会在控制台打印出 `Hello Cython` 214 | 215 | .. include:: ../_common_definitions.txt 216 | -------------------------------------------------------------------------------- /docs/how-to/wheel.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: console 2 | 3 | ======================== 4 | 打包加密脚本成为 Wheel 5 | ======================== 6 | 7 | 测试项目的目录结构如下:: 8 | 9 | $ tree test-project 10 | 11 | test-project 12 | ├── MANIFEST.in 13 | ├── pyproject.toml 14 | ├── setup.cfg 15 | └── src 16 | └── parent 17 | ├── child 18 | │ └── __init__.py 19 | └── __init__.py 20 | 21 | 文件 ``MANIFEST.in`` 的内容如下:: 22 | 23 | recursive-include dist/parent/pyarmor_runtime_00xxxx *.so 24 | 25 | 文件 ``pyproject.toml`` 26 | 27 | .. code-block:: ini 28 | 29 | [build-system] 30 | requires = [ 31 | "setuptools>=66.1.1", 32 | "wheel" 33 | ] 34 | build-backend = "setuptools.build_meta" 35 | 36 | 文件 ``setup.cfg`` 37 | 38 | .. code-block:: ini 39 | 40 | [metadata] 41 | name = parent.child 42 | version = attr: parent.child.VERSION 43 | 44 | [options] 45 | package_dir = 46 | =dist/ 47 | 48 | packages = 49 | parent 50 | parent.child 51 | parent.pyarmor_runtime_00xxxx 52 | 53 | include_package_data = True 54 | 55 | :file:`src/parent/__init__.py` 和 :file:`src/parent/child/__init__.py` 是相同的: 56 | 57 | .. code-block:: python 58 | 59 | VERSION = '0.0.1' 60 | 61 | 首先加密包:: 62 | 63 | $ cd test-project 64 | $ pyarmor gen --recursive -i src/parent 65 | 66 | 加密成功之后输出的目录结构如下:: 67 | 68 | $ tree dist 69 | 70 | dist 71 | └── parent 72 | ├── child 73 | │ ├── __init__.py 74 | │ └── __pycache__ 75 | │ └── __init__.cpython-311.pyc 76 | ├── __init__.py 77 | └── pyarmor_runtime_00xxxx 78 | ├── __init__.py 79 | └── pyarmor_runtime.so 80 | 81 | 接着就构建 wheel:: 82 | 83 | $ python -m build --skip-dependency-check --no-isolation 84 | 85 | 但是却报错了 86 | 87 | .. code-block:: python 88 | 89 | * Building sdist... 90 | Traceback (most recent call last): 91 | File "/usr/lib/python3/dist-packages/setuptools/config/expand.py", line 81, in __getattr__ 92 | return next( 93 | ^^^^^ 94 | StopIteration 95 | 96 | The above exception was the direct cause of the following exception: 97 | 98 | Traceback (most recent call last): 99 | File "/usr/lib/python3/dist-packages/setuptools/config/expand.py", line 191, in read_attr 100 | return getattr(StaticModule(module_name, spec), attr_name) 101 | 102 | 检查错误堆栈发现问题出现在 ``StaticModule`` ,通过异常中文件名称和行号,查看在包 ``setuptools`` 源代码中这个类的定义,它使用了 ``ast.parse`` 直接解析源代码获取模块的局部变量,这当然在加密脚本中行不通的。为了解决这个问题,可以直接在加密后的脚本 ``dist/parent/child/__init__.py`` 中增加一行 103 | 104 | .. code-block:: python 105 | 106 | from pyarmor_runtime_00xxxx import __pyarmor__ 107 | VERSION = '0.0.1' 108 | ... 109 | 110 | 但是默认情况下加密脚本是不允许修改的,为了能够修改这个脚本,需要使用下面的命令进行配置:: 111 | 112 | $ pyarmor cfg -p parent.child.__init__ restrict_module = 0 113 | $ pyarmor gen --recursive -i src/parent 114 | 115 | 其中选项 :option:`pyarmor cfg -p` ``parent.child.__init__`` 是只取消对单个模块的 ``parent/child/__init__.py`` 的限制,而其他模块依旧使用约束模式。 116 | 117 | 现在修改 ``dist/parent/child/__init__.py`` 之后再次运行下面的命令进行打包:: 118 | 119 | $ python -m build --skip-dependency-check --no-isolation 120 | 121 | **自定义运行辅助包的名称** 122 | 123 | 如果你想自定义运行辅助包的名称为 ``libruntime`` ,并且存放到子目录 ``parent.child`` 下面,你需要修改 ``MANIFEST.in``:: 124 | 125 | recursive-include dist/parent/child/libruntime *.so 126 | 127 | 和 ``setup.cfg``:: 128 | 129 | [options] 130 | ... 131 | packages = 132 | parent 133 | parent.child 134 | parent.child.libruntime 135 | ... 136 | 137 | 然后使用下面的配置进行加密:: 138 | 139 | $ pyarmor cfg package_name_format "libruntime" 140 | $ pyarmor gen --recursive --prefix parent.child src/parent 141 | 142 | 在创建 wheel 之前不要忘记对 ``dist/parent/child/__init__.py`` 进行修改:: 143 | 144 | $ python -m build --skip-dependency-check --no-isolation 145 | 146 | **更进一步** 147 | 148 | 为了能够在加密完成之后自动修改 ``dist/parent/child/__init__.py`` ,你可以创建一个 :term:`加密插件` ``.pyarmor/myplugin.py``: 149 | 150 | .. code-block:: python 151 | 152 | __all__ = ['VersionPlugin'] 153 | 154 | class VersionPlugin: 155 | 156 | @staticmethod 157 | def post_build(ctx, inputs, outputs, pack): 158 | script = os.path.join(outputs[0], 'parent', 'child', '__init__.py') 159 | with open(script, 'a') as f: 160 | f.write("\nVERSION = '0.0.1'") 161 | 162 | 然后启用这个插件:: 163 | 164 | $ pyarmor cfg plugins + "myplugin" 165 | 166 | 这样以后每次打包只需要运行下面的命令就可以了:: 167 | 168 | $ pyarmor gen --recursive --prefix parent.child src/parent 169 | $ python -m build --skip-dependency-check --no-isolation 170 | 171 | .. include:: ../_common_definitions.txt 172 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Pyarmor |version| 用户文档 3 | ============================ 4 | 5 | :版本: |release| 6 | :主页: |Website| 7 | :邮箱: |Contact| 8 | :作者: |Author| 9 | 10 | .. important:: 11 | 12 | Pyarmor 9.1.0 中新增功能的文档目前发布在新的 Pyarmor 学习系统 13 | 14 | https://eke.dashingsoft.com/pyarmor/docs/zh/index.html 15 | 16 | 包括三个新命令: `pyarmor init`, `pyarmor env`, `pyarmor build` 17 | 18 | 以及新的加密脚本类型: `rft`, `mini`, `vmc`, `ecc` 19 | 20 | 如何阅读本手册 21 | ============== 22 | 23 | |Pyarmor| 有完备的文档系统,这里是帮助用户如何快速找到需要的相关内容 24 | 25 | * :doc:`第一部分: 基础教程 ` 适合第一次使用 Pyarmor 的用户,这里以实例的形式一步接着一步的说明了加密脚本和包的最常用的场景。也可以先看看 :doc:`tutorial/getting-started` 26 | 27 | * :doc:`第二部分: 应用实践 ` 针对每一个特定的需求,说明在 Pyarmor 中应该如何去做,使用什么样的命令和选项去实现。阅读这部分内容需要对 Pyarmor 和 Python 都有一定的了解。 28 | 29 | * :doc:`第三部分: 技术手册 ` 从技术层的角度详细列出了所有的概念定义,命令手册,配置选项和错误信息代码。它适用于使用 Pyarmor 的高级用户,需要查找相关的参数和配置,了解这些配置项的可用值和不同值的作用和含义。 30 | 31 | * :doc:`第四部分: 深入了解 ` 这部分针对 Pyarmor 提供的功能,从如何实现的层面进行了详细的解释。阅读这部分内容需要完全掌握了 Pyarmor 使用到的主要概念,以及对 Python 脚本的执行过程有相当了解。它适用于需要对 Pyarmor 进行扩展和定制,以满足更高一层需求的用户。 32 | 33 | * :doc:`第五部分: 许可模式和许可证类型 ` 描述了 |Pyarmor| 的最终用户许可协议, |Pyarmor| 的许可模式,不同的许可类型,以及如何购买 |Pyarmor| 许可证。 34 | 35 | 获取帮助 36 | ======== 37 | 38 | 使用中遇到问题? 39 | 40 | 那么首先看看 :doc:`常见问题 ` – 这里详细说明了对于常见问题的通用解决方法,大部分的用户问题按照通用方法的步骤,都可以得到解决。后面的章节则分类列出了用户经常提问的一些问题和解决方案,使用过程中出现问题来这里通常是最快的解决方式。 41 | 42 | 想找某一个特定的关键字,搜一下 :ref:`总索引 `, 或者浏览 :ref:`文档总目录 ` 43 | 44 | 还是没有找到? 看一下 :ref:`如何在 Github 上提问 `. 45 | 46 | 发现 Pyarmor_ 的问题,请点击 `问题报告`_ 按照模版进行提交。 47 | 48 | 目录 49 | ==== 50 | 51 | .. toctree:: 52 | :numbered: 53 | :maxdepth: 3 54 | :name: mastertoc 55 | 56 | part-1 57 | part-2 58 | part-3 59 | part-4 60 | licenses 61 | questions 62 | 63 | 索引表 64 | ====== 65 | 66 | * :ref:`总索引 ` 67 | * :ref:`模块索引 ` 68 | * :ref:`搜索 ` 69 | 70 | .. include:: _common_definitions.txt 71 | -------------------------------------------------------------------------------- /docs/licenses.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 许可模式 3 | ========== 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | 本文档仅适用于 `Pyarmor`_ 8.0 之后的版本。 13 | 14 | Pyarmor 发布在 `PyPI`_ ,可以直接使用下面的命令进行安装试用:: 15 | 16 | $ pip install pyarmor 17 | $ pyarmor gen foo.py 18 | $ python dist/foo.py 19 | 20 | 试用版本永不过期,但是有功能方面的限制,这些限制可以通过不同的许可证进行解锁。Pyarmor 目前共有四种许可模式: 21 | 22 | - 基础版许可证: 主要解锁大脚本加密限制 23 | - 专家版许可证: 提供多种不可逆的加密模式 24 | - 集团版许可证: 满足离线加密的需要 25 | - 管线版许可证: 专门用于 CI/CD 集成构建环境 26 | 27 | .. list-table:: Table-1. 不同许可证的对比表 28 | :widths: 40 12 12 12 12 12 29 | :header-rows: 1 30 | :stub-columns: 1 31 | 32 | * - 功能特征 33 | - 试用版 34 | - 基础版 35 | - 专家版 36 | - 集团版 37 | - 管线版 38 | * - 大脚本 / 混淆字符串常量 [#]_ 39 | - 40 | - Y 41 | - Y 42 | - Y 43 | - Y 44 | * - BCC / RFT / FLY 模式 [#]_ 45 | - 46 | - 47 | - Y 48 | - Y 49 | - Y 50 | * - 离线加密 [#]_ 51 | - 52 | - 53 | - 54 | - Y 55 | - 56 | * - 最多构建设备数 [#]_ 57 | - 58 | - 100 59 | - 100 60 | - 200 61 | - 0 62 | * - 本地运行不受限制的 Docker [#]_ 63 | - 64 | - 65 | - 66 | - Y 67 | - 68 | * - 使用在 CI/CD pipeline [#]_ 69 | - Y 70 | - Y 71 | - 72 | - 73 | - Y 74 | 75 | .. rubric:: 备注 76 | 77 | .. [#] 78 | - 大脚本文件,是指加密脚本的大小超过一定值 79 | - 混淆字符串,是指对脚本中的字符串常量进行混淆保护的功能 80 | .. [#] 81 | - RFT 加密模式,是指通过重命名脚本中的函数,类,方法和变量的名称来保护脚本的功能 82 | - BCC 加密模式,是指把 Python 脚本中部分函数转换成为对应的 C 函数,通过编译直接生成机器指令代码,从而对脚本进行保护的功能 83 | .. [#] 离线加密,是指使用 Pyarmor 加密脚本的设备可以离线加密脚本,而不需要在线验证 Pyarmor 许可证 84 | .. [#] 最多可以安装 Pyarmor 的构建设备数目,管线许可证不可用于本地设备 85 | .. [#] 在本地设备上面运行不受限制 Docker 容器,本地设备可以离线或者在私有网络 86 | .. [#] 使用 Pyarmor 许可证在 CI/CD pipeline 需要额外的选项和命令 87 | 88 | .. important:: 89 | 90 | 如果 CI/CD 管线中 runner 有自己的硬盘系统,CI 许可证可能无法工作 91 | 92 | 一般情况下,如果 runner 不是 Docker 容器,而是物理设备,直接使用专家版许可证 93 | 94 | .. important:: 95 | 96 | 所有的 Pyarmor 许可证都是被安装在生成加密脚本的构建设备上,而不是运行加密脚本的设备上 97 | 98 | 运行加密脚本的设备不需要安装 Pyarmor,运行加密脚本的时候也不会去验证 Pyarmor 许可证 99 | 100 | 虽然加密脚本是被 Pyarmor 生成,但是生成的加密脚本完全独立于 Pyarmor 101 | 102 | 你可以像使用正常脚本一样使用加密脚本,它的行为由你而不是由 Pyarmor 来决定 103 | 104 | 如果产品使用的是 Python 2.7 和 Python 3.6 以及之前的任何版本,只能使用老版本的许可证,新版本许可证只支持 Python 3.7+ 105 | 106 | 使用条款 107 | ======== 108 | 109 | 1. 只能自己使用 Pyarmor 110 | 111 | 只能用来加密产权属于自己的脚本,不可以任何形式去加密别人的脚本,例如在自己的程序中调用 Pyarmor 的功能去加密你的客户的脚本,或者通过网站提供基于 Pyarmor 的加密服务等等 112 | 113 | 2. 无收益无需许可 114 | 115 | 如果你的脚本不能为你带来大的收入,那么不需要购买 Pyarmor 的许可证,就可以使用 Pyarmor 试用版来加密你的脚本。但是如果你通过销售加密脚本得到收益,那么就需要购买 Pyarmor 许可证 116 | 117 | 3. 一次性收费 118 | 119 | 除了**管线版许可证**是按照年度收费外,其他所有的 Pyarmor 许可证都是一次性收费,可以在购买 Pyarmor 时候对应的版本中永久使用, **但是不保证可以在未来的 Pyarmor 版本中可以使用** 120 | 121 | 4. 一个产品一个许可证 122 | 123 | 每一个许可证都有一个 18 位字符长度唯一的编号,并授权给有且只有一种产品使用。也就是说,任何一种使用本软件进行保护的产品都有自己唯一的许可证编号,不允许两种不同产品使用相同的许可证编号。 124 | 125 | Pyarmor 的许可策略是每一个产品都需要购买一个许可证。一个产品是指一种产品,而不是一个产品的拷贝。例如,微软的Excel是一个产品,但是却被装在无数的设备上 126 | 127 | 5. 允许合理使用 128 | 129 | 如果你为一个产品购买了许可证,但是你的另外一个产品还没有销售收入,那么在其销售收入还不大于 Pyarmor 许可证费用的 100 倍的时候,可以借用第一个产品的许可证 130 | 131 | :term:`Pyarmor 管线版` 许可证在 CI/CD 管线中注册有请求速率的限制,并且每月总注册次数也有限制 132 | 133 | 完整使用条款请参阅 `Pyarmor 最终用户许可协议`_ 134 | 135 | .. seealso:: :doc:`how-to/register` 136 | 137 | 数据隐私 138 | ======== 139 | 140 | 许可证编号和被授权的产品名称会嵌入到加密脚本中,除此之外,加密脚本中没有任何用户相关的注册信息,例如注册名称和邮箱等 141 | 142 | 使用基础版和专家版许可证进行加密的时候会收集设备信息并发送到服务器进行验证 143 | 144 | 在 CI/CD 环境中使用基础版和管线版许可证进行加密的时候,会收集容器相关信息以及许可证信息发送到服务器进行验证 145 | 146 | 除了许可证文件之外,不会上传任何用户脚本到服务器进行验证 147 | 148 | 技术支持 149 | ======== 150 | 151 | 许可费用仅为解锁功能,不包含技术支持 152 | 153 | Pyarmor 是一个命令行工具,所有的功能都通过选项实现,所有的选项都有 :doc:`完整的文档 ` 。用户需要自己学习和了解 Pyarmor 的各个选项以及使用方法,并根据自己的需求选择正确的选项,Pyarmor 开发组不会去了解用户的项目结构和加密需求,然后告诉用户需要使用什么选项 154 | 155 | Pyarmor 提供了完备的 :doc:`在线文档系统 ` 156 | 157 | 用户通过 :doc:`常见问题的解决方案 ` 以及 :doc:`FAQs ` ,可以解决大部分使用方法上的问题 158 | 159 | 帮助命令 `pyarmor man` 可以为每一个选项提供丰富的示例,并且为常用的使用场景提供示例 160 | 161 | Pyarmor 开发组正在开发新的教程,以动画的方式直观的帮助用户学习和使用 Pyarmor,并且快速解决使用过程中遇到的问题(大约在2024年10月能够完善) 162 | 163 | 罗马不是一天建成的,Pyarmor 开发组会根据用户反馈,持续改进学习系统,让用户可以快速有效的解决使用过程中遇到的问题 164 | 165 | 用户在Github 上的 Pyarmor 项目中报告缺陷和请求新功能,也可以通过命令 `pyarmor man` 快速提交。 166 | 167 | 邮件仅用于处理安全方面或者不适合公开的问题,其他技术问题一般不会回复 168 | 169 | Pyarmor 开发组对提交问题的响应时间一般为 24 小时之内,节假日或者特殊情况可能会延长 170 | 171 | Pyarmor 开发组不提供微信,电话等其他形式的技术支持 172 | 173 | 购买 174 | ==== 175 | 176 | 在浏览器中打开 Pyarmor 官网的购物车,支持微信和支付宝 177 | 178 | https://pyarmor.dashingsoft.com/cart/order.html 179 | 180 | 在购物车页面选择需要的许可证类型,填写注册名称,并完成支付。 181 | 182 | 支付成功之后,在一个工作日之内激活文件会发送到注册邮箱,请按照激活文件中的方法和步骤完成注册和激活,或者参考这里 :doc:`../how-to/register` 。 183 | 184 | 购买软件许可的费用是一次性收费,可以永久在购买本软件时候的版本中使用,但是许可证可能在任何一个升级版本中失效,Pyarmor 不承诺许可证可以在今后所有的升级版本中使用。 185 | 186 | .. list-table:: 表-2. 不同授权模式的价格列表(中国) 187 | :name: 授权模式价格表1 188 | :header-rows: 1 189 | 190 | * - 授权模式 191 | - 不含税价格(人民币元) 192 | - 含税价格(人民币元) 193 | - 说明 194 | * - 基础版 195 | - 298 196 | - 359 197 | - 198 | * - 专家版 199 | - 512 200 | - 562 201 | - 202 | * - 集团版 203 | - 868 204 | - 918 205 | - 206 | * - 管线版 207 | - 520 / 年 208 | - 569 / 年 209 | - 210 | 211 | 退款条款 212 | ======== 213 | 214 | 满足下列所有条件,可以进行退款 215 | 216 | - 购买日期在30天之内 217 | - 该许可激活文件还没有在命令 ``pyarmor reg`` 中使用 218 | 219 | 请直接发送退款请求到 pyarmor@163.com 220 | 221 | 附录 222 | ==== 223 | 224 | 关于一种产品的解释和说明 225 | ------------------------ 226 | 227 | 不用于销售的所有的 Python 脚本都属于一种特殊的产品 ``non-profits`` 228 | 229 | 一种产品在本协议中指的是独立销售的软件所有组成部分,包括开发需要的各种设备,以及提供支持的服务器,云服务器等。一种产品也包括产品的当前版本,历史版本,以及将来的升级版本。一种产品也包括基础功能相同,组合不同特殊功能而形成的不同版本的产品,这种产品的特征是不同版本对外销售名称一样,只是通过辅助名称等来进行区分。 230 | 231 | Pyarmor 是一种产品,它所包含 232 | 233 | - Pyarmor 基础版,专家版和集团版 都属于 Pyarmor 这一种产品 234 | - pyarmor-webui,为 Pyarmor 提供图形界面的工具,也属于 Pyarmor 这一种产品 235 | - Pyarmor 的后台订单系统是用 django 开发的一个程序,这也属于 Pyarmor 这一种产品 236 | - 开发 Pyarmor 所使用的笔记本电脑,测试 Pyarmor 使用的台式机,以及运行后台订单系统的云服务器都属于 Pyarmor 这一种产品 237 | - Pyarmor 7.x 和 Pyarmor 8.x 也都属于 Pyarmor 一种产品。 238 | 239 | Microsoft Office 产品系列不是一种产品,它包括的各个产品,例如 Microsoft Word 和 Microsoft Excel 是功能完全不同的两个产品,所以 Microsoft Office 不是一种产品。而 Microsoft Word 是一种产品,它的各个版本系列 Micorsoft Word 2003,Word 2007 等也都属于 Microsoft word 这一种产品。 240 | 241 | .. _select-license-type: 242 | 243 | 如何选择许可证类型 244 | ---------------------------------- 245 | 246 | 下面列出的许可证都只能用于 Python 3.7+ 247 | 248 | .. list-table:: 表-2. 选择不同许可证类型 249 | :widths: 40 12 12 12 12 12 250 | :header-rows: 1 251 | :stub-columns: 1 252 | 253 | * - 使用条件 254 | - 试用版 255 | - 基础版 256 | - 专家版 257 | - 集团版 258 | - 管线版 259 | * - 在 CI/CD 中每个月运行小于 100 次 260 | - Y 261 | - Y 262 | - Y 263 | - 264 | - Y 265 | * - 在 CI/CD 中每个月运行大于 100 次 266 | - Y 267 | - Y 268 | - 269 | - 270 | - Y 271 | * - 需要在离线设备加密脚本 272 | - Y 273 | - 274 | - 275 | - Y 276 | - 277 | * - 需要使用不可逆的加密模式 278 | - 279 | - 280 | - Y 281 | - Y 282 | - Y 283 | * - 在本地 Docker 容器中使用每月不超过 100 次 284 | - Y 285 | - Y 286 | - Y 287 | - Y 288 | - Y 289 | * - 在本地 Docker 容器中使用每月超过 100 次 290 | - Y 291 | - Y 292 | - 293 | - Y 294 | - Y 295 | 296 | .. _how-many-licenses-required: 297 | 298 | 需要购买几个许可证 299 | ------------------ 300 | 301 | 1. 列出所有单独销售的产品名称 302 | 303 | - 如果该产品的销售额低于 100 x Pyarmor 许可证费用,不需要列出该产品 304 | - 如果产品的销售额大于 100 x Pyarmor 费用的数目少于 2 个,那么只需要一个许可证即可 305 | 306 | 2. 判断多个不同产品是否同一个产品 307 | 308 | A 和 B 是两个单独销售的产品 309 | 310 | a. 第一种情况,满足下列条件,则可以认为 B 和 A 可以使用同一个许可证 311 | 312 | - B 完全包含 A 的功能 313 | - B 多于 A 的功能要和原来的 A 的功能相关 314 | 315 | 例如 Pyarmor Basic 版本 和 Pyarmor Pro 版本 可以使用同一个许可证,因为 316 | 317 | - Pyarmor Pro 包含 Pyarmor Basic 的所有功能 318 | - Pyarmor Pro 的额外功能是不可逆加密模式,这是对 Pyarmor Basic 加密功能的增强 319 | 320 | b. 第二种情况,下列情况需要为 A 和 B 单独购买许可证 321 | 322 | - B 完全包含 A 的功能 323 | - A 的功能在 B 的功能占比很少 324 | 325 | 例如,A 是一个面部识别产品,B 是使用面部识别功能的一个考勤管理系统 326 | 327 | c. 第三种情况,满足下列条件,则可以认为 B 和 A 可以使用同一个许可证 328 | 329 | - B 是针对 A 产品的功能补充 330 | 331 | 例如 A 是 CAD 文件编辑器,B 是 CAD 文件转换器,用于将 CAD 文件转换成为 PDF 格式 332 | 333 | d. 第四种情况,下列情况需要为 B 和 A 单独购买许可证 334 | 335 | - B 和 A 的功能完全独立 336 | 337 | 例如,Micorsoft Office Suite 包括 Microsoft Word (A) 和 Micorsoft Excel (B) 338 | 339 | 3. 关于共享后台产品的选择方式 340 | 341 | 产品 A 和 B 都是独立销售的产品,共用一个后台引擎 Engineer 342 | 343 | a. 需要加密共享后台 Engineer,产品 A 和 B 的前端代码都不需要加密 344 | 345 | - 共享后台 Engineer 购买一个许可证即可 346 | 347 | b. 需要加密共享后台,产品 A 和 B 的前端代码也需要加密 348 | 349 | - 前端产品 A 和 B 都需要一个许可证 350 | - 共享后台不需要购买许可证 351 | 352 | 4. 多个不同产品需要使用一个许可证 353 | 354 | 如果从产品角度来说的两个不同产品,在开发过程中需要使用同一个许可证 355 | 356 | - 购买多个许可证 357 | - 为每一个产品分别注册相应的许可证 358 | - 在加密过程中使用其中任意一个许可证 359 | 360 | .. _how-to-upgrade-license: 361 | 362 | 如何升级Pyarmor许可证 363 | --------------------- 364 | 365 | 目前并不支持补差价升级许可证,例如升级 Pyarmor 基础版到 Pyarmor 专家版许可证等 366 | 367 | 例外的情况主要有 368 | 369 | - 老版本许可证(应用于 Pyarmor <= 7.x)在满足一定条件下可以升级到 Pyarmor 基础版许可证 370 | - 如果从 Pyarmor 8.0 升级到 Pyarmor 9.0,许可证也需要进行相应的重新注册 371 | 372 | 具体操作步骤请参考 `升级 Pyarmor 之后的许可证兼容性 `_ 373 | 374 | 简单来说,如果能够根据文档说明升级成功,那么就是可以升级。如果升级过程中提示出现错误,那么就是无法升级。Pyarmor 开发组不处理升级许可证的请求,有可能不会回复这种类型的邮件。 375 | 376 | 问与答 377 | ------ 378 | 379 | **我的产品刚刚开始销售,是否可以使用 Pyarmor 试用版进行加密保护我的产品** 380 | 381 | 在销售额没有超过 Pyarmor 许可证费用的 100 倍之前,可以使用 Pyarmor 试用版进行加密保护。销售额超过之后,就需要购买 Pyarmor 许可证。 382 | 383 | **我关注到pyarmor对授权许可产品的定义是”用于销售的产品“,即一个用于销售的产品需要购买1个许可证,那么如果我想购买pyarmor用于加密一切应用服务器上存放的python程序,这里涉及到的服务器可能有上百个,但在这种情况下我并未将pyarmor用于任何产品销售的场景,这些服务器上的脚本代码可能单纯是用于支撑某种在线服务的,比如说一个报表系统或者一个XXX管理系统,那么这种情况下我应该购买怎样的授权** 384 | 385 | 一个产品不仅仅是前端,后端也同样是属于一个产品的组成部分的。有两种购买方式, 386 | 387 | 1. 前端都不需要加密,那么所有的后端可以作为一个产品购买一个许可,即使这个后端会为多个前端提供服务 388 | 2. 前端需要加密,那就和原来的方式一样了 389 | 390 | 另外,不是所有的后端都可以作为一个产品, 例如有两个代码和功能完全不同的游戏引擎 A 和 B,分别服务不同的游戏 391 | 392 | 那么引擎 A 和 引擎 B 虽然都是后端,但是要作为两个产品 393 | 394 | 而每一个游戏引擎,虽然服务多个游戏产品,但是每一个游戏引擎可以作为一个产品 395 | 396 | **现在假设我有4个脚本,A脚本是运维管理员用,主要用于对两个系统进行同步。B脚本用于在服务器中启动报表管理平台并获取报表管理平台的日志,C脚本用于启动财务管理平台并获取财务管理平台日志,D脚本用于动态从加密设备中调取密码供BC服务器使用,那么当我想对ABCD四个脚本都进行加密时,这会被认定为几个产品?** 397 | 398 | 简单的一个原则,如果你有单独销售的一个产品,产品销售额大于 100 倍的 Pyarmor 许可证费用,就需要购买一个许可。 399 | 400 | 这个产品用到的所有需要的后台代码,都属于这个产品。 401 | 402 | 结合 Pyarmor 的合理使用原则,对于产品销售额还没有达到 100 倍的许可证费用,可以借用其他产品的许可证。 403 | 404 | 这就是 Pyarmor 许可的原则,结合这个原则,来理解条款 405 | 406 | **可是我的这几个脚本并不是单独销售的产品,没有直接关联的销售额,比如报表管理平台是用来周期性生成报表报送给监管机构满足监管要求的,它并不向外出售,再比如财务管理平台用来管理企业的进销存等财务信息,也并不向外出售,若干运维脚本仅仅是运维人员自行编写的、用来方便工作的,也不向外出售,那么在这种情况下,我应该如何购买许可?** 407 | 408 | 没有收益的代码和脚本都归属于一个特殊产品 non-profits,可以借用其他产品的许可证 409 | 410 | .. include:: _common_definitions.txt 411 | -------------------------------------------------------------------------------- /docs/part-1.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 使用教程 3 | ========== 4 | 5 | .. toctree:: 6 | :maxdepth: 3 7 | 8 | tutorial/getting-started 9 | tutorial/installation 10 | tutorial/obfuscation 11 | tutorial/advanced 12 | tutorial/customization 13 | 14 | .. include:: _common_definitions.txt 15 | -------------------------------------------------------------------------------- /docs/part-2.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 应用实践 3 | ========== 4 | 5 | .. toctree:: 6 | :maxdepth: 3 7 | 8 | how-to/security 9 | how-to/protection 10 | how-to/packing 11 | how-to/wheel 12 | how-to/obfuscation 13 | how-to/register 14 | how-to/ci 15 | how-to/third-party 16 | 17 | .. include:: _common_definitions.txt 18 | -------------------------------------------------------------------------------- /docs/part-3.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | 技术手册 3 | ======== 4 | 5 | .. toctree:: 6 | :maxdepth: 3 7 | 8 | reference/concepts 9 | reference/man 10 | reference/environments 11 | reference/errors 12 | reference/solutions 13 | 14 | .. include:: _common_definitions.txt 15 | -------------------------------------------------------------------------------- /docs/part-4.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 深入了解 3 | ========== 4 | 5 | .. toctree:: 6 | :maxdepth: 3 7 | 8 | topic/obfuscation 9 | topic/obfuscated-script 10 | topic/repack 11 | topic/rftmode 12 | topic/bccmode 13 | topic/performance 14 | topic/localization 15 | 16 | .. include:: _common_definitions.txt 17 | -------------------------------------------------------------------------------- /docs/questions.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 常见问题 3 | ========== 4 | 5 | Pyarmor 是一个命令行工具,提供了丰富的选项来应对不同的情况。对于简单的应用,使用默认选项就可以工作,不需要学习如何使用 Pyarmor。但是对于一些高级的功能,或者比较复杂的应用,尤其是需要使用大型的第三方包来调用加密脚本,用户需要一定的时间去学习和使用各种选项,并组合这些选项去解决加密过程中问题。 6 | 7 | 当加密脚本出现问题的时候,这不一定是 Pyarmor 的 Bug,很可能是这个脚本使用默认的选项和配置无法工作。这种情况下需要分析问题并找到 Pyarmor 解决这个问题的选项和设置,这就需要用户参考 Pyarmor 的命令手册去找到合适的选项。 8 | 9 | **用户需要自己学习和了解 Pyarmor 的各个选项以及使用方法,并根据自己的需求选择正确的选项,Pyarmor 开发组不会去了解用户的项目结构和加密需求,然后告诉用户需要使用什么选项** 10 | 11 | Pyarmor 有完整的文档系统,所有的功能和选项都已经在文档中描述,Pyarmor 开发组能告诉你的,这些文档中都已经有说明。 12 | 13 | 如果使用的是命令 :command:`pyarmor-7` 或者 Pyarmor < 8.0,请参阅 `Pyarmor 7.x 在线文档`_ 14 | 15 | 通常一开始不需要阅读全部文档,但是如果还没有读过 :doc:`tutorial/getting-started` ,请务必把入门教程全部读一下,这里面有一些关于加密脚本必要的概念。完整阅读这篇文档应该不需要15分钟的时间,否则你可能会花几个小时的时间去解决因为错误使用加密脚本导致的问题。 16 | 17 | .. important:: 18 | 19 | 在 Pyarmor 开发组处理的报告中,有 80% 属于使用错误的问题,根据用户这些常见的错误问题,Pyarmor 开发组整理出了一份文档 :doc:`reference/solutions` 。请首先通过该文档的检查表或者 https://eke.dashingsoft.com/pyarmor/ask/ 确认不是使用错误,对于使用错误的问题,Pyarmor 开发组会标记为 `invalid` 或者 `documented` 并直接关闭该问题。 20 | 21 | .. _asking questions: 22 | 23 | 如何在 Github 上提问 24 | ==================== 25 | 26 | 请首先尝试下面的通用解决方案,这应该能够解决大部分的问题,并且避免了提交重复的问题: 27 | 28 | - 如果是了解是否有某项功能,首先浏览一下 :ref:`文档总目录 ` 29 | 30 | - 如果有错误信息提示,参阅 :doc:`常见错误信息 ` 里面的错误信息和解决方案 31 | 32 | - 如果是运行加密脚本的问题,请务必阅读 :doc:`topic/obfuscated-script` 33 | 34 | - 如果是使用 pack 方面的问题,请参阅 :doc:`topic/repack` 35 | 36 | - 如果是使用 :term:`RFT 模式` 的问题,请参阅 :ref:`using rftmode` 37 | 38 | - 如果是使用 :term:`BCC 模式` 的问题,请参阅 :ref:`using bccmode` 39 | 40 | - 如果是第三库无法正常调用加密脚本,请参阅 :doc:`how-to/third-party` 41 | 42 | - 如果是和安全性能相关的问题,请参阅 :doc:`topic/performance` 43 | 44 | - 浏览一下本页后面的常见问题 45 | 46 | - 运行没有加密的脚本,确保没有加密之前的脚本能够正确的运行 47 | 48 | - 如果加密的是复杂的应用,请首先测试一个简单的脚本,确认使用同样的加密选项的简单脚本能够工作 49 | 50 | - 启用调试模式,跟踪日志,在控制台输出更多信息,仔细检查日志信息,确认问题所在 51 | 52 | - 如果使用的不是最新版本的 Pyarmor,请升级到最新版本 53 | 54 | - 搜索一下 `问题报告`_ 55 | 56 | - 搜索一下 `讨论区`_ 57 | 58 | 如果你不想花费时间去阅读文档,请在 `讨论区`_ 提问,但是 Pyarmor 开发组并不保证一定回答这部分的所有问题。 59 | 60 | 如果你发现 Pyarmor 中存在 Bug,请提交到 `问题报告`_ 61 | 62 | .. _reporting issues: 63 | 64 | 报告问题 65 | ======== 66 | 67 | Pyarmor 开发组欢迎真正的 Bug 报告,并且会尽快解决这些 Bug。报告 Bug 请提供必要的信息以便 Pyarmor 开发组能够重现问题,这样有助于快速解决问题,无法被 Pyarmor 开发组重现的问题解决起来需要更长的周期。 68 | 69 | 一个好的报告应该至少包括 70 | 71 | - 清晰准确的标题 72 | - 重现 Bug 的必要步骤 73 | - 实际的结果 74 | - 期望的结果 75 | 76 | Pyarmor 的 Bug 一般分为下列类型: 77 | 78 | - 加密过程出现的问题 79 | - 运行加密脚本出现的问题 80 | - 打包加密脚本相关的问题 81 | 82 | 为了方便处理 Bug,请使用 `pyarmor man` 按照提示提交问题报告。 83 | 84 | .. important:: 85 | 86 | 如果 Bug 报告缺少必要的信息或者描述不清楚,可能会被标识为为 `invalid` 并且直接关闭。 87 | 88 | 加密过程的问题 89 | -------------- 90 | 91 | 如果有错误信息提示,请首先查看 :doc:`常见错误信息 ` 里面的错误信息和解决方案,请确认造成问题的原因不是使用方法的问题 92 | 93 | 报告问题请参考当前目录下面的文件 :file:`pyarmor.bug.log` ,这个文件在 Pyarmor 出错之后自动生成。例如:: 94 | 95 | [BUG]: no found input "fooxxx.py" 96 | 97 | ## Command Line 98 | pyarmor gen fooxxx.py 99 | 100 | ## Environments 101 | Home /Users/jondy/.pyarmor 102 | Platform darwin.x86_64 (darwin.x86_64) 103 | Python 3.12.0 104 | Pyarmor 9.0.4 (group), 006000, btarmor 105 | 106 | 请把这个文件的内容作为 Bug 报告的内容提交到 `问题报告`_ ,并根据实际情况对 Bug 的内容进行必要补充和说明 107 | 108 | 打包的相关问题 109 | -------------- 110 | 111 | 在报告打包问题之前,请分别进行下面的检查 112 | 113 | - 没有加密之前,使用相同的 PyInstaller 选项可以直接打包,打包后的可执行文件运行正确 114 | - 不要打包,只是使用相同的选项加密脚本,加密后的脚本能够正确运行 115 | 116 | 如果以上都检查通过,请按照下一节 `运行加密脚本的问题` 中的规范提交打包问题 117 | 118 | 运行加密脚本的问题 119 | ------------------ 120 | 121 | 如果运行加密脚本的过程遇到问题,请首先查看 :doc:`topic/obfuscated-script` 122 | 123 | 如果有错误信息,请务必查看 :doc:`常见错误信息 ` 里面的错误信息和解决方案,请确认造成问题的原因不是使用方法的问题 124 | 125 | 如果使用了 :option:`--enable-bcc` ,尝试不使用这个选项;如果仅当使用该选项的时候出现问题,那么请参阅 :ref:`using bccmode` 查找解决方案 126 | 127 | 如果使用了 :option:`--enable-rft` ,尝试不使用这个选项;如果仅当使用该选项的时候出现问题,那么请参阅 :ref:`using rftmode` 查找解决方案 128 | 129 | 尝试使用更少的选项加密脚本,然后在运行脚本,确定导致出现问题的最少选项 130 | 131 | 然后按照下面的规范进行报告:: 132 | 133 | 标题: [Bug] 错误描述 134 | 135 | ### 加密脚本的完整命令 136 | pyarmor gen ... 137 | 138 | ### 运行加密脚本以及异常信息和错误堆栈 139 | python dist/foo.py 140 | 141 | ### 期望的结果 142 | 以及其他需要补充的相关信息 143 | 144 | 热点问题 145 | ======== 146 | 147 | **看到网上有破解 Pyarmor 的工具?请问 Pyarmor 能够防止这些破解工具吗?** 148 | 149 | Pyarmor 开发组一般不会关注和了解这些破解工具,而是致力于研究 CPython 的源代码,以改进算法来提高安全性,所以没有办法直接回答这类问题。 150 | 151 | 可以确定的是,Pyarmor 提供了多种不可逆的加密模式,从原理上来说,是不可能把加密脚本恢复成为原来的脚本。 152 | 153 | 请参考 :doc:`how-to/security` ,使用你可用的最高安全选项去加密一个简单的参考脚本,例如 154 | 155 | .. code-block:: python 156 | 157 | import sys 158 | 159 | def fib(n): 160 | a, b = 0, 1 161 | while a < n: 162 | print(a, end=' ') 163 | a, b = b, a+b 164 | print() 165 | 166 | print('python version:', sys.version_info[:2]) 167 | print('this is fib(10)', fib(10)) 168 | 169 | 然后尝试使用网上所说的方法和工具进行破解。 170 | 171 | 如果能够被破解,请把Python 版本,Pyarmor 的版本,运行的平台,加密使用的选项,参考脚本以及破解的方法发送到 |Contact| 172 | 173 | 请不要在 Pyarmor 上发布任何破解工具的链接。 174 | 175 | Pyarmor 的核心功能是防止加密脚本被还原,对使用各种逆向工程方法的内存拷贝,直接修改内存绕过加密脚本的约束设置并没有特别的保护。关于如何对运行数据进行保护的解决方案,请参考 :doc:`how-to/protection` 176 | 177 | .. 178 | Apple 上的 Segment fault 179 | ======================== 180 | 181 | 首先升级 Pyarmor 到 8.3.0 之后的版本,这个版本解决了使用非系统 Python 导致的崩溃问题 182 | 183 | 如果已经是最新版本,那么检查预编译的扩展模块 ``pytransform3.so`` 和 ``pyarmor_runtime.so`` 184 | 185 | - 确保它们被正确签名, ``codesign -v /path/to/xxx.so`` 186 | - 检查所有的依赖库, ``otool -L /path/to/xxx.so`` ,确保所有的依赖库都存在 187 | 188 | 如果是 Pyarmor 8.3.0 之前的版本,主要有下面的几种原因 189 | 190 | 1. 通常情况下,这是因为不正确的 code signature 191 | 192 | 如果是加密或者注册的时候发生了崩溃,请尝试对扩展模块 ``pytransform3.so`` 重新签名:: 193 | 194 | $ codesign -s - -f /path/to/lib/pythonX.Y/site-packages/pyarmor/cli/core/pytransform3.so 195 | 196 | 如果是运行加密脚本的时候发生了崩溃,请尝试对扩展模块 ``pyarmor_runtime.so`` 重新签名:: 197 | 198 | $ codesign -s - -f dist/pyarmor_runtime_000000/pyarmor_runtime.so 199 | 200 | 请参阅 Apple 官方文档 `Using the latest code signature format`__ 201 | 202 | 2. 另外一种原因是 Python 的依赖库无法找到 203 | 204 | 使用非标准方式安装的 Python 也可能会导致崩溃,需要使用 otool 和 install_name_tool 解决依赖库问题 205 | 206 | 因为预编译的扩展模块需要一些依赖库,如果依赖库的位置不对,就可能直接崩溃。使用 ``otool -L`` 可以查看依赖库:: 207 | 208 | $ otool -L /path/to/lib/pythonX.Y/site-packages/pyarmor/cli/core/pytransform3.so 209 | 210 | /path/to/lib/pythonX.Y/site-packages/pyarmor/cli/core/pytransform3.so: 211 | pytransform3.so (compatibility version 0.0.0, current version 1.0.0) 212 | @rpath/lib/libpython3.9.dylib (compatibility version 3.9.0, current version 3.9.0) 213 | ... 214 | 215 | 除了系统库之外,主要就是 Python 动态库 ``@rpath/lib/libpython3.9.dylib`` ,其中默认配置的 ``rpath`` 为:: 216 | 217 | $ install_name_tool -id pytrnsform3.so \ 218 | -change $deplib @rpath/lib/libpython$ver.dylib \ 219 | -add_rpath @executable_path/.. \ 220 | -add_rpath @loader_path/.. \ 221 | -add_rpath /System/Library/Frameworks/Python.framework/Versions/$ver \ 222 | -add_rpath /Library/Frameworks/Python.framework/Versions/$ver \ 223 | build/$host/libs/cp$ver/$name.so 224 | 225 | 也可以使用下面的命令查看 ``rpath``:: 226 | 227 | $ otool -l /path/to/lib/pythonX.Y/site-packages/pyarmor/cli/core/pytransform3.so 228 | 229 | 检查当前系统是否存在 ``@rpath/lib/libpython3.9.dylib`` ,如果不存在这个文件的话,需要使用 ``install_name_tool`` 适配当前的 Python 安装环境,假设 Python 动态库是 ``/usr/local/Python.framework/Versions/3.9/Python``:: 230 | 231 | $ install_name_tool -change @rpath/lib/libpython3.9.dylib /usr/local/Python.framework/Versions/3.9/Python \ 232 | /path/to/lib/pythonX.Y/site-packages/pyarmor/cli/core/pytransform3.so 233 | 234 | 对于 ``dist/pyarmor_runtime_000000/pyarmor_runtime.so`` 也是同样的,必须保证依赖库都存在,否则需要进行适配运行环境。 235 | 236 | 如何找到当前 Python 解释器对应的动态库,请自行搜索答案。注意有些预编译的 Python 没有使用动态库,那么是无法运行加密脚本的,需要重新编译支持动态库的版本。 237 | 238 | 请参阅 Apple 官方文档 `Run-Path Dependent Libraries`__ 239 | 240 | 3. 如果系统安装了多个 Python,确保链接到正确的动态库 241 | 242 | 例如,除了系统 Python3.9 之外,还使用 anaconda3 安装了 Python 3.9 在 ``/Users/my_username/anaconda3/bin/python`` 243 | 244 | 当使用 ``/Users/my_username/anaconda3/bin/python`` 运行加密脚本的时候,首先会装载动态库 ``dist/pyarmor_runtime_000000/pyarmor_runtime.so`` ,这个动态库依赖 Python 系统库,按照配置会首先查找 ``/Users/my_username/anaconda3/bin/python/../lib/libpython3.9.dylib`` ,如果这个文件不存在,那么会继续查找 ``/Library/Frameworks/Python.framework/Versions/3.9/lib/libpython3.9.dylib`` ,如果这个文件存在,那么会使用系统的 Python 动态库文件,这样可能就会造成崩溃问题。 245 | 246 | 这种情况下就需要使用 install_name_tool 修改 ``dist/pyarmor_runtime_000000/pyarmor_runtime.so`` 适配当前的运行环境,使之能够装载 anaconda3 对应的 Python 动态库。Pyarmor 默认适配的是系统 Python ,并且不可能自动适配所有的运行环境,需要用户根据部署环境进行适配。 247 | 248 | 4. 权限设置问题 249 | 250 | Pyarmor 使用了 JIT 技术来提高安全性,在 Apple M1,这可能需要对 Python 进行某些设置。使用下面的命令检查 Python 的 entitlements 并进行必要的设置:: 251 | 252 | $ codesign -d --entitlements - $(which python) 253 | 254 | 请参阅 Apple 官方文档 `Allow Execution of JIT-compiled Code Entitlement`__ 255 | 256 | 5. 最后看一下 Apple 的 segment fault 日志,根据提示的错误信息搜索网络找解决方案 257 | 258 | __ https://developer.apple.com/documentation/xcode/using-the-latest-code-signature-format/ 259 | __ https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/RunpathDependentLibraries.html 260 | __ https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-jit 261 | 262 | 注册问题 263 | ======== 264 | 265 | **ERROR request license token failed (104)** 266 | 267 | 首先请确认网络可用,其次检查防火墙设置,如果可能的话,暂时关闭防火墙进行测试。 268 | 269 | 在 Windows 下面,防火墙要允许动态库 ``pytransform3.pyd`` 访问 `pyarmor.dashingsoft.com` 的端口 ``80`` ,在其他系统,防火墙要允许 ``pytransform3.so`` 访问 `pyarmor.dashingsoft.com` 的端口 ``80`` ,具体防火墙的规则设置请参阅防火墙的文档。 270 | 271 | **集团版许可证报错: ERROR request license token failed** 272 | 273 | 首先升级 Pyarmor 到 8.4.0+ 274 | 275 | 其次使用调试选项 ``-d`` 在离线机器上面运行注册命令,例如:: 276 | 277 | $ pyarmor -d reg pyarmor-device-regfile-6000.4.zip 278 | 279 | 检查控制台的输出日志,确保离线许可证的设备包含当前设备。例如:: 280 | 281 | DEBUG group license for machines: ['tokens/mb04eb35da4f5378185c8663522e0a5e3'] 282 | DEBUG got machine id: mb04eb35da4f5378185c8663522e0a5e3 283 | 284 | 如果设备不匹配,请使用 Pyarmor 8.4.0 以后的版本重新为当前设备生成离线注册文件:: 285 | 286 | $ pyarmor -v 287 | 288 | Pyarmor 8.4.0 289 | ... 290 | 291 | $ pyarmor reg -g 5 292 | 293 | 对于虚拟设备,请确保每次启动之后的设备 id 均保持一致。 294 | 295 | 使用许可相关问题 296 | ================ 297 | 298 | **专家版许可证联网验证是否需要上传用户脚本到服务器?** 299 | 300 | 不需要。基础版和专家版许可证需要收集设备相关硬件信息并发送到服务器进行验证,除了许可证文件之外,不会上传任何用户脚本到服务器。 301 | 302 | **我的软件每年大概能分发1W+,一个集团版许可证能绑定1万个离线设备吗? 这些设备到期后,许可证的数量能回收吗** 303 | 304 | 这里明确一些基本概念 305 | 306 | - 运行加密脚本的机器不需要安装 Pyarmor,也不需要 Pyarmor 许可证 307 | - Pyarmor 许可证主要应用于加密脚本的机器 308 | - 集团版许可证只供企业用户使用,最多允许 100 个离线设备使用该许可证 309 | - 关于 Pyarmor 许可证的详细说明和使用文档,请参阅 :doc:`licenses` 和 :doc:`how-to/register` 310 | 311 | **购买后能够多设备使用吗? 如果可以多设备使用,最大的设备数上限是多少?** 312 | 313 | 不同许可证有不同的设备使用限制。 314 | 315 | 对于基础版和专家版许可证,最多可以在 100 台不同设备上使用 Pyarmor ,每一个 Docker 容器也会被认为是一台设备。 316 | 317 | 对于集团版许可证,最多可以在 200 台离线设备使用,但是可以在离线设备上运行数量不受限制的 Docker 容器 318 | 319 | 基础版许可证通过使用额外的注册步骤也可以应用于 CI/CD 管线构建环境,但是专家版不能。 320 | 321 | 需要在 CI/CD 环境使用专家版功能的可以选择管线许可证,这个主要是应用于 CI/CD 管线构建环境 322 | 323 | **如果现在购买基础版后续是否可以补差价升级专家版或者集团版本?** 324 | 325 | 目前还不支持各种版本的补差价升级。 326 | 327 | **购买时为不含税,后续是否可以通过补齐同版本差价进行开发票?** 328 | 329 | 目前不支持补齐差价开发票。 330 | 331 | **我们采购流程需要双方签一个合同,请问是否支持呢?** 332 | 333 | Pyarmor 是一个工具产品,不额外签订其他合同, `Pyarmor 最终用户许可协议`_ 就是合同文件。 334 | 335 | **你们提到一个许可证授权给有且只有一个产品使用,表示的是我只能用来加密一个脚本吗?** 336 | 337 | 你可以把“一个产品”替换成为“一种产品”来进行理解,一种产品指的是独立销售的软件所有组成部分,包括开发需要的各种设备,以及提供支持的服务器,云服务器等。一种产品也包括产品的当前版本,历史版本,以及将来的升级版本。一种产品也包括基础功能相同,组合不同特殊功能而形成的不同版本的产品,这种产品的特征是不同版本对外销售名称一样,只是通过辅助名称等来进行区分。 338 | 339 | 详细说明和例子请参考 :doc:`licenses` 340 | 341 | **是否可以对大文件提供试用许可证** 342 | 343 | 不提供。 344 | 345 | Pyarmor 是一个小工具,大部分的功能在试用版中均可以得到验证。对于一些无法试用的高级功能,主要是 RFT 模式和 BCC 模式,这些模式都可以通过配置忽略把不支持的部分脚本或者函数进行忽略。也就是说,只要通过额外的配置,它们总是可以工作的。 346 | 347 | **我们有自己的 CICD,需要在镜像内对代码进行加密,那么这是否意味着我们在每次打镜像时都需要通过 “pyarmor register pyarmor-regcode-xxxx.txt” 来对pyarmor进行注册?我们的 CICD 会触发的比较频繁,那么如果比较频繁的进行 register 操作的话是否会影响pyarmor的使用?** 348 | 349 | 关于在 CI/CD 中使用 Pyarmor 的许可证, Pyarmor 9 中有专门的解决方案,具体使用方法请参考 :doc:`how-to/ci` 350 | 351 | **我们之前是使用的Pyarmor 6.8.1版本进行的前期测试和验证,现在想要正式购买授权,但目前最新版本已经是8.0+,那这个之前的6.8.1版本的授权该如何购买** 352 | 353 | 如果确定是使用 Pyarmor 8 之前的版本,在 `购买页面`__ 可以选择购买老版本许可证。 354 | 355 | 直接支持老版本的许可证的最终版本是 Pyarmor 7.7.4 356 | 357 | 如果要在 Pyarmor 8.0+ 中使用老版本的许可证,需要使用命令 pyarmor-7 358 | 359 | __ https://pyarmor.dashingsoft.com/cart/order.html 360 | 361 | **Pyarmor 的许可证是否限制加密次数** 362 | 363 | 对于太过频繁的使用,Pyarmor Team 可能会发送邮件要求用户给出合理的解释。如果是非正常的使用,违反了 Pyarmor 最终用户许可协议,会按照协议进行处理 364 | 365 | **我关注到pyarmor对授权许可产品的定义是”用于销售的产品“,即一个用于销售的产品需要购买1个许可证,那么如果我想购买pyarmor用于加密一切应用服务器上存放的python程序,这里涉及到的服务器可能有上百个,但在这种情况下我并未将pyarmor用于任何产品销售的场景,这些服务器上的脚本代码可能单纯是用于支撑某种在线服务的,比如说一个报表系统或者一个XXX管理系统,那么这种情况下我应该购买怎样的授权** 366 | 367 | 一个产品不仅仅是前端,后端也同样是属于一个产品的组成部分的。有两种购买方式, 368 | 369 | 1. 前端都不需要加密,那么所有的后端可以作为一个产品购买一个许可,即使这个后端会为多个前端提供服务 370 | 2. 前端需要加密,那就和原来的方式一样了 371 | 372 | 另外,不是所有的后端都可以作为一个产品, 例如有两个代码和功能完全不同的游戏引擎 A 和 B,分别服务不同的游戏 373 | 374 | 那么引擎 A 和 引擎 B 虽然都是后端,但是要作为两个产品 375 | 376 | 而每一个游戏引擎,虽然服务多个游戏产品,但是每一个游戏引擎可以作为一个产品 377 | 378 | **现在假设我有4个脚本,A脚本是运维管理员用,主要用于对两个系统进行同步。B脚本用于在服务器中启动报表管理平台并获取报表管理平台的日志,C脚本用于启动财务管理平台并获取财务管理平台日志,D脚本用于动态从加密设备中调取密码供BC服务器使用,那么当我想对ABCD四个脚本都进行加密时,这会被认定为几个产品?** 379 | 380 | 简单的一个原则,如果你有单独销售的一个产品,产品销售额大于 100 倍的 Pyarmor 许可证费用,就需要购买一个许可。 381 | 382 | 这个产品用到的所有需要的后台代码,都属于这个产品。 383 | 384 | 结合 Pyarmor 的合理使用原则,对于产品销售额还没有达到 100 倍的许可证费用,可以借用其他产品的许可证。 385 | 386 | 这就是 Pyarmor 许可的原则,结合这个原则,来理解条款 387 | 388 | **可是我的这几个脚本并不是单独销售的产品,没有直接关联的销售额,比如报表管理平台是用来周期性生成报表报送给监管机构满足监管要求的,它并不向外出售,再比如财务管理平台用来管理企业的进销存等财务信息,也并不向外出售,若干运维脚本仅仅是运维人员自行编写的、用来方便工作的,也不向外出售,那么在这种情况下,我应该如何购买许可?** 389 | 390 | 没有收益的代码和脚本都归属于一个特殊产品 non-profits,可以借用其他产品的许可证 391 | 392 | .. include:: _common_definitions.txt 393 | -------------------------------------------------------------------------------- /docs/reference/concepts.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 概念定义 3 | ========== 4 | 5 | .. glossary:: 6 | 7 | BCC 模式 8 | 9 | 一种加密的方法,可以把脚本中的函数转换成为 :term:`C` 函数,然后使用优化选项编译生成机器指令。使用这种方式加密的函数是不可逆的,无法恢复成为原来的 Python 函数。 10 | 11 | 本地配置 12 | 13 | 一般在当前目录下会创建一个目录 ``.pyarmor`` ,用来存放本地配置,默认本地配置的文件是 ``.pyarmor/config`` 14 | 15 | 相同选项的本地配置会覆盖 :term:`全局配置` 16 | 17 | 参考 :ref:`pyarmor cfg` 18 | 19 | C 20 | 21 | 一种最古老的编程语言,现在依然生命力旺盛。 22 | 23 | 根目录 24 | 25 | 用来存放 Pyarmor 注册信息,全局配置文件等的路径,默认情况下是当前登陆用户的根目录下面的子目录 :file:`~/.pyarmor` 26 | 27 | 使用 sudo 命令可能会改变 Pyarmor 的根目录 28 | 29 | 激活文件 30 | 31 | 一个文本文件,购买任何 :term:`Pyarmor 许可证` 之后,都会有一个相应的激活文件发送到注册邮箱。 32 | 33 | 激活文件主要用于第一次注册 :term:`Pyarmor 许可证` ,激活文件一旦完成初始登记,就无法在继续使用。 34 | 35 | 初始登记成功会同时生成相应的 :term:`注册文件` ,后面在任何设备上进行注册都需要使用 :term:`注册文件` 。 36 | 37 | 加密插件 38 | 39 | 一个 :term:`Python` 脚本, 在加密过程被调用,可以对输出的文件进行一些额外的操作。 40 | 41 | 参考 :ref:`plugins` 42 | 43 | 脚本补丁 44 | 45 | 一个 Python 脚本,可以被嵌入到加密脚本中,执行一些额外的检查。 46 | 47 | 参考 :ref:`hooks` 48 | 49 | JIT 50 | 51 | JUST-IN-TIME 的缩写,是一种在运行时刻生成机器指令的技术,可以有效防止静态反编译工具对代码进行分析 52 | 53 | 开发机器 54 | 55 | 是指安装和运行 Pyarmor 的设备,在开发机器上对脚本进行加密 56 | 57 | 不是所有的平台都可以运行 Pyarmor,所有支持的运行环境请参考 :doc:`environments` 58 | 59 | 客户设备 60 | 61 | 是指运行加密脚本的设备 62 | 63 | 客户设备上面不需要安装 Pyarmor 64 | 65 | 扩展模块 66 | 67 | 一个使用 :term:`C` 或者 C++ 语言编写的 :term:`Python 模块` 68 | 69 | 模块私有配置 70 | 71 | 每一个加密模块可以有自己的私有配置,一般存放在 :term:`本地配置` 的目录下面,和模块同名的 ``module.ruler`` 文件 72 | 73 | 相同选项的私有配置会覆盖 :term:`本地配置` 74 | 75 | Pyarmor 76 | 77 | Pyarmor 是一个用来加密 Python 脚本的工具。 78 | 79 | Pyarmor 的组成部分 80 | 81 | - :term:`Pyarmor 项目` 82 | - :term:`pyarmor 包` 83 | 84 | Pyarmor 包 85 | 86 | 一个 :term:`Python 包` ,它包含下列包 87 | 88 | * :mod:`pyarmor` 89 | * :mod:`pyarmor.cli` 90 | * :mod:`pyarmor.cli.core` 91 | * :mod:`pyarmor.cli.runtime` 92 | 93 | 在 Pyarmor 8.3 之后, :mod:`pyarmor.cli.runtime` 因为太大而被分为下列包: 94 | 95 | - :mod:`pyarmor.cli.core.freebsd` 96 | - :mod:`pyarmor.cli.core.android` 97 | - :mod:`pyarmor.cli.core.windows` 98 | - :mod:`pyarmor.cli.core.themida` 99 | - :mod:`pyarmor.cli.core.linux` 100 | - :mod:`pyarmor.cli.core.alpine` 101 | - :mod:`pyarmor.cli.core.darwin` 102 | 103 | Pyarmor 基础版 104 | 105 | 一种 :term:`Pyarmor 许可证` 类型 106 | 107 | Pyarmor 集团版 108 | 109 | 一种 :term:`Pyarmor 许可证` 类型 110 | 111 | Pyarmor 用户 112 | 113 | 使用 :term:`Pyarmor` 的组织机构或者开发人员 114 | 115 | Pyarmor 项目 116 | 117 | 项目文件存放在 |Home| 118 | 119 | 这里有 Pyarmor 开源部分的代码, 提交的 `问题报告`_ 和最新的文档。 120 | 121 | Pyarmor 许可证 122 | 123 | 由 Pyarmor 开发团队颁发,用于解锁 Pyarmor 试用版本中功能限制 124 | 125 | 请参考 :doc:`Pyarmor 许可模式和许可证 <../licenses>` 126 | 127 | Pyarmor 专家版 128 | 129 | 一种 :term:`Pyarmor 许可证` 类型 130 | 131 | Pyarmor 管线版 132 | 133 | 一种 :term:`Pyarmor 许可证` 类型,主要用于在 CI/CD 管线中使用 :term:`Pyarmor 专家版` 的功能 134 | 135 | Python 136 | 137 | 一种编程语言,官网地址 Python_ 138 | 139 | Python 脚本 140 | 141 | 一个包含 Python 源代码的文件 142 | 143 | 外部概念 https://docs.python.org/3.11/glossary.html#term-module 144 | 145 | Python 模块 146 | 147 | 要么是一个 :term:`Python 脚本` ,要么是一个 :term:`扩展模块` 148 | 149 | Python 包 150 | 151 | 外部概念 https://docs.python.org/3.11/glossary.html#term-package 152 | 153 | 全局配置 154 | 155 | 存放 Pyarmor 运行配置的全局文件,全局配置文件必须存放在 Pyarmor 的 :term:`根目录` ,默认的文件名称是 ``~/.pyarmor/config/global`` 156 | 157 | 参考 :ref:`pyarmor cfg` 158 | 159 | RFT 模式 160 | 161 | 一种不可逆的加密模式,可以重命名 Python 脚本中的函数,类,方法,变量和参数 162 | 163 | 外部密钥 164 | 165 | 一个用于存储 :term:`运行密钥` 的文件,通常的名称为 ``pyarmor.rkey`` 166 | 167 | 外部密钥可以存放在下面的任何一个路径: 168 | 169 | - :term:`运行辅助包` 所在的路径 170 | - 环境变量 :envvar:`PYARMOR_RKEY` 指定的路径,路径中不能使用 ``..`` ,尾部不能是路径分隔符,一般用来指定一个绝对路径,例如 ``/var/data`` 171 | - 当前路径 172 | 173 | 或者是文件:当前可执行文件的全路径名称 + ``.pyarmor.rkey`` 174 | 175 | 运行辅助包 176 | 177 | 一个 :term:`Python 包` ,名称一般为 ``pyarmor_runtime_xxxxxx`` 178 | 179 | 一般生成加密脚本的同时,也会生成相应的运行辅助包,运行加密脚本需要有相应的运行辅助包。 180 | 181 | 运行辅助文件 182 | 183 | 是指运行加密脚本需要的所有其他文件 184 | 185 | 通常情况下它等价于 :term:`运行辅助包` ,如果使用了 :term:`外部密钥` ,那么也包含外部密钥 186 | 187 | 运行密钥 188 | 189 | 保存加密脚本的运行设置和相关约束限制,包括脚本的有效期,脚本绑定的设备信息,也包括控制加密脚本行为的其他标志和设置。 190 | 191 | 通常情况下运行密钥被嵌入到 :term:`运行辅助包` 里面,但是也可以是一个独立文件形式的 :term:`外部密钥` 192 | 193 | 运行平台 194 | 195 | Pyarmor 定义的标准名称,用来标示运行 Pyarmor 和加密脚本的操作系统和 CPU 架构 196 | 197 | 下面列出了所有定义的运行平台: 198 | 199 | * Windows 200 | - windows.x86_64 201 | - windows.x86 202 | * Many Linuxs 203 | - linux.x86_64 204 | - linux.x86 205 | - linux.aarch64 206 | - linux.armv7 207 | * Apple Intel and Silicon 208 | - darwin.x86_64 209 | - darwin.aarch64 or darwin.arm64 210 | * FreeBSD 211 | - freebsd.x86_64 212 | * Alpine Linux (musl-c) 213 | - alpine.x86_64 214 | - alpine.aarch64 215 | * Android 216 | - android.x86_64 217 | - android.x86 218 | - android.aarch64 219 | - android.armv7 220 | 221 | 注册文件 222 | 223 | 一个 ``.zip`` 格式的压缩文件,主要用于除了初始登记之后的所有注册。 224 | 225 | 第一次初始登记使用 :term:`激活文件` ,激活成功之后生成注册文件,之后在任何设备上都要使用注册文件进行注册。 226 | 227 | 228 | .. module:: pyarmor 229 | :synopsis: 一个用来加密 Python 脚本的命令行工具,提供 Pyarmor 7 和 Pyarmor 8 命令行接口。如果只需要使用 Pyarmor 8,可以直接安装 :mod:`pyarmor.cli` 230 | 231 | .. module:: pyarmor.cli 232 | :synopsis: 一个用来加密 Python 脚本的命令行工具,仅提供 Pyarmor 8 命令行接口 233 | 234 | .. module:: pyarmor.cli.core 235 | :synopsis: 一个平台相关的二进制 Wheel 包,提供运行 Pyarmor 需要的预编译扩展模块 `pyansform3` 236 | 237 | .. module:: pyarmor.cli.runtime 238 | :synopsis: 支持 Pyarmor 跨平台加密的包,提供所有平台运行加密脚本需要的预编译动态库 `pyarmor_runtime` 239 | 240 | .. module:: pyarmor.cli.core.android 241 | :synopsis: 在 Android 系统使用 Pyarmor 的依赖包,提供运行 Pyarmor 和加密脚本需要的预编译动态库 242 | 243 | .. module:: pyarmor.cli.core.freebsd 244 | :synopsis: 在 FreeBSD 系统使用 Pyarmor 的依赖包,提供运行 Pyarmor 和加密脚本需要的预编译动态库 245 | 246 | .. module:: pyarmor.cli.core.windows 247 | :synopsis: 支持跨平台加密的包,仅提供所有 Windows 平台运行加密脚本需要的预编译动态库 248 | 249 | .. module:: pyarmor.cli.core.themida 250 | :synopsis: 支持跨平台加密的包,仅提供 Windows 平台下使用 Themida 保护的运行加密脚本需要的预编译动态库 251 | 252 | .. module:: pyarmor.cli.core.linux 253 | :synopsis: 支持跨平台加密的包,仅提供所有 Linux + glibc 平台运行加密脚本需要的预编译动态库 254 | 255 | .. module:: pyarmor.cli.core.alpine 256 | :synopsis: 支持跨平台加密的包,仅提供所有 Alpine Linux (musl-c) 平台运行加密脚本需要的预编译动态库 257 | 258 | .. module:: pyarmor.cli.core.darwin 259 | :synopsis: 支持跨平台加密的包,仅提供所有 Darwin 平台运行加密脚本需要的预编译动态库 260 | 261 | .. include:: ../_common_definitions.txt 262 | -------------------------------------------------------------------------------- /docs/reference/environments.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: none 2 | 3 | ==================== 4 | 生成加密脚本的环境 5 | ==================== 6 | 7 | 这里列出了和命令 :command:`pyarmor` 相关的所有一切。 8 | 9 | 首先命令 :command:`pyarmor` 运行在 :term:`开发机器` 上面,使用 `supported Python versions`_ 运行在这些 `supported platforms`_ 10 | 11 | 命令行选项, `配置文件选项`_ , `加密插件`_ , `脚本补丁`_ 和一些环境变量影响和改变着命令 :command:`pyarmor` 的行为。 12 | 13 | 所有的命令行选项和相关的环境变量在 :doc:`man` 中有详细描述。 14 | 15 | .. _supported python versions: 16 | 17 | 支持的 Python 版本 18 | ================== 19 | 20 | .. table:: 表-1. 不同功能支持的 Python 版本表 21 | :widths: auto 22 | 23 | =================== ===== ========= ========== ====== ====== ======= ============== 24 | Python 2.7 3.0~3.6 3.7~3.10 3.11 3.12 3.13 备注 25 | =================== ===== ========= ========== ====== ====== ======= ============== 26 | RFT 模式 No No Y Y Y Y [#]_ 27 | BCC 模式 No No Y Y Y Y 28 | pyarmor 8 基础功能 No No Y Y Y Y 29 | pyarmor-7 Y Y Y No No No 30 | =================== ===== ========= ========== ====== ====== ======= ============== 31 | 32 | .. _supported platforms: 33 | 34 | 支持的平台 35 | ========== 36 | 37 | .. table:: 表-2. 不同功能支持的平台列表(一) 38 | :widths: auto 39 | 40 | =================== ============ ======== ======= ============ ========= ======= ======= 41 | OS Windows Apple [#]_ Linux [#]_ 42 | ------------------- ------------ ----------------- ----------------------------------------- 43 | Arch x86/x86_64 x86_64 arm64 x86/x86_64 aarch64 armv7 armv6 44 | =================== ============ ======== ======= ============ ========= ======= ======= 45 | Themida 保护 Y No No No No No No 46 | RFT 模式 Y Y Y Y Y Y No 47 | BCC 模式 Y Y Y Y Y N/y No 48 | pyarmor 8 基础功能 Y Y Y Y Y Y No 49 | pyarmor-7 [#]_ Y Y Y Y Y Y Y 50 | =================== ============ ======== ======= ============ ========= ======= ======= 51 | 52 | .. table:: 表-3. 不同功能支持的平台列表(二) [#]_ 53 | :widths: auto 54 | 55 | =================== ============ ========= ========= ============ ========= ======= ======= 56 | OS FreeBSD Alpine Linux Android 57 | ------------------- ------------ -------------------- ----------------------------------------- 58 | Arch x86_64 x86_64 aarch64 x86/x86_64 aarch64 armv7 armv6 59 | =================== ============ ========= ========= ============ ========= ======= ======= 60 | RFT 模式 Y Y Y Y Y Y No 61 | BCC 模式 Y Y Y Y Y Y No 62 | pyarmor 8 基础功能 Y Y Y Y Y Y No 63 | pyarmor-7 Y Y Y Y Y Y Y 64 | =================== ============ ========= ========= ============ ========= ======= ======= 65 | 66 | .. table:: 表-4. 不同功能支持的平台列表(三) [#]_ 67 | :widths: auto 68 | 69 | =================== ============= ================================================== 70 | OS Linux Linux, Alpine Linux 71 | ------------------- ------------- -------------------------------------------------- 72 | Arch loongarch64 ppc64le, mips32el/64el, riscv64 [#]_ 73 | =================== ============= ================================================== 74 | RFT 模式 Y Y 75 | BCC 模式 N N 76 | JIT 功能 N Y 77 | pyarmor 8 基础功能 Y Y 78 | =================== ============= ================================================== 79 | 80 | .. rubric:: 注释 81 | 82 | .. [#] ``N/y`` 意味着现在还不支持,但是将来会支持 83 | .. [#] Apple Silcon 仅支持 Python 3.9+ 84 | .. [#] 这里的 Linux 使用的是 glibc 85 | .. [#] pyarmor-7 支持更多的平台,参考 `Pyarmor 7.x 文档`__. 86 | .. [#] 下表中平台是在 Pyarmor 8.3 新增加的 87 | .. [#] 这些框架在 Pyarmor 8.5.9 中新增加的并且未经测试,预编译的扩展模块分别存放在包 `pyarmor.cli.core.linux`__ and `pyarmor.cli.core.alpine`__ 88 | .. [#] 框架 riscv64 仅支持 Python 3.10+ 89 | 90 | .. important:: 91 | 92 | pyarmor-7 是 Pyarmor 7.x 的问题修正版本,只支持老版本的许可证。在新版本的许可证下面使用可能会报错 ``HTTP 401 error`` 93 | 94 | __ https://pyarmor.readthedocs.io/zh/v7.x/platforms.html 95 | __ https://pypi.org/project/pyarmor.cli.core.linux/#files 96 | __ https://pypi.org/project/pyarmor.cli.core.alpine/#files 97 | 98 | 配置文件选项 99 | ============ 100 | 101 | 有三种类型的配置文件 102 | 103 | * :term:`全局配置` 文件,一个 ``.ini`` 格式的文件 :file:`~/.pyarmor/config/global` 104 | * :term:`本地配置` 文件,一个 ``.ini`` 格式的文件 :file:`.pyarmor/config` 105 | * :term:`模块私有配置` 每一个模块可以有自己的私有配置,存放在 :term:`本地配置` 的目录下面 106 | 107 | 使用命令 :ref:`pyarmor cfg` 来查看和设置配置文件选项。 108 | 109 | .. _plugins: 110 | 111 | 加密插件 112 | ======== 113 | 114 | .. versionadded:: 8.2 115 | 116 | .. program:: pyarmor gen 117 | 118 | 加密插件是一个 Python 脚本,在生成输出文件之后被调用,可以对输出文件进行一些修改和调整,也可以做任何需要的事情。 119 | 120 | 加密插件常用的场景: 121 | 122 | - 对输出目录的文件进行额外处理 123 | - 修改加密脚本中导入运行辅助包的语句,以满足特殊的导入需求 124 | - 增加注释信息到 :term:`外部密钥` 文件,这样通过注释使用外部脚本就可以读取运行配置信息 125 | - 修改运行辅助包中扩展模块 :mod:`pyarmor_runtime` 的后缀,以避免名称冲突 126 | - 使用 `install_name_tool` 修改扩展模块 :mod:`pyarmor_runtime` 的依赖包,解决有些 Darwin 环境无法装载扩展模块的问题 127 | 128 | 一个加密插件的脚本可以定义一个或者多个插件类,同时必须定义属性 ``__all__`` 来输出脚本中定义的插件类,没有输出的插件类不起作用 129 | 130 | 插件类的定义如下 131 | 132 | .. py:class:: PluginName 133 | 134 | .. py:staticmethod:: post_script(ctx, res, source) 135 | 136 | 如果该方法存在,那么它会在加密脚本生成之后被调用 137 | 138 | 这个方法主要用来对生成的加密脚本进行修改定制,通过修改 `source` 并返回修改后的脚本内容 139 | 140 | :param Context ctx: 加密环境 141 | :param FileResource res: 实例 `pyarmor.cli.resource.FileResource` 142 | :param str source: 加密后的脚本内容 143 | 144 | .. py:staticmethod:: post_build(ctx, inputs, outputs, pack=None) 145 | 146 | 如果该方法存在,那么当所有加密文件都生成之后会被 :ref:`pyarmor gen` 调用 147 | 148 | :param Context ctx: 加密环境 149 | :param list inputs: 所有命令行输入的脚本和包名称 150 | :param list outputs: 输出目录 151 | :param str pack: 要么是 None,要么是选项 :option:`--pack` 指定的文件名称 152 | 153 | .. py:staticmethod:: post_key(ctx, keyfile,**keyinfo) 154 | 155 | 如果该方法存在,那么当 :term:`外部密钥` 文件生成之后会被 :ref:`pyarmor gen key` 调用 156 | 157 | :param Context ctx: 加密环境 158 | :param str keyfile: 输出的外部密钥文件 159 | :param dict keyinfo: 外部密钥绑定的信息 160 | 161 | 参数 ``keyinfo`` 中可能的项目有 162 | 163 | :key expired: None 或者有效期(epoch) 164 | :key devices: None 或者设备信息列表 165 | :key data: None 或者是绑定的私有数据(Bytes) 166 | :key period: None 或者是定时检查运行密钥的周期(秒) 167 | 168 | .. py:staticmethod:: post_runtime(ctx, source, dest, platform) 169 | 170 | 如果该方法存在,那么当每一个运行平台的扩展模块 ``pyarmor_runtime`` 生成之后会被 :ref:`pyarmor gen` 调用,如果是生成多平台的脚本,它可能被调用多次。 171 | 172 | :param Context ctx: 加密环境 173 | :param str source: 预编译的扩展模块文件名称 174 | :param str dest: 输出的扩展模块文件名称 175 | :param str platform: 扩展模块对应的 :term:`运行平台` 名称 176 | 177 | 为了启用插件脚本,需要进行配置。配置的时候不需要输入扩展名 ``.py`` ,直接使用脚本名称。例如:: 178 | 179 | $ pyarmor cfg plugins + "script name" 180 | 181 | Pyarmor 按照顺序依次搜索同名的脚本: 182 | 183 | - 当前路径 184 | - :term:`本地配置` 目录,通常是 ``.pyarmor/`` 185 | - :term:`全局配置` 目录,通常就是 ``~/.pyarmor/`` 186 | 187 | 这里有一个示例插件脚本 ``fooplugin.py`` 188 | 189 | .. code-block:: python 190 | 191 | __all__ = ['EchoPlugin'] 192 | 193 | class EchoPlugin: 194 | 195 | @staticmethod 196 | def post_runtime(ctx, source, dest, platform): 197 | print('-------- test fooplugin ----------') 198 | print('ctx is', ctx) 199 | print('source is', source) 200 | print('dest is', dest) 201 | print('platform is', platform) 202 | 203 | 把它保存到 ``.pyarmor/fooplugin.py`` 并启用它:: 204 | 205 | $ pyarmor cfg plugins + "fooplugin" 206 | 207 | 加密一个脚本,在控制台可以看到插件运行的输出信息:: 208 | 209 | $ pyarmor gen foo.py 210 | 211 | 禁用插件使用下面的方式:: 212 | 213 | $ pyarmor cfg plugins - "fooplugin" 214 | 215 | .. _hooks: 216 | 217 | 脚本补丁 218 | ======== 219 | 220 | .. versionadded:: 8.2 221 | 222 | 脚本补丁也是一个 Python 脚本,在加密的时候被嵌入到脚本的最前面,相当于直接在脚本的前面插入了插件源代码,这样加密脚本运行的时候就会首先执行插件代码。 223 | 224 | 当加密脚本的时候,Pyarmor 会在 :term:`本地配置` 和 :term:`全局配置` 目录下面查看有没有路径 ``hooks`` ,如果有的话,那么会查看有没有和加密脚本同名的文件,有的话就会被插入到加密脚本中。 225 | 226 | 例如, ``.pyarmor/hooks/foo.py`` 就是 ``foo.py`` 的补丁,而 ``.pyarmor/hooks/joker.card.py`` 是 ``joker/card.py`` 的补丁。 227 | 228 | 脚本补丁就是一个普通的 Python 脚本,但是它可以用两个特殊的内置函数 :func:`__pyarmor__` and :func:`__assert_armored__` 来做一些加密脚本特有的事情。 229 | 230 | 需要注意的是补丁是直接插入到脚本的模块级别的代码中,所以要避免名称冲突,影响了原来脚本的执行。 231 | 232 | .. seealso:: :func:`__pyarmor__` :func:`__assert_armorred__` 233 | 234 | 特殊脚本补丁 235 | ------------ 236 | 237 | .. versionadded:: 8.3 238 | 239 | 一般的脚本补丁是嵌入到了加密脚本中,如果需要在运行加密脚本之前就进行一些定制或者额外的检查,那么就需要使用到特殊的脚本补丁 ``.pyarmor/hooks/pyarmor_runtime.py`` ,这个脚本补丁可以定义在加密脚本执行之前就被调用的函数。 240 | 241 | 首先创建脚本 ``.pyarmor/hooks/pyarmor_runtime.py`` ,然后定义一个函数 :func:`bootstrap` ,这个函数在扩展模块 `pyarmor_runtime` 初始化的过程中被调用,其他代码都会被忽略。 242 | 243 | .. function:: bootstrap(user_data) 244 | 245 | :param bytes user_data: 运行密钥里面的用户自定义数据 246 | :return: 如果返回 False ,那么扩展模块 pyarmor_runtime 初始化失败,并且抛出保护异常 247 | 返回其他任何值,继续执行加密脚本 248 | :raises SystemExit: 直接退出,不显示调用堆栈 249 | :raises ohter Exception: 退出并且显示调用堆栈 250 | 251 | An example script: 252 | 253 | .. code-block:: python 254 | 255 | def bootstrap(user_data): 256 | # 必须在函数内容导入需要的名称,不要在模块级别导入 257 | import sys 258 | import time 259 | from struct import calcsize 260 | 261 | print('user data is', user_data) 262 | 263 | # 检查平台,不支持 32 位 264 | if sys.platform == 'win32' and calcsize('P'.encode()) * 8 == 32: 265 | raise SystemExit('no support for 32-bit windows') 266 | 267 | # 在 Windows 平台下面检查是否有调试器存在 268 | if sys.platform == 'win32': 269 | from ctypes import windll 270 | if windll.kernel32.IsDebuggerPresent(): 271 | print('found debugger') 272 | return False 273 | 274 | # 在这个例子中,传入的自定义数据是时间戳 275 | if time.time() > int(user_data.decode()): 276 | return False 277 | 278 | 验证一下这个脚本,首先拷贝这个脚本到 ``.pyarmor/hooks/pyarmor_runtime.py`` ,然后执行下面的命令:: 279 | 280 | $ pyarmor gen --bind-data 12345 foo.py 281 | $ python dist/foo.py 282 | 283 | user data is b'12345' 284 | Traceback (most recent call last): 285 | File "dist/foo.py", line 2, in 286 | ... 287 | RuntimeError: unauthorized use of script (1:10325) 288 | 289 | 如果需要提取获取设备的硬件信息,可以编写自己的代码,也可以直接使用 Pyarmor 的扩展模块 `pytransform3` 。例如,在 Windows 平台,首先把扩展模块 `pytransform3.pyd` 也拷贝到运行环境,然后使用下面的代码获取硬件信息 290 | 291 | .. code-block:: python 292 | 293 | def bootstrap(user_data): 294 | from pytransform3 import get_hd_info 295 | # 请参考 pyarmor/cli/get_hd_info.py 中的代码 296 | print('Machine ID: %s' % get_hd_info(22).decode()) 297 | 298 | ==================== 299 | 运行加密脚本的环境 300 | ==================== 301 | 302 | 加密脚本运行在 :term:`客户设备` 上面 303 | 304 | 支持的 Python 版本和平台 305 | ======================== 306 | 307 | 运行加密脚本支持的平台和 Python 版本和 `生成加密脚本的环境`_ 是一样的。 308 | 309 | 环境变量 310 | ======== 311 | 312 | 这里的环境变量会被加密脚本使用来决定一些运行设置 313 | 314 | .. envvar:: LANG 315 | 316 | 操作系统环境变量,加密脚本读取它来决定运行时刻的语言设置 317 | 318 | .. envvar:: PYARMOR_LANG 319 | 320 | 用来设置运行时刻使用的语言设置。 321 | 322 | 如果这个变量存在,那么 :envvar:`LANG` 会被忽略 323 | 324 | .. envvar:: PYARMOR_RKEY 325 | 326 | 设置搜索 :term:`外部密钥` 的路径 327 | 328 | 第三方解释器的支持 329 | ================== 330 | 331 | 对于第三方的解释器(例如 Jython 等)以及通过嵌入 Python C/C++ 代码调用加密脚本,只要第三方解释器能够和 CPython :term:`扩展模块` 兼容,就可以使用加密脚本。查看第三方解释器的文档,确认它是否支持 CPython 的扩展模块, 332 | 333 | 已知的一些问题 334 | 335 | * `PyPy` 无法运行加密脚本,因为它完全不同于 `CPython` 。 336 | 337 | * 在 Linux 下面 装载 Python 动态库 `libpythonXY.so` 的时候 `dlopen` 必须设置 `RTLD_GLOBAL` ,否则加密脚本无法运行。 338 | 339 | * Boost::python,默认装载 Python 动态库是没有设置 `RTLD_GLOAL` 的,运行加密脚本的时候会报错 "No PyCode_Type found" 。解决方法就是在初始化的调用方法 `sys.setdlopenflags(os.RTLD_GLOBAL)` ,这样就可以共享动态库输出的函数和变量。 340 | 341 | * 模块 `ctypes` 必须存在并且 `ctypes.pythonapi._handle` 必须被设置为 Python 动态库的句柄,PyArmor 会通过该句柄获取 Python C API 的地址。 342 | 343 | * WASM 目前不支持,因为这需要把运行库的代码也编译成为 WASM,但是 WASM 是很容易就被反编译成为原来的 C 代码,为了安全性,所以目前没有支持 WASM 的计划。如果有更多的用户提出这个需求,会考虑实现一个轻量级的运行库,只支持能够运行 RFT 模式的加密脚本,但是目前还没有开发计划。 344 | 345 | .. _specialized builtin functions: 346 | 347 | 加密脚本内置函数 348 | ================ 349 | 350 | .. versionadded:: 8.2 351 | 352 | 有两个特殊的函数无需导入,可以直接在加密脚本中使用,通常它们一般用在 :term:`脚本补丁` 中。 353 | 354 | .. function:: __pyarmor__(arg, kwarg, name, flag) 355 | 356 | :param bytes name: 必须是 ``b'hdinfo'`` 或者 ``b'keyinfo'`` 357 | :param int flag: 必须是 ``1`` 358 | 359 | **查询当前设备硬件信息** 360 | 361 | 当 ``name`` 等于 ``b'hdinfo'`` 时候,调用这个函数可以获取当前设备硬件信息 362 | 363 | :param int arg: 获取不同类型的设备信息,有效值:0,1,2,4 364 | :param str kwarg: None,或者设备名称 365 | :return: arg 为 0 返回硬盘序列号 366 | :return: arg 为 1 返回网卡以太网地址 367 | :return: arg 为 2 返回IPv4 地址 368 | :return: arg 为 3 无效值 369 | :return: arg 为 4 返回当前域名 370 | :rtype: 字符串 371 | 372 | 例如, 373 | 374 | .. code-block:: python 375 | 376 | __pyarmor__(0, None, b'hdinfo', 1) 377 | __pyarmor__(1, None, b'hdinfo', 1) 378 | 379 | 在 Linux 系统, ``kwarg`` 可以用来指定网卡或者硬盘的设备名称,例如: 380 | 381 | .. code-block:: python 382 | 383 | __pyarmor__(0, "/dev/vda2", b'hdinfo', 1) 384 | __pyarmor__(1, "eth2", b'hdinfo', 1) 385 | 386 | 在 Windows 系统, ``kwarg`` 还可以用来得到全部网卡或者全部硬盘的信息。例如: 387 | 388 | .. code-block:: python 389 | 390 | __pyarmor__(0, "/0", b'hdinfo', 1) # First disk 391 | __pyarmor__(0, "/1", b'hdinfo', 1) # Second disk 392 | 393 | __pyarmor__(1, "*", b'hdinfo', 1) 394 | __pyarmor__(1, "*", b'hdinfo', 1) 395 | 396 | **查询运行密钥中的数据和有效期** 397 | 398 | 当 ``name`` 等于 ``b'keyinfo'`` 的时候,调用这个函数可以获取运行密钥中信息 399 | 400 | :param int arg: 指定获取的信息,有效值:0,1 401 | :param kwarg: None,暂时没有使用 402 | :return: arg 为 0 返回运行密钥绑定的私有数据,没有绑定的数据返回 ``b''`` 403 | :rtype: Bytes 404 | :return: arg 为 1 返回运行密钥的有效期 (epoch),如果没有设置有效期返回 -1 405 | :rtype: Long 406 | :return: 如果发生错误,那么返回 None 407 | 408 | 例如, 409 | 410 | .. code-block:: python 411 | 412 | print('bind data is', __pyarmor__(0, None, b'keyinfo', 1)) 413 | print('expired epoch is' __pyarmor__(1, None, b'keyinfo', 1)) 414 | 415 | .. function:: __assert_armored__(arg) 416 | 417 | :param object arg: 模块,函数或者方法 418 | :returns: 如果 ``arg`` 指定的对象是被加密过的,那么返回 ``arg`` 自身,否则抛出保护异常 419 | 420 | 例如 421 | 422 | .. code-block:: python 423 | 424 | m = __import__('abc') 425 | __assert_armored__(m) 426 | 427 | def hello(msg): 428 | print(msg) 429 | 430 | __assert_armored__(hello) 431 | hello('abc') 432 | 433 | .. include:: ../_common_definitions.txt 434 | -------------------------------------------------------------------------------- /docs/reference/errors.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 错误消息 3 | ========== 4 | 5 | .. highlight:: none 6 | 7 | .. program:: pyarmor gen 8 | 9 | 这里列出了加密时候和运行加密脚本的时候的错误信息列表,并且给出了导致错误发生的可能原因以及解决方案。 10 | 11 | 如果错误信息没有在这里找到,那么一般情况下这种错误不是 Pyarmor 引起的,可能是因为系统环境配置不正确,缺失系统包等原因造成的。这部分原因不需要 Pyarmor 进行任何修改,只要把环境配置正确,安装必要的包等就可以解决。对于这种类型的问题,请直接在百度或者其他任何搜索引擎,网站和论坛等查找解决方案。 12 | 13 | 加密时候的错误消息 14 | ================== 15 | 16 | **加密常见错误信息** 17 | 18 | 下表列出的是运行命令 :command:`pyarmor` 时候常见的错误信息,部分可能的原因和解决方案 19 | 20 | .. list-table:: 表-1. 加密时候错误信息表 21 | :name: pyarmor errors 22 | :header-rows: 1 23 | 24 | * - 错误信息 25 | - 原因和解决方案 26 | * - out of license 27 | - 使用试用版或者当前许可证没有的功能 28 | 29 | 解决方案请参考 :doc:`../licenses` 30 | * - not machine id 31 | - 当前设备的硬件信息发生了改变可能会造成这个错误 32 | 33 | 重现在当前设备注册 Pyarmor 可以解决这个问题 34 | * - query machine id failed 35 | - 如果无法获取到当前设备的相关硬件信息,会报这个错误 36 | * - relative import "%s" overflow 37 | - 尝试直接加密一个脚本,但是脚本里面使用了相对导入的语句 38 | 39 | 解决方案: 直接加密脚本所在的包 (目录),而不是单独加密一个文件 40 | 41 | **注册错误信息** 42 | 43 | 下表列出一般发生在注册 Pyarmor 时候的错误信息 44 | 45 | .. list-table:: 表-1.1 注册时候错误信息表 46 | :name: register errors 47 | :header-rows: 1 48 | 49 | * - 错误信息 50 | - 原因和解决方案 51 | * - HTTP Error 400: Bad Request 52 | - 请升级到 Pyarmor 8.2+,以显示正确的错误信息,然后采取相应的解决方案 53 | * - HTTP Error 401: Unauthorized 54 | - 在新版本的许可证下使用 pyarmor-7 命令 55 | 56 | 没有解决方案,pyarmor-7 只支持老版本的许可证 57 | * - HTTP Error 503: Service Temporarily Unavailable 58 | - 在 1 分钟之内使用了多次注册命令 59 | 60 | 许可证服务器一分钟之内至多允许同一个 IP 的三次请求,过多的请求将返回 503 错误 61 | * - unknown license type OLD 62 | - 在 Pyarmor 8 中使用老版本的许可证 63 | 64 | 请参阅这里的升级说明 :doc:`../licenses` 65 | 66 | 解决方案:使用命令 ``pyarmor-7`` 67 | * - This code has been used too many times 68 | - 如果是在 CI/Docker 中使用 Pyarmor,请通过该订单的注册邮箱发送订单信息到 pyarmor@163.com 以解锁该订单 69 | * - no registration code found in pyarmor-regcode-xxxx.txt 70 | - 重新下载邮件附件 `pyarmor-regcode-xxxx.txt` ,检查其内容,应该和邮件正文一样 71 | * - update license token failed 72 | - 如果在 1 分钟之内运行注册命令超过 3 次,请等上 5 分钟之后在进行测试 73 | 74 | 如果在 24 小时之内,在超过 100 台不同设备或者Docker 容器上运行注册命令,请耐心等待直到有可用的 Token 75 | 76 | 如果当前设备的日期和时间不正确,请调整为当前时间 77 | 78 | 如果还存在问题,在浏览器打开网页 `http://pyarmor.dashingsoft.com//api/auth2/` 79 | 80 | 如果页面返回 `NO:missing parameters` ,这说明网络没有问题,Pyarmor 许可证服务器也没有问题 81 | 82 | 如果使用的是 v8.5.3 之前的版本,首先升级到 v8.5.3+,然后使用下面的命令检查 Python 是否可以访问服务器:: 83 | 84 | $ python 85 | >>> from urllib.request import urlopen 86 | >>> res = urlopen('http://pyarmor.dashingsoft.com//api/auth2/') 87 | >>> print(res.read()) 88 | b'NO:missing parameter' 89 | 90 | 如果返回其他或者抛出异常,那么很有可能是防火墙设置问题,不允许 Python 访问网络,请参考防火墙文档正确进行配置 91 | 92 | 93 | 运行加密脚本的错误信息 94 | ====================== 95 | 96 | **Pyarmor 报告的错误信息** 97 | 98 | 这里列出的是运行加密脚本的时候 Pyarmor 报出的错误信息,部分可能的原因和解决方案。 99 | 100 | 如果该消息有错误代码,那么用户可以使用消息代码对这个消息进行国际化和本地化处理。没有错误代码的消息无法进行定制。 101 | 102 | .. list-table:: 表-2. 运行加密脚本的错误信息表(Pyarmor) 103 | :name: runtime errors 104 | :header-rows: 1 105 | 106 | * - 错误代码 107 | - 错误信息 108 | - 原因和解决方案 109 | * - 110 | - error code out of range 111 | - Internal error 112 | * - error_1 113 | - this license key is expired 114 | - 115 | * - error_2 116 | - this license key is not for this machine 117 | - 118 | * - error_3 119 | - missing license key to run the script 120 | - 121 | * - error_4 122 | - unauthorized use of script 123 | - 124 | * - error_5 125 | - this Python version is not supported 126 | - 127 | * - error_6 128 | - the script doesn't work in this system 129 | - 130 | * - error_7 131 | - the format of obfuscated script is incorrect 132 | - 可能的原因: 133 | 134 | 1. 加密脚本是由其他不兼容的 Pyarmor 生成的,请使用最新的 Pyarmor 进行加密 135 | 2. 无法获得运行辅助包所在的路径,也会报这个错误 136 | * - error_8 137 | - the format of obfuscated function is incorrect 138 | - 139 | * - 140 | - RuntimeError: Resource temporarily unavailable 141 | - 设置了加密脚本的有效期,但是无法访问时间服务器导致的问题 142 | 143 | 解决方案: 144 | 145 | 1. 使用本地时间 146 | 2. 用户使用 :term:`脚本补丁` 自己进行校验 147 | 3. 升级到 Pyarmor 8.4.4+ ,并使用 HTTP 服务器进行检验,需要通过命令设置一个有效的 HTTP 服务器。例如 `pyarmor cfg nts=http://your.http-server.com/api/v2/` 148 | * - 149 | - Protection Exception 150 | - 如果使用了选项 :option:`--assert-call` 或者 :option:`assert-import` ,那么参考 :doc:`../tutorial/advanced` 中的 `过滤需要保护的函数和模块` ,根据错误堆栈的信息,忽略出错的函数或者模块。 151 | 152 | 153 | **Python 报告的错误信息** 154 | 155 | 通常情况下,这种错误不是 Pyarmor 造成的。 156 | 157 | 解决这里的问题只需要参考 Python 的文档,把加密脚本看作是普通脚本,就可以解决问题,也可以直接在百度或者 Python 相关的论坛网站找到答案。 158 | 159 | .. list-table:: 表-2.1 运行加密脚本的错误信息表(Python) 160 | :name: other runtime errors 161 | :header-rows: 1 162 | 163 | * - 错误信息 164 | - 原因和解决方案 165 | * - ImportError: attempted relative import with no known parent package 166 | - 1. ``from .pyarmor_runtime_000000 import __pyarmor__`` 167 | 168 | 解决方案:不要使用 :option:`-i` 或者 :option:`--prefix` 去加密脚本 169 | 170 | 外部错误信息 171 | ============ 172 | 173 | 这里列出的错误信息主要是外部环境引起的,和 Pyarmor 本身无关,只有进行正确配置或者安装缺少的组件,就可以解决这里的问题 174 | 175 | 请通过 google 或者其它搜索引擎搜索相应的错误信息去找到解决方案 176 | 177 | **No such file or directory: 'nul'** 178 | 179 | 这个通常是 Windows 操作系统的问题,例如没有及时更新 Windows 系统,或者使用的是盗版的 Windows 等等原因造成的 180 | 181 | 解决方案包括全部更新 Widnows 系统,或者 182 | 183 | 在命令行执行下面的命令重新创建 NULL:: 184 | 185 | sc create null binpath=C:\Windows\System32\drivers\null.sys type=kernel start=auto error=normal 186 | 187 | 然后重启 NULL 服务:: 188 | 189 | sc start null 190 | 191 | 如果能够正常启动,那么应该可以解决这个问题 192 | 193 | .. include:: ../_common_definitions.txt 194 | -------------------------------------------------------------------------------- /docs/reference/solutions.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | 常见的使用错误 3 | ================ 4 | 5 | .. program:: pyarmor gen 6 | 7 | 在 Pyarmor 开发组处理的报告中,有 80% 属于使用错误的问题,根据用户这些常见的错误问题,Pyarmor 开发组整理该文档。请首先通过该文档的检查确认不是使用错误,对于使用错误的问题,这里也会提示出正确的使用方法。 8 | 9 | 如果是加密过程发生的错误,请查看章节 :ref:`build device` 并根据相应的条件查找解决方案 10 | 11 | 如果是运行加密过程发生的错误,请查看章节 :ref:`target device` 根据相应的条件查找解决方案 12 | 13 | .. _build device: 14 | 15 | 运行 Pyarmor 的设备 16 | =================== 17 | 18 | .. _fix-bootstrap-issue: 19 | 20 | 启动失败 21 | -------- 22 | 23 | 1. 检查是否已经正确安装 `pyarmor.cli.core` 包 24 | 25 | 搜索 pyarmor 的安装目录树中是否存在扩展模块 `pytransform3.pyd` 或者 `pytransform3.so` 26 | 27 | 如果不存在,请参考安装文档进行正确安装 28 | 29 | 2. 检查扩展模块 `pytransform3` 是否适用于当前平台 30 | 31 | 使用 Python 直接运行下面的命令: 32 | 33 | .. code-block:: bash 34 | 35 | $ python 36 | >>> from pyarmor.cli.core import Pytransform3 37 | >>> Pytransform3.version() 38 | 1 39 | >>> 40 | 41 | 如果不能正确返回,尝试使用下面的命令检查扩展模块是否适用于当前平台 42 | 43 | 在 Linux 平台下面,使用 `ldd` 执行下面的命令 44 | 45 | .. code-block:: bash 46 | 47 | $ ldd /path/to/pytransform3.so 48 | 49 | 在 MacOS 平台下面,使用 `codesign` 和 `otool` 执行下面的命令 50 | 51 | .. code-block:: bash 52 | 53 | $ codesign -v /path/to/pytransform3.so 54 | $ otool -L /path/to/pytransform3.so 55 | 56 | 在 Windows 平台下面,使用 [cygcheck]_ 进行检查:: 57 | 58 | C:\> cygcheck \path\to\pytransform3.pyd 59 | 60 | 或者:: 61 | 62 | C:\> dumpbin /dependents \path\to\pytransform3.pyd 63 | 64 | 根据报错信息安装相应的包看能否解决问题 65 | 66 | 3. 如果上面的命令报错并且无法解决,查看 :doc:`environments` 是否支持当前平台 67 | 68 | 对于 Linux 的变体系统,可以查看包 `pyarmor.cli.core.linux` , `pyarmor.cli.core.alpine` ,以及 `pyarmor.cli.core.android` 等的内容,每一个包里面都有多个 `pytransform.so` ,例如:: 69 | 70 | $ unzip ./pyarmor.cli.core.linux-6.5.3-cp310-none-any.whl 71 | 72 | Archive: ./pyarmor.cli.core.linux-6.5.3-cp310-none-any.whl 73 | inflating: pyarmor/cli/core/linux/__init__.py 74 | inflating: pyarmor/cli/core/linux/aarch64/pyarmor_runtime.so 75 | inflating: pyarmor/cli/core/linux/aarch64/pytransform3.so 76 | inflating: pyarmor/cli/core/linux/armv7/pyarmor_runtime.so 77 | inflating: pyarmor/cli/core/linux/armv7/pytransform3.so 78 | inflating: pyarmor/cli/core/linux/loongarch64/pyarmor_runtime.so 79 | inflating: pyarmor/cli/core/linux/loongarch64/pytransform3.so 80 | inflating: pyarmor/cli/core/linux/mips32el/pyarmor_runtime.so 81 | inflating: pyarmor/cli/core/linux/mips32el/pytransform3.so 82 | ... 83 | 84 | 依次使用 `ldd` 检查,如果某一个包中某一个目录下面的 `pytransfrom.so` 可以用,那么拷贝它们到包 `pyarmor.cli.core` 的安装目录,例如:: 85 | 86 | $ cp pyarmor/cli/core/linux/loongarch64/*.so /path/to/pyarmor/cli/core 87 | $ pyarmor gen foo.py 88 | 89 | 或者安装这个包,同时设置环境变量:: 90 | 91 | $ pip install ./pyarmor.cli.core.linux-6.5.3-cp310-none-any.whl 92 | $ export PYARMOR_PLATFORM=linux.loongarch64 93 | $ pyarmor gen foo.py 94 | 95 | 4. 如果支持该平台,那么尝试升级 Python 小版本到最新的补丁版本,例如,升级 Python 3.11.0b2 到 3.11.9 96 | 97 | .. _fix-register-issue: 98 | 99 | 注册失败 100 | -------- 101 | 102 | 如果使用的是 :term:`激活文件` ( `pyarmor-regcode-xxxx.txt` ),确保使用次数没有超过 3 次,正常情况下这个文件仅用于第一次激活注册,以后的所有注册都应该使用 :term:`注册文件` ``pyarmor-regfile-xxxx.zip`` 103 | 104 | **基础版或者专家版许可证** 105 | 106 | 1. 检查设备的日期时间设置是否正确 107 | 2. 是否一分钟之内运行超过 3 次的注册命令 108 | 3. 如果是在运行 Docker 容器的话,是否在一分钟之内运行超过 3 个的 Docker 容器 109 | 4. 是否在 24 小时之内在超过 100 台不同设备(Docker 容器)上面运行注册命令 110 | 111 | 如果以上限制都没有超过的话, 112 | 113 | 1. 尝试在浏览器访问 http://pyarmor.dashingsoft.com/api/auth2/ 或者直接使用 `curl` 等进行测试 114 | 115 | 如果返回 "NO:missing parameters" ,这说明网络和 Pyarmor 服务器都是好着的 116 | 117 | 否则请检查本机的网络配置 118 | 119 | 2. 检查 Python 解释器是否可以访问 Pyarmor 服务器。使用 Python 解释器执行下面的命令,如果无法返回期望结果,那么请检查本机防火墙配置或者公司网络配置 120 | 121 | .. code-block:: bash 122 | 123 | $ python 124 | >>> from urllib.request import urlopen 125 | >>> res = urlopen('http://pyarmor.dashingsoft.com/api/auth2/') 126 | >>> print(res.read()) 127 | b'NO:missing parameter' 128 | 129 | 注意如果当前设备安装有多个 Python 的话,这个 Python 解释器必须是用来执行 Pyarmor 的解释器,否则没有意义 130 | 131 | 3. 如果以上都正确返回,还是无法注册成功,请提交问题报告 132 | 133 | 报告中需要包含注册失败的许可证编号,例如 pyarmor-vax-6058 134 | 135 | **集团版许可证** 136 | 137 | 1. 当前设备的 Machine ID 是否每次启动发生变化 138 | 139 | - 如果使用的是 Pyarmor 8.5 之前的版本,首先升级 Pyarmor 到最新版本 140 | - 如果是在硬件信息重启之后会发生变化的虚拟机器,例如 Github Action 的默认 runner ,无法直接使用集团版许可证 141 | 142 | 2. 是否已经使用集团版许可证的 :term:`注册文件` (一般为 ``pyarmor-regfile-????.zip`` )生成了当前设备的注册文件(一般为 ``pyarmor-device-regfile-????.N.zip`` ) 143 | 144 | 3. 设备注册文件中的 Machine ID 是否和当前设备匹配 145 | 146 | 4. 如果设备 Machine ID 每次启动都保持不变,并且使用了正确的设备注册文件,请提交问题报告,至少包含下列信息: 147 | 148 | - 设备的 Machine ID 149 | - 设备类型,是物理设备,还是虚拟机,还是云服务器等 150 | - 操作系统类型 151 | - 对于类似 Linux 系统,提供 `uname -a` 的输出 152 | 153 | **使用集团版许可证运行多个 Docker 容器** 154 | 155 | 确保 Docker 主机 和 Docker 容器在同一个网络中,使用 `ifconfig` 命令检查网路配置。 156 | 157 | .. _fix-obfuscate-issue: 158 | 159 | 加密失败 160 | -------- 161 | 162 | **update license token failed** 163 | 164 | 请参考 `注册失败` 中的解决方案 165 | 166 | **加密出现其他异常或者错误信息** 167 | 168 | 1. 加密一个简单脚本 foo.py ,看能否成功 169 | 2. 如果不能成功,那么参考上面 `启动失败` 的检查点,确保扩展模块 `pytransform3` 适用当前平台 170 | 3. 如果扩展模块没有问题,请尝试在忽略配置环境产生的影响 171 | 172 | 例如,把当前目录下面的局部配置目录 `.pyarmor` 更名为 `.pyarmor.bak` , 把全局配置目录 `~/.pyarmor/config` 更名为 `~/.pyarmor/config.bak` 173 | 174 | 4. 使用更少加密选项,看能否成功,确定导致问题的选项 175 | 5. 报告问题,使用调试选项 `-d` 生成 `pyarmor.report.bug` ,并基于此 176 | 177 | - 提交一个尽可能简单的可以重现的脚本 178 | - 加密选项使用最少的可以重现的选项,尽量不要提交不会导致崩溃的无关选项 179 | 180 | .. _fix-pack-issue: 181 | 182 | 打包失败 183 | -------- 184 | 185 | 1. 直接使用 PyInstaller 打包没有加密的脚本,确保打包可以成功 186 | 2. 查看 :doc:`../topic/repack` 187 | 188 | .. _target device: 189 | 190 | 运行加密脚本的设备 191 | ================== 192 | 193 | .. _fix-runtime-bootstrap-issue: 194 | 195 | 导入运行辅助库装载失败 196 | ---------------------- 197 | 198 | 1. 检查 :ref:`运行辅助包` 是否存在 199 | 200 | 在加密脚本目录树中,搜索扩展模块 `pyarmor_runtime.pyd` 或者 `pyarmor_runtime.so` ,确保其存在 201 | 202 | :ref:`运行辅助包` 是一个正常的 Python 包,可以把它当作第三方的包来使用,但是必须能够被加密脚本导入 203 | 204 | 2. 检查加密脚本的导入语句是否正确 205 | 206 | 直接打开加密脚本,应该可以看到加密脚本的第一条语句是一个标准的 `from ... import` 语句,请按照 Python 本身的导入机制确保能导入 :ref:`运行辅助包` 207 | 208 | 可以通过移动运行辅助包的位置或者修改导入语句,以及使用选项 :option:`-i` 或者 :option:`--prefix` 重现生成加密脚本来解决这个问题 209 | 210 | 3. 尝试升级 Python 小版本到最新的补丁版本,例如,升级 Python 3.11.0b2 到 3.11.9 211 | 212 | **如果加密设备和目标设备不是同一台机器** 213 | 214 | 1. 确保运行加密脚本的 Python 版本和加密时候使用的 Python 大小版本(Major.Minor)一致 215 | 216 | 2. 如果两者架构不同,请确保加密的时候使用了正确的跨平台加密选项 :option:`--platform` 217 | 218 | 3. 检查扩展模块 `pyarmor_runtime` 是否匹配目标设备的 Python 版本和平台架构 219 | 220 | 在 Linux 平台下面,使用 `ldd` 执行下面的命令 221 | 222 | .. code-block:: bash 223 | 224 | $ ldd /path/to/pyarmor_runtime.so 225 | 226 | 在 MacOS 平台下面,使用 `codesign` 和 `otool` 执行下面的命令 227 | 228 | .. code-block:: bash 229 | 230 | $ codesign -v /path/to/pyarmor_runtime.so 231 | $ otool -L /path/to/pyarmor_runtime.so 232 | 233 | 在 Windows 平台下面,使用 [cygcheck]_ 进行检查:: 234 | 235 | C:\> cygcheck \path\to\pyarmor_runtime.pyd 236 | 237 | 或者:: 238 | 239 | C:\> dumpbin /dependents \path\to\pyarmor_runtime.pyd 240 | 241 | 根据报错信息安装相应的包看能否解决问题。 242 | 243 | 如果无法解决问题,查看 :doc:`environments` 是否支持当前平台 244 | 245 | 另外对于跨平台加密,尝试使用不同的目标平台,例如目标平台是 Android 的话,如果 `--platform android.aarch64` 加密的脚本无法运行,可以尝试 `--platform linux.aarch64` 或者 `--platform alpine.aarch64` ,因为它们的区别主要在于使用不同的 LIBC 库 246 | 247 | **unauthorized use of script** 248 | 249 | 1. 不要使用 :option:`--private` , :option:`--restrict` , :option:`--assert-call` , :option:`--assert-import` 等约束选项 250 | 251 | 2. 使用 `pyarmor cfg assert.call:excludes "xxx"` 或者 `pyarmor cfg assert.import:excludes "xxx"` 排除出现问题的函数和模块 252 | 253 | 3. 发现导致出现问题的选项,并报告问题,至少包含以下内容 254 | 255 | - 加密时候使用的完整选项 256 | - 可以重现问题的简单脚本,不要使用第三方库,尤其是依赖很多,安装包很大的第三方库 257 | - 运行加密脚本的完整命令和完整的异常信息堆栈 258 | 259 | .. _fix-run-obfuscated-script-issue: 260 | 261 | 运行加密脚本出现错误或者异常 262 | ---------------------------- 263 | 264 | 1. 尝试不要使用 RFT/BCC/约束选项等进行加密,看是否可以运行,可以运行的话说明问题是 RFT/BCC/约束选项 造成的,请参考下面的相应分支继续检查 265 | 266 | 2. 如果使用最少的选项加密脚本依旧不可以运行,尝试加密一个简单脚本,看看是否可以正确运行 267 | 268 | 3. 简化脚本,尝试找到一个可以重现问题的最简单脚本,最好不需要使用第三方库,如果使用到第三方库,查看 :doc:`../how-to/third-party` 269 | 270 | 4. 提交问题报告,至少包含下列内容 271 | 272 | - 加密时候使用的完整选项 273 | - 可以重现问题的简单脚本,不要使用第三方库,尤其是依赖很多,安装包很大的第三方库 274 | - 运行加密脚本的完整命令和完整的异常信息堆栈 275 | 276 | **使用了约束选项进行加密** 277 | 278 | 参考上面的 **unauthorized use of script** 279 | 280 | **使用了 RFT 模式进行加密** 281 | 282 | 参阅 :ref:`using rftmode` 中的解决方案 283 | 284 | **使用了 BCC 模式进行加密** 285 | 286 | 首先使用一个简单脚本验证环境配置正确,如果简单脚本不工作,那么是多数是配置不正确或者未知的环境变量的影响 287 | 288 | 然后参阅 :ref:`using bccmode` 中的解决方案 289 | 290 | .. _fix-run-packed-script-issue: 291 | 292 | 运行打包的加密脚本出现错误或者异常 293 | ---------------------------------- 294 | 295 | 1. 不要打包,只是使用相同选项加密脚本,然后运行加密后的脚本,检查是否运行正常,如果不正常,那么参考上面加密脚本运行失败的解决方案 296 | 297 | 2. 不要加密脚本,直接使用 PyInstaller_ 打包没有加密的脚本,检查打包后的可执行文件是否运行正常,如果不正常,请参考 PyInstaller_ 文档解决问题 298 | 299 | 3. 如果以上都正常,尝试使用更少的加密选项,重复步骤 1 和 步骤 2,找到导致问题出现的选项,然后报告问题 300 | 301 | 特别的,如果打包设备和目标设备不是同一台机器,必须在目标设备上检查运行结果 302 | 303 | 例如,在步骤1的检查中,直接在加密设备上运行加密脚本是没有意义的,必须在目标设备上运行加密脚本进行检查 304 | 305 | .. _platform issues: 306 | 307 | 平台相关问题 308 | ============ 309 | 310 | Darwin Apple Silicon 如果刚运行就崩溃,那么需要对扩展模块 `pytransform3` 或者 `pyarmor_runtime` 进行签名 311 | 312 | .. rubric:: 注释 313 | 314 | .. [cygcheck] 如何快速获取 `cygcheck.exe` 315 | 316 | 直接下载 https://pyarmor.dashingsoft.com/downloads/tools/cygcheck.zip 解压 317 | 318 | 或者从官网下载 319 | 320 | - 打开 https://cygwin.com/mirrors.html 321 | - 选择一个网站,依次进入目录 `x86_64/release/cygwin/` 322 | - 下载最新的 cygwin-3.5.3-1.tar.xz 323 | - 解压 tar xJf cygwin-3.5.3-1.tar.xz 324 | - 提取其中的 cygcheck.exe 325 | 326 | .. include:: ../_common_definitions.txt 327 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | docutils<0.18 2 | sphinx==5.3.0 3 | sphinx_rtd_theme==1.1.1 4 | readthedocs-sphinx-search==0.3.2 5 | urllib3<2.0 6 | -------------------------------------------------------------------------------- /docs/topic/bccmode.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | 深入了解 BCC 模式 3 | ================= 4 | 5 | .. highlight:: console 6 | 7 | .. program:: pyarmor gen 8 | 9 | BCC 模式会把部分函数直接转换成为二进制代码,在根本上避免被还原成为 Python 函数。 10 | 11 | BCC 模式需要配置 :term:`C` 编译器,对于 `Linux` 和 `Darwin` 来说,一般不需要进行配置,只要默认的 ``gcc`` 和 ``clang`` 能工作就可以。在 `Windows` 环境下面,可以使用下面任意一种方式配置 ``clang.exe`` ,目前其它编译器还不支持: 12 | 13 | * 如果已经有 ``clang.exe`` ,只要在其它路径直接运行 ``clang.exe`` 不出错就可以。如果文件存在,但是无法在任意路径直接运行,可以配置环境变量 ``PYARMOR_CC`` 来指定这个文件,例如:: 14 | 15 | set PYARMOR_CC=C:\path\to\clang.exe 16 | 17 | * 从 `LLVM 官网 `_ 下载并安装预编译版本 18 | * 从 Pyarmor 官网下载 `clang-9.0.zip`__ ,压缩包大小约为 26M 左右,里面只有一个可执行文件,解压后存放在 :term:`根目录` 下面,默认是 ``%HOME%/.pyarmor`` 19 | 20 | __ https://pyarmor.dashingsoft.com/downloads/tools/clang-9.0.zip 21 | 22 | 启用 BCC 模式 23 | ============= 24 | 25 | 配置好编译器之后,使用选项 :option:`--enable-bcc` 启用 BCC 模式:: 26 | 27 | $ pyarmor gen --enable-bcc foo.py 28 | 29 | 模块级别的代码不会转换转换成为 :term:`C` 的函数,模块的任何函数如果使用了不被支持的特性,也不会转换成为 :term:`C` 函数,这么没有使用 BCC 模式加密的函数会根据选项使用其他方式进行加密。 30 | 31 | 查看被 BCC 模式加密的函数 32 | ========================= 33 | 34 | 启用跟踪模式可以在跟踪日志文件 ``pyarmor.trace.log`` 中记录那些函数被转换成为了 :term:`C` 函数。例如:: 35 | 36 | $ pyarmor cfg enable_trace=1 37 | $ pyarmor gen --enable-bcc foo.py 38 | 39 | 查看跟踪日志中使用 ``trace.bcc`` 记录的内容:: 40 | 41 | $ ls pyarmor.trace.log 42 | $ grep trace.bcc pyarmor.trace.log 43 | 44 | trace.bcc foo:5:hello 45 | trace.bcc foo:9:sum2 46 | trace.bcc foo:12:main 47 | 48 | 第一条日志记录的是 ``foo.py`` 第5行的函数 ``hello`` 被转换成为 C 函数 49 | 第二条日志记录的是 ``foo.py`` 第9行的函数 ``sum2`` 被转换成为 C 函数 50 | 51 | 如果在 ``trace.bcc`` 之后的字符是 ``!`` ,那么意味着这个函数被 BCC 模式忽略。例如:: 52 | 53 | trace.bcc ! foo:29:Test.new (unsupported function "super") 54 | 55 | 不转换特定的模块和函数 56 | ====================== 57 | 58 | 使用 BCC 模式加密脚本并不是完全和原来的脚本兼容,如果运行 BCC 模式加密脚本出现了兼容性问题,那么解决方案就是不要转换特定的模块,或者模块中特定的函数。 59 | 60 | 为了避免影响其他脚本,这里使用 :term:`模块私有配置` 来修改单独修改模块的设置。 61 | 62 | 让 BCC 模式忽略一个模块 ``pkgname.modname`` 使用下面的命令:: 63 | 64 | $ pyarmor cfg -p pkgname.modname bcc:disabled=1 65 | 66 | 忽略模块中的函数或者方法使用下面的命令:: 67 | 68 | $ pyarmor cfg -p pkgname.modname bcc:excludes="name" 69 | $ pyarmor cfg -p pkgname.modname bcc:excludes="name1 name2 name3" 70 | 71 | $ pyarmor cfg -p pkgname.modname bcc:excludes="Class.method_1" 72 | $ pyarmor cfg -p pkgname.modname bcc:excludes="Class.*" 73 | 74 | 如果没有选项 ``-p`` 的话,其他模块中同名函数和方法也会被忽略。 75 | 76 | 下面是一个示例脚本 :file:`foo.py` 77 | 78 | .. code-block:: python 79 | 80 | def hello_a(): 81 | pass 82 | 83 | def hello_b(): 84 | pass 85 | 86 | class Test(object): 87 | 88 | def __init__(self): 89 | pass 90 | 91 | def hello_a(): 92 | pass 93 | 94 | 使用下面的任意一种方式来忽略其中的函数和方法:: 95 | 96 | $ pyarmor cfg -p foo bcc:excludes = "hello_a" 97 | $ pyarmor cfg -p foo bcc:excludes = "hello_a hello_b" 98 | $ pyarmor cfg -p foo bcc:excludes = "hello_*" 99 | 100 | $ pyarmor cfg -p foo bcc:excludes = "Test.hello_a" 101 | $ pyarmor cfg -p foo bcc:excludes = "Test.*" 102 | $ pyarmor cfg -p foo bcc:excludes = "Test.__*__" 103 | 104 | $ pyarmor cfg -p foo bcc:excludes = "hello_a Test.hello_a" 105 | 106 | 如果只需要 BCC 模式处理特定的函数,那么使用选项 `bcc:includes`:: 107 | 108 | # 恢复默认选项 109 | $ pyarmor cfg bcc:excludes = "" 110 | 111 | # BCC 模式只处理模块函数 "hello_a" 112 | $ pyarmor cfg -p foo bcc:includes = "hello_a" 113 | 114 | # BCC 模式还需要处理类方法 "Test.hello_a" 115 | $ pyarmor cfg -p foo bcc:includes + "Test.hello_a" 116 | 117 | # BCC 模式处理类 "Test" 的所有方法,除了 "__init__" 118 | $ pyarmor cfg -p foo bcc:includes="Test.*" bcc:excludes="Test.__init__" 119 | 120 | 我们可以启用跟踪日志,查看这些语句的效果,看看这些模块和函数是否没有被 BCC 模式处理。例如:: 121 | 122 | $ pyarmor cfg enable_trace 1 123 | $ pyarmor gen --enable-bcc foo.py 124 | $ grep trace.bcc pyarmor.trace.log 125 | 126 | 另外一个例子,忽略 ``joker/card.py`` 但是使用 BCC 模式加密包 ``joker`` 的其他模块:: 127 | 128 | $ pyarmor cfg -p joker.card bcc:disabled=1 129 | $ pyarmor gen --enable-bcc /path/to/pkg/joker 130 | 131 | 选项 `bcc:includes` 和 `bcc:excludes` 只对模块基本的函数和类有效,它们不能处理子函数和子类。例如 132 | 133 | .. code-block:: python 134 | 135 | def hello(): 136 | 137 | def wrap(): 138 | pass 139 | 140 | class Test: 141 | 142 | def __init__(self): 143 | pass 144 | 145 | 下面的命令无法忽略子函数 ``wrap`` 和子类 ``Test``:: 146 | 147 | pyarmor cfg bcc:excludes = "wrap hello.wrap Test.__init__ hello.Test.__init__" 148 | 149 | 忽略子函数和子类的唯一方法是直接忽略其所在的顶层函数 ``hello``:: 150 | 151 | pyarmor cfg bcc:excludes = hello 152 | 153 | .. versionadded:: 8.3.4 154 | 155 | 新增选项 `bcc:includes`. 156 | 157 | .. versionchanged:: 8.3.4 158 | 159 | 选项 `bcc:excludes` 的使用方法发生了变化。在之前的版本:: 160 | 161 | # 忽略模块函数和所有的类方法 "hello_a" 162 | pyarmor cfg bcc:excludes="hello_a" 163 | 164 | # 在方法前指定类名称的话,无法忽略任何方法 165 | pyarmor cfg bcc:excludes="Myclass.hello_a" 166 | 167 | 在现在的版本中:: 168 | 169 | # 需要使用下面的形式来忽略模块函数和所有的类方法 "hello_a" 170 | pyarmor cfg bcc:excludes="hello_a *.hello_a" 171 | 172 | # 可以指定类名称来忽略单独的类方法 173 | pyarmor cfg bcc:excludes="Myclass.hello_a" 174 | 175 | 改变的脚本特性 176 | ============== 177 | 178 | 使用 BCC 模式加密后脚本和原来的脚本存在一些额外的不同 179 | 180 | * 部分异常的提示信息和原来不一样 181 | 182 | * 如果不是在异常处理的过程中,直接调用没有参数的 `raise` 抛出的异常类型不同 183 | 184 | 没有加密前 185 | 186 | .. code-block:: python 187 | 188 | >>> raise 189 | RuntimeError: No active exception to reraise 190 | 191 | 在 BCC 模式加密的脚本中 192 | 193 | .. code-block:: python 194 | 195 | >>> raise 196 | UnboundlocalError: local variable referenced before assignment 197 | 198 | * 函数对象的属性,尤其是以双下划线开始的属性,例如 ``__qualname__`` 等等,在转换成为 C 函数之后都不存在,使用这些属性的函数加密后无法正常工作 199 | 200 | * 在异常处理语句中,`sys.exception()` 总是返回 None,依赖系统异常的相关功能都无法工作。例如 201 | 202 | .. code-block:: python 203 | 204 | import traceback 205 | 206 | def main(): 207 | try: 208 | 1 / 0 209 | except Exception as e: 210 | 211 | # 在使用 BCC 模式加密的脚本中,sys.exception() 总是返回 None 212 | print(sys.exception()) 213 | 214 | # 在使用 BCC 模式加密的脚本中,下面的语句不会显示 traceback: 215 | # NoneType: None 216 | traceback.print_exc() 217 | 218 | # 在使用 BCC 模式加密的脚本中,可以使用下面的语句打印异常堆栈 219 | # 但是异常堆栈中不包含行号和 Python 代码 220 | traceback.print_exception(e) 221 | 222 | main() 223 | 224 | 不支持的特性 225 | ============ 226 | 227 | 使用了下列特性的函数无法转换成为 :term:`C` 函数 228 | 229 | .. code-block:: python 230 | 231 | unsupport_nodes = ( 232 | ast.ExtSlice, 233 | 234 | ast.AsyncFunctionDef, ast.AsyncFor, ast.AsyncWith, 235 | ast.Await, ast.Yield, ast.YieldFrom, ast.GeneratorExp, 236 | 237 | ast.NamedExpr, 238 | 239 | ast.MatchValue, ast.MatchSingleton, ast.MatchSequence, 240 | ast.MatchMapping, ast.MatchClass, ast.MatchStar, 241 | ast.MatchAs, ast.MatchOr 242 | ) 243 | 244 | 如果调用了下列任意一个内置函数,那么该函数也无法转换成为 :term:`C` 函数: 245 | 246 | * exec, 247 | * eval 248 | * super 249 | * locals 250 | * sys._getframe 251 | 252 | 例如,下面这些函数都不会使用终极模式加密,因为它们或者使用了不支持的特性,或者调 253 | 用了不支持的函数: 254 | 255 | .. code-block:: python 256 | 257 | async def nested(): 258 | return 42 259 | 260 | def foo1(): 261 | for n range(10): 262 | yield n 263 | 264 | def foo2(): 265 | frame = sys._getframe(2) 266 | print('parent frame is', frame) 267 | 268 | 已知的一些问题 269 | ============== 270 | 271 | * 当格式化字符串出现语法错误, BCC 模式抛出 `SystemError: NULL object passed to Py_BuildValue` 而不是 `SyntaxError` 或者 `ValueError`. 272 | 273 | 在测试 `lib/python3.12/test/test_fstring.py` 中下列用例时发现: 274 | 275 | - test_invalid_syntax_error_message 276 | - test_missing_variable 277 | - test_syntax_error_for_starred_expressions 278 | - test_with_a_commas_and_an_underscore_in_format_specifier 279 | - test_with_an_underscore_and_a_comma_in_format_specifier 280 | - test_with_two_commas_in_format_specifier 281 | - test_with_two_underscore_in_format_specifier 282 | 283 | * 生成 BCC 代码时候,Pyarmor 有时候报错 `RuntimeError: link bcc code failed` 284 | 285 | 可以尝试增加额外的编译选项 `-DENABLE_BCC_MEMSET` 来解决这个问题。例如,在 32位 Windows 下面可以使用下面的命令增加额外的编译选项:: 286 | 287 | pyarmor cfg windows.x86.bcc:cflags += " -DENABLE_BCC_MEMSET" 288 | 289 | 或者直接修改配置文件 :file:`pyarmor/cli/default.cfg` ,这个选项的最终有效值应该是:: 290 | 291 | Section: windows.x86.bcc 292 | 293 | cflags = --target=i686-elf-linux -O3 -Wno-unsequenced -fno-asynchronous-unwind-tables -fno-unwind-tables -fno-stack-protector -fPIC -mno-sse -std=c99 -c -DENABLE_BCC_MEMSET 294 | 295 | .. include:: ../_common_definitions.txt 296 | -------------------------------------------------------------------------------- /docs/topic/localization.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | 本地化和国际化的工作原理 3 | ======================== 4 | 5 | 加密过程 6 | ======== 7 | 8 | 查找本地配置目录下面的 :file:`messages.cfg` 9 | 没有则查找全局配置目录下面的 :file:`messages.cfg` 10 | 11 | 如果存在定制的消息文件,那么把定制的消息存到运行辅助包里面 12 | 13 | 打开消息文件的编码方式默认是 ``utf-8`` ,如果需要使用其他编码方式,使用下面的命令进行配置:: 14 | 15 | $ pyarmor cfg messages=messages.cfg:gbk 16 | 17 | .. seealso:: :ref:`runtime errors` 18 | 19 | 运行加密脚本的过程 20 | ================== 21 | 22 | 确定默认语言 23 | 24 | * 首先查看 :envvar:`PYARMOR_LANG` 25 | * 其次查看 :envvar:`LANG` 26 | 27 | 查找定制的错误信息,首先匹配语言,然后匹配错误代码 28 | 29 | 没有找到则使用默认的错误信息 30 | 31 | .. include:: ../_common_definitions.txt 32 | -------------------------------------------------------------------------------- /docs/topic/obfuscated-script.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | 深入了解加密脚本 3 | ================ 4 | 5 | .. highlight:: console 6 | 7 | .. program:: pyarmor gen 8 | 9 | 阅读本文档需要一定的 Python 基础,了解 Shell 脚本,环境变量等相关知识。 10 | 11 | 加密脚本就是普通 Python 文件 12 | ============================ 13 | 14 | Pyarmor 加密后的脚本输出的是同名的 ``.py`` 文件和一个 :term:`运行辅助包` 。它们和普通 Python 模块一样,可以被 Python 解释器调用执行,这也是 Pyarmor 的一个加密特点,可以使用加密后的脚本无缝替换原来的脚本。 15 | 16 | 使用加密脚本完全和使用普通的 Python 脚本一样,例如,使用解释器直接运行:: 17 | 18 | python dist/foo.py 19 | 20 | 使用文本编辑器打开这个脚本,它的内容一般如下:: 21 | 22 | from pyarmor_runtime import __pyarmor__ 23 | __pyarmor__(__name__, __file__, b'\x28\x83....') 24 | 25 | 可以看到第一条语句就是导入扩展模块 ``pyarmor_runtime`` ,它就是和加密脚本在相同目录下的文件 ``pyarmor_runtime.so`` 。扩展模块 ``pyarmor_runtime`` 不是必须和加密脚本放在一起,只要它存在于 Python 搜索模块的任何路径,能被 Python 导入进来,加密脚本就可以正常使用。 26 | 27 | 如果运行加密脚本的时候提示模块 ``pyarmor_runtime`` 无法找到,首先要能找到扩展模块文件,然后在扩展模块文件所在的目录直接使用 Python 解释器导入这个模块:: 28 | 29 | cd dist/ 30 | python 31 | >>> import pyarmor_runtime 32 | Traceback (most recent call last): 33 | File "", line 1, in 34 | RuntimeError: the format of obfuscated script is incorrect 35 | 36 | 如果不是抛出这个异常,说明这个扩展模块文件不适用于当前平台和 Python 版本。 37 | 38 | 扩展模块是二进制的动态库,有些没有加密的时候,脚本可以正常执行,加密之后无法运行。就是因为运行环境无法装载扩展模块而造成的。这种情况只要了解扩展模块的相关知识,设置运行环境允许加载扩展模块,都可以正常运行加密脚本。判断运行环境是否允许加载扩展模块的方法是把任何一个系统扩展模块拷贝到当前目录,看看能否导入。 39 | 40 | 在导入扩展模块 ``pyarmor_runtime`` 之前,所有的事情都是 Python 的自身功能,和 Pyarmor 和脚本是否加密都没有关系。解决这里出现的问题需要的就是学习 Python 相关的知识,特别是 Python 是如何根据模块名称去搜索和装载模块和扩展模块的。 41 | 42 | 扩展模块 ``pyarmor_runtime`` 第一次被导入的时候,会进行一些初始化工作,包括检查 :term:`运行密钥` 等,如果初始化失败,那么抛出异常退出。 43 | 44 | **装载加密模块** 45 | 46 | 如果初始化正常完成,那么执行加密脚本的第二行语句,调用从扩展模块中导入的函数 ``__pyarmor__`` 来完成对加密模块的装载工作。 47 | 48 | 把模块加载完成之后,就又把控制器交给 Python 解释器,执行解密后的模块。 49 | 50 | **装载加密函数** 51 | 52 | 当 Python 解释器调用加密函数的时候,控制权交给扩展模块 ``pyarmor_runtime`` 进行解密,解密完成之后返回 Python 解释器继续执行。 53 | 54 | 函数调用返回之前,控制权重新交给扩展模块 ``pyarmor_runtime`` 进行加密和做一些保护清理工作,最后在返回给 Python 解释器继续执行。 55 | 56 | 运行辅助包 57 | ========== 58 | 59 | 上面对加密脚本的说明中进行了简化,实际加密之后扩展模块 ``pyarmor_runtime`` 是在一个 :term:`运行辅助包` 里面,让我们查看一下加密后的目录就一目了然:: 60 | 61 | $ pyarmor gen foo.py 62 | 63 | $ ls dist/ 64 | 65 | foo.py pyarmor_runtime_000000 66 | 67 | $ ls dist/pyarmor_runtime_000000 68 | ... __init__.py 69 | ... pyarmor_runtime.so 70 | 71 | 这里 ``dist/pyarmor_runtime_000000`` 就是运行辅助包,使用运行辅助包主要是为了能够支持在多平台运行的加密脚本,我们可以把预编译的不同平台扩展模块 ``pyarmor_runtime`` 都存放到包目录下面,然后在 ``__init__.py`` 里面根据不同的平台,导入相应平台的扩展模块,这样就可以让加密脚本运行在多平台下面。 72 | 73 | 运行辅助包也是一个正常的 :term:`Python 包` ,它不是必须和加密脚本在一起,只要满足 Python 模块导入机制的要求,它可以存放在任何地方。请查看选项 :option:`--use-runtime` 和命令 :ref:`pyarmor gen runtime` 了解更多使用方法。 74 | 75 | 运行辅助包可以使用相对导入的方式,也可以使用绝对导入的方式。Pyarmor 提供了相关选项 :option:`-i` 和 :option:`--prefix` 来帮助加密脚本生成正确的导入语句。如果还不能满足需求,可以自己编写 :term:`加密插件` 来修改加密脚本的导入语句,从而确保能导入运行辅助包。 76 | 77 | 运行辅助包的名称可以被配置成为其他名称,请参考 :doc:`../tutorial/customization` 中的 `设置运行辅助包的名称` 78 | 79 | .. seealso:: :doc:`../tutorial/advanced` 中的 `使用公共的运行辅助包` 80 | 81 | 运行密钥 82 | ======== 83 | 84 | 运行密钥保存对加密脚本的约束信息以及相关的一些运行设置。 85 | 86 | 运行密钥一般嵌入到扩展模块中,但是也可以使用外部密钥文件。 87 | 88 | 扩展模块在初始化的时候要验证运行密钥,验证失败就直接报错退出,只要验证成功之后才会继续执行。 89 | 90 | 运行密钥的验证模式只是在扩展模块初始化的时候进行验证,但是可以配置成为每一次导入加密模块都进行验证,也可以配置成为定时进行验证。 91 | 92 | 如果使用外部密钥文件,可以在其头部插入任何可读文本作为注释,这样通过这些注释,不需要在加密脚本内部就可以读取运行密钥的相关信息,从而做一些额外的处理。 93 | 94 | 用户可以在运行密钥中绑定任何私有数据(但是长度有一定限制,不能超过 4K),然后使用 :term:`脚本补丁` 在加密脚本自己去验证这些数据,从而实现对加密脚本的限制和约束。这里所有的数据格式和业务逻辑全部由用户自己控制,Pyarmor 只是提供了这种扩展机制。 95 | 96 | .. seealso:: :ref:`pyarmor gen key` 97 | 98 | .. _restrict modes: 99 | 100 | 约束模式 101 | ======== 102 | 103 | 约束模式用来对加密脚本进行一定的约束和限制。 104 | 105 | 默认约束模式是不允许对加密后的脚本进行修改。 106 | 107 | 使用 :option:`--private` 之后不允许外部脚本访问加密脚本的属性和方法 108 | 109 | 使用 :option:`--restrict` 之后不允许外部脚本导入加密脚本,也不允许外部脚本访问加密脚本的属性和方法 110 | 111 | 如果需要禁用全部的约束,使用下面的命令:: 112 | 113 | $ pyarmor cfg restrict_module 0 114 | 115 | 一般情况下,是对某一个特殊脚本禁用约束,而其他脚本的约束不变,这样在配置的时候需要指定相应的模块:: 116 | 117 | $ pyarmor cfg -p NAME restrict_module 0 118 | 119 | .. _the differences of obfuscated scripts: 120 | 121 | 加密脚本和原来脚本的区别 122 | ======================== 123 | 124 | 加密脚本和原来的脚本相比,存在下列一些的不同: 125 | 126 | * 加密脚本是和 Python 版本绑定的,例如使用 Python 3.5 加密的脚本,只能使用 Python 3.5 去运行,而无法使用 Python 3.6 去运行,但是可以使用不同补丁的 3.5 版本。 127 | 128 | * 加密脚本是平台相关,因为使用到了动态库,不同的平台需要相应平台的动态库。平台根据操作系统,CPU 架构来进行区别,例如 32 位 X86 Windows,Linux Aarch64。 129 | 130 | * 执行加密角本的 Python 不能是调试版,准确的说,不能是设置了 Py_TRACE_REFS 或者 Py_DEBUG 生成的 Python 131 | 132 | * 使用 ``sys.settrace``, ``sys.setprofile``, ``threading.settrace`` 和 ``threading.setprofile`` 设置的回调函数在加密脚本中将被忽略,所以任何使用这些函数的工具无法正常工作。 133 | 134 | * 模块 ``inspect`` 和其他任何第三方包如果试图访问加密脚本的 Byte Code 或者直接访问代码对象的某些属性,也会崩溃,失败或者得到错误的数据 135 | 136 | * 使用 ``cPickle`` 或者其他序列化工具传递加密代码对象,传递之后的代码对象可能无法正常运行。 137 | 138 | * ``sys._getframe([n])`` 可能得到的不是期望的运行框架,因为加密脚本可能增加了额外的运行框架。 139 | 140 | * 加密脚本抛出异常中的行号和原来的脚本在个别情况下会不一样 141 | 142 | * 代码块的属性 ``__file__`` 在加密脚本是 ```` ,而不是文件名称,在异常信息中会看到文件名的显示是 ```` 143 | 144 | 需要注意的是模块的属性 ``__file__`` 还和原来的一样,还是文件名称。加密下面的脚本并运行,就可以看到输出结果的不同: 145 | 146 | .. code-block:: python 147 | 148 | def hello(msg): 149 | print(msg) 150 | 151 | # The output will be 'foo.py' 152 | print(__file__) 153 | 154 | # The output will be '' 155 | print(hello.__file__) 156 | 157 | 有些选项也会影响到脚本的内部结构: 158 | 159 | * ``pyarmor cfg mix_argname=1`` 会导致 annotations 无法使用 160 | 161 | * 直接使用 `importlib.util.spec_from_file_location` 导入加密模块需要额外处理,参考 `第846个问题报告`__ 162 | 163 | .. seealso:: 164 | 165 | :doc:`../how-to/third-party` 166 | 167 | :doc:`../tutorial/advanced` 中的 `生成跨平台加密脚本` 以及 `支持多个 Python 版本的加密脚本` 168 | 169 | __ https://github.com/dashingsoft/pyarmor/issues/846 170 | 171 | 第三方解释器的支持 172 | ------------------ 173 | 174 | 对于第三方的解释器(例如 Jython 等)以及通过嵌入 Python C/C++ 代码调用加密脚本,只要第三方解释器能够和 CPython :term:`扩展模块` 兼容,就可以使用加密脚本。请自行查看第三方解释器的文档,确认它是否支持 CPython 的扩展模块。 175 | 176 | 已知的一些问题 177 | 178 | * `PyPy` 无法运行加密脚本,因为它完全不同于 `CPython` 。 179 | 180 | * 在 Linux 下面 装载 Python 动态库 `libpythonXY.so` 的时候 `dlopen` 必须设置 `RTLD_GLOBAL` ,否则加密脚本无法运行。 181 | 182 | * Boost::python,默认装载 Python 动态库是没有设置 `RTLD_GLOAL` 的,运行加密脚本的时候会报错 "No PyCode_Type found" 。解决方法就是在初始化的调用方法 `sys.setdlopenflags(os.RTLD_GLOBAL)` ,这样就可以共享动态库输出的函数和变量。 183 | 184 | * 模块 `ctypes` 必须存在并且 `ctypes.pythonapi._handle` 必须被设置为 Python 动态库的句柄,PyArmor 会通过该句柄获取 Python C API 的地址。 185 | 186 | * WASM 目前不支持,因为这需要把运行库的代码也编译成为 WASM,但是 WASM 是很容易就被反编译成为原来的 C 代码,为了安全性,所以目前没有支持 WASM 的计划。如果有更多的用户提出这个需求,会考虑实现一个轻量级的运行库,只支持能够运行 RFT 模式的加密脚本,但是目前还没有开发计划。 187 | 188 | .. include:: ../_common_definitions.txt 189 | -------------------------------------------------------------------------------- /docs/topic/obfuscation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | 加密过程详解 3 | ============ 4 | 5 | .. highlight:: console 6 | 7 | .. program:: pyarmor gen 8 | 9 | 本文档适用于下列用户 10 | 11 | * 默认选项无法满足加密脚本的性能或者安全方面的需求 12 | * 加密脚本出现问题不知道错在那里 13 | 14 | 阅读本文档需要一定的 Python 基础,了解 Shell 脚本,环境变量等相关知识。 15 | 16 | 一般用户在操作系统中都一个用户目录,默认情况 pyarmor 运行过程的配置和数据文件会存放在这里。在命令行使用下面的命令查看配置目录的位置:: 17 | 18 | $ echo $HOME/.pyarmor 19 | 20 | 或者在命令行输入 python 命令,打开 Python 控制台,运行下面的语句:: 21 | 22 | >>> import os 23 | >>> print(os.path.expanduser("~/.pyarmor")) 24 | 25 | .. note:: 26 | 27 | 在本文档的示例代码中,以 `$` 开头的命令都是 Shell 命令,而以 `>>>` 开头的都是在 Python 控制台运行的脚本语句。 28 | 29 | 在本文档中,后面会使用 ``~/.pyarmor/`` 表示这个目录。 30 | 31 | 我们首先看下面是一个最简单的加密命令:: 32 | 33 | $ pyarmor gen foo.py 34 | 35 | 运行完成之后,加密脚本存放在目录 `dist/` 下面,输出的文件:: 36 | 37 | dist/ 38 | foo.py 39 | pyarmor_runtime.so 40 | 41 | 在这个过程中,pyarmor 都做了什么呢? 42 | 43 | 设置运行环境 44 | ============ 45 | 46 | pyarmor 首先设置运行环境,主要的步骤如下 47 | 48 | 1. 创建运行环境 ctx,根据全局配置文件进行初始化 49 | 2. 如果当前目录存在 ``.pyarmor/config`` 文件,使用其中的选项修改 ctx 50 | 3. 读取命令行选项,修改运行环境 ctx 51 | 52 | 搜索脚本 53 | ======== 54 | 55 | 接下来 pyarmor 根据命令行传入的文件或者目录,开始搜索需要加密的所有脚本。 56 | 57 | 配置项 ``pyexts`` 列出了需要加密的脚本扩展名,默认是 ``[.py, .pyw]`` 58 | 59 | 首先,命令行输入的脚本,即便扩展名不在 ``pyexts`` 之中,也会被增加到需要加密的脚本列表 ctx.resources 60 | 61 | 其次,命令行输入的目录下面,所有扩展名在 ``pyexts`` 之中的会被增加到列表 62 | 63 | 然后根据下面的两个配置项,决定下一步的搜索范围 64 | 65 | * 选项 :option:`--recursive` 递归搜索所有的子目录 66 | * 选项 ``--find-all`` 查找所有依赖的模块和包(尚未实现) 67 | 68 | 依赖脚本查找 69 | ============ 70 | 71 | **这个功能尚未实现** 72 | 73 | 当 ``--find-all`` 选项设定的时候,会自动查找加密脚本的依赖模块和包。 74 | 75 | pyarmor 首先设定搜索路径 76 | 77 | * 继承目前 Python 系统设置的搜索路径 78 | * 把配置项 ``pypaths`` 中指定的路径增加到搜索列表 79 | * 把当前目录增加到搜索列表 80 | 81 | 然后使用下面的算法查找脚本 `foo` 依赖模块: 82 | 83 | 1. 把脚本 foo.py 解析生成语法树 tree 84 | 2. 搜索树中所有的 import/from...import 语句 85 | 3. 把其中导入的模块作为依赖项 86 | 4. 根据模块名称和搜索路径,查找对应的模块文件 87 | 5. 把模块文件加入到需要加密的脚本列表 88 | 89 | 有一些隐含导入的模块无法通过上面的算法发现,这时候会使用脚本的规则文件来辅助查找。脚本的规则文件一般是人工创建,把依赖文件或者包名称添加到组 ``finder`` 下面,例如:: 90 | 91 | [finder] 92 | includes = a.py b/c d.pt 93 | 94 | 脚本规则文件的命名和存放路径必须符合下面的规则,否则无法起作用: 95 | 96 | * `~/.pyarmor/config/foo.rules` 存放在全局配置目录 rules 下面的同名文件 97 | * `.pyarmor/foo.rules` 当前配置目录下的扩展名为 .rules 的同名文件 98 | 99 | 如果规则文件存在,那么这里面指定的模块和文件都会被进行加密处理,并且如果指定了具体的文件名称,即便扩展名不在配置项 ``.pyexts`` 也会被增加到加密列表中。 100 | 101 | 源代码处理 102 | ========== 103 | 104 | 找到所有需要处理的脚本之后,下一步就是依次对每一个脚本的源代码进行处理 105 | 106 | 源代码的处理算法主要步骤 107 | 108 | * 读取文件内容,生成资源文件 `res` 109 | * 遍历环境配置中所有源代码处理器,依次处理资源文件 110 | * 处理完成之后把源代码编译成为语法树 111 | 112 | 文件的编码使用的是系统默认值,如果读取文件出现编码错误,修改配置文件的选项 ``encoding`` ,设定正确的值。 113 | 114 | 内联注释处理器 115 | -------------- 116 | 117 | 内置的源代码处理器是内联注释处理器,默认启用的。 118 | 119 | 内联注释处理器只是简单的把源代码中每一行包含的 ``# pyarmor:`` 字符串替换为空,这样可以使得原来被注释的代码生效。例如,原来的脚本如下 120 | 121 | .. code-block:: python 122 | :emphasize-lines: 1,4 123 | 124 | # pyarmor: print('inline pyarmor code') 125 | def foo(): 126 | i = 2 127 | # pyarmor: i += 1 128 | print('i is', i) 129 | 130 | 处理完之后,代码的内容就变成 131 | 132 | .. code-block:: python 133 | :emphasize-lines: 1,4 134 | 135 | print('inline pyarmor code') 136 | def foo(): 137 | i = 2 138 | i += 1 139 | print('i is', i) 140 | 141 | 这个的设计初衷是有时候需要执行一些只能在加密脚本中才能运行的代码,这样在没有加密的时候,调试起来很不方便。所以先把这部分的代码注释掉,在加密之前把注释自动去掉。 142 | 143 | 语法树处理 144 | ========== 145 | 146 | 源代码处理完成之后,输出的是源代码对应的 `ast.Module` 语法树。 147 | 148 | 语法树的处理算法主要步骤 149 | 150 | * 根据环境配置,找到所有的语法树处理器 151 | * 遍历所有语法树处理器,依次处理资源文件 152 | * 最后输出的是经过各个处理器修改后的语法树 153 | 154 | 为了节省内存,源代码处理完成,文件内容会被释放。如果语法树处理器需要从文件的源代码读取信息,需要重新打开文件读取。 155 | 156 | 每一个语法处理器接受的输入是上一个语法处理器的输出,所以后面的语法处理器处理的语法树可能和最初的源代码并不一致。 157 | 158 | 内置的语法树处理器主要有二个 159 | 160 | * 函数调用处理器,通过修改 ast.Call,确保调用的函数是经过加密的 161 | * 导入模块处理器,通过修改 ast.Import,确保导入的模块是经过加密的 162 | 163 | .. 164 | 函数调用处理器 165 | -------------- 166 | 167 | 函数调用处理器主要功能是通过修改 ast.Call,在运行时刻检查被调用的函数是否是加密后的函数,主要目的是为了防止运行过程中加密函数被替换。 168 | 169 | 函数调用处理器的基本算法如下 170 | 171 | * 遍历当前脚本的语法树,找到所有 ast.Import 中的模块 172 | * 读取当前脚本的规则表中那些隐藏的模块名称 173 | * 遍历当前脚本的语法树的 ast.Call 结点 174 | * 判断 ast.Call 是否调用的是加密模块 175 | * 如果是的话,修改当前结点,对调用进行保护 176 | 177 | 通过修改 ast.Call ,在调用之前检查被调用的函数是否加密,从而保护加密函数不能被黑客使用注入脚本等方式替换 178 | 179 | 模块中被调用的这些函数有的没有被加密,例如扩展模块中函数,系统库的函数。完全自动识别所有脚本中被调用的函数是加密的,还是没有被加密的,基本是不可能实现的。所以必须通过人工的规则配置,来达到最大限度的自动识别 180 | 181 | 函数调用如果不是使用名称,而是使用表达式的话,算法无法正确判断。这时候就需要人工配置模块的检查规则,来处理这些调用。 182 | 183 | 模块的规则文件说明参考上面的模块规则 ,调用检查器的规则添加到节 ``assert.call`` 下面:: 184 | 185 | [assert.call] 186 | includes = 187 | excludes = 188 | 189 | 脚本调用保护的规则定义在组 ``assert.call`` ,其基本的实现算法如下 190 | 191 | * 如果命令行选项 ``--assert-call`` 没有指定,那么不进行任何调用保护 192 | * 对于任何一个 ast.Call ,使用 ``includes`` 和 ``excludes`` 规则进行检查。只有所有 ``includes`` 规则返回真,并且所有 ``excludes`` 都返回假的 ast.Call 会进行保护 193 | 194 | 对于 includes 规则 195 | 196 | * 空表示自动确定那些调用需要保护, 197 | 198 | 首先是保护本模块定义的函数名称的调用 199 | 这样可能会存在导入其它模块的同名函数,这种情况可以使用 excludes 进行排除 200 | 201 | 其次检查导入的模块是否被加密,如果加密的会,也会保护那些可以确定是从加密模块导入的方法 202 | 对于这些例外,可以增加额外的规则到 includes 中 203 | 204 | * includes 包含多个规则时候,规则的运算是 or ,即任何一个为真,则返回真 205 | * includes 可以使用 True 表示任何调用都需要保护 206 | 207 | 对于 excludes 规则 208 | 209 | * 空表示返回 False 210 | * excludes 包含多个规则的时候,规则的运算是 or ,即任何一个为真,则返回 True 211 | 212 | 规则的格式如下:: 213 | 214 | [scope:]pattern 215 | 216 | 名称匹配,第一个字符表示匹配方式 217 | '+' 前缀 218 | '-' 后缀 219 | '*' 包含 220 | '/' 正则表达式,正则表达式不能包含 ":" 221 | '=' 相等 222 | 223 | 其它任意字符开始则表示 pattern 是空格分开的名称列表 224 | 225 | 由于函数检查是在运行时刻进行,对加密脚本性能有一定影响,默认是没有启用的。使用选项 :option:`--assert-call` 可以启用被调用函数的保护检查:: 226 | 227 | $ pyarmor gen --assert-call foo.py 228 | 229 | 导入模块处理器 230 | -------------- 231 | 232 | 导入模块处理器主要功能是通过修改 ast.Import,在运行时刻检查被导入的模块是否被加 233 | 密,主要目的是为了防止运行过程中加密模块被替换。 234 | 235 | 导入调用处理器的基本算法如下 236 | 237 | * 遍历当前脚本的语法树,找到所有 ast.Import 中的模块 238 | * 读取当前脚本的规则表中那些隐藏的模块名称 239 | * 遍历当前脚本的语法树的 ast.Import 结点 240 | * 判断 ast.Import 是否导入的是加密模块 241 | * 如果是的话,修改当前结点,对导入的模块进行检查 242 | 243 | 对于算法无法正确判断的模块导入,这时候就需要人工配置模块的检查规则,来处理这些调 244 | 用。导入模块处理器的规则添加到节 `[assert.import]` 下面:: 245 | 246 | [assert.import] 247 | enables = 248 | includes = [scope:]name[@lineno] 249 | excludes = 250 | 251 | 该选项默认没有启用,使用选项 `--assert-import` 可以启用调用导入模块处理器:: 252 | 253 | pyarmor gen --assert-import foo.py 254 | 255 | 模块代码处理 256 | ============ 257 | 258 | 所有的语法树处理器完成之后,输出的修改后的 `ast.Module` 语法树。接下来就是对模块代码进行处理:: 259 | 260 | * 根据环境配置,找到所有的模块代码处理器 261 | * 编译语法树为模块代码对象 mco 262 | * 遍历所有模块代码处理器,依次处理资源文件的模块代码对象 mco 263 | * 最后输出的是经过各个处理器修改后的模块代码对象 264 | 265 | 每一个模块代码处理器接受的输入是上一个模块代码处理器的输出,所以除了第一个代码处理器之外,后面的处理器输入的 mco 可能和语法树 mtree 不一致。 266 | 267 | 内置的模块代码处理器有二个 268 | 269 | * 重构代码处理器,主要实现自动重命名模块中类,函数,属性和变量的名称 270 | * 加密代码处理器,主要是对代码对象进行加密处理 271 | 272 | .. seealso:: :option:`--obf-code` :option:`--mix-str` :option:`--enable-rft` :option:`--enable-bcc` 273 | 274 | 加密脚本生成 275 | ============ 276 | 277 | 下列相关选项影响加密脚本中导入 :term:`运行辅助包` 的方式 278 | 279 | * :option:`--prefix` 280 | * :option:`-i` 281 | 282 | 同名脚本的输出路径 283 | ------------------ 284 | 285 | 如果存在同名脚本,例如:: 286 | 287 | pyarmor gen a/foo.py b/foo.py 288 | 289 | 默认输出分别是: 290 | 291 | * a/foo.py -> ``dist/foo.py`` 292 | * b/foo.py -> ``dist-1/foo.py`` 293 | 294 | 运行辅助文件生成 295 | ---------------- 296 | 297 | 选项 :option:`--enable-themida` 和 :option:`--platform` 决定了运行辅助文件中需要那些动态库。 298 | 299 | .. include:: ../_common_definitions.txt 300 | -------------------------------------------------------------------------------- /docs/topic/performance.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | 性能和安全 3 | ============ 4 | 5 | .. highlight:: console 6 | 7 | .. program:: pyarmor gen 8 | 9 | **关于安全性** 10 | 11 | Pyarmor 的核心功能是保护 Python 脚本无法被反编译,通过多种不可逆加密模式的实现,已经能够实现 Python 脚本无法使用任何方式完全反编译出来。 12 | 13 | 如果你看到有人宣称能够的破解 Pyarmor,请首先参考 :doc:`../how-to/security` ,使用你所可用的最高安全选项去加密一个简单的参考脚本,然后尝试使用网上所说的方法和工具进行破解。如果能够被破解,再把Python 版本,Pyarmor 的版本,运行的平台,加密使用的选项,参考脚本以及破解的方法发送到 |Contact| 14 | 15 | Pyarmor 并没有提供内存数据保护和很强的反调试保护,即便没有这些保护,Pyarmor 也能保证加密脚本无法被恢复成为原来的脚本。但是对于使用各种逆向工程方法直接修改内存运行数据,以及运行时刻的内存数据,并没有提供完整的保护机制。如何对这方面的要求很高,那就需要结合 Pyarmor 提供的功能以及使用其他反调试工具和技术一起来进行保护,详细说明请参考 :doc:`../how-to/protection` 16 | 17 | **关于性能** 18 | 19 | Pyarmor 提供了大量的选项来平衡性能和安全性,用户可以根据需要选择不同的选项。这里列出了所有相关的选项以及其对安全性和性能方面的影响。 20 | 21 | 需要注意的是不同的脚本,以及不同的使用场景,相同的选项对性能的影响可能有很大的不同。这里的所有性能测试数据都是基于同一个简单的测试脚本,如果对性能有很高的要求,请使用相应的场景和脚本进行测试,这里的结果不一定有参考价值。 22 | 23 | 在运行任何 Python 脚本之前,除非特别说明,都要清除脚本对应的 ``__pycache__`` 。这个目录存放脚本对应 ``.pyc`` ,如果这个目录存在,就意味着执行时间里面没有包含编译脚本的时间,但是编译脚本(.py->.pyc)是特别花费时间的,和函数的执行时间相比,不是一个数量级别。 24 | 25 | 测试脚本 ``benchmark.py`` 的内容如下 26 | 27 | .. code-block:: python 28 | 29 | import sys 30 | 31 | 32 | class BenTest(object): 33 | 34 | def __init__(self): 35 | self.a = 1 36 | self.b = "b" 37 | self.c = [] 38 | self.d = {} 39 | 40 | 41 | def foo(): 42 | ret = [] 43 | for i in range(100000): 44 | ret.extend(sys.version_info[:2]) 45 | ret.append(BenTest()) 46 | return len(ret) 47 | 48 | 49 | 主脚本 ``testben.py`` 的内容如下 50 | 51 | .. code-block:: python 52 | 53 | import benchmark 54 | import sys 55 | import time 56 | 57 | 58 | def metric(func): 59 | if not hasattr(time, 'process_time'): 60 | time.process_time = time.clock 61 | 62 | def wrap(*args, **kwargs): 63 | t1 = time.process_time() 64 | result = func(*args, **kwargs) 65 | t2 = time.process_time() 66 | print('%-16s: %10.3f ms' % (func.__name__, ((t2 - t1) * 1000))) 67 | return result 68 | return wrap 69 | 70 | 71 | def test_import(): 72 | t1 = time.process_time() 73 | import benchmark2 as m2 74 | t2 = time.process_time() 75 | print('%-16s: %10.3f ms' % ('test_import', ((t2 - t1) * 1000))) 76 | return m2 77 | 78 | 79 | @metric 80 | def test_foo(): 81 | benchmark.foo() 82 | 83 | 84 | if __name__ == '__main__': 85 | print('Python %s.%s' % sys.version_info[:2]) 86 | test_import() 87 | test_foo() 88 | 89 | **默认加密脚本的性能** 90 | 91 | 首先比较不同 Python 版本下的加密和不加密脚本的性能 92 | 93 | 使用下面的脚本,分别运行没有加密和加密的脚本两次,其中第二次运行的主要目的是测试存在 ``__pycache__`` 情况下的模块导入时间:: 94 | 95 | $ rm -rf dist __pycache__ 96 | 97 | $ cp benchmark.py benchmark2.py 98 | $ python testben.py 99 | 100 | Python 3.7 101 | test_import : 1.303 ms 102 | test_foo : 250.360 ms 103 | 104 | $ python testben.py 105 | 106 | Python 3.7 107 | test_import : 0.290 ms 108 | test_foo : 252.273 ms 109 | 110 | $ pyarmor gen testben.py benchmark.py benchmark2.py 111 | $ python dist/testben.py 112 | 113 | Python 3.7 114 | test_import : 0.907 ms 115 | test_foo : 311.076 ms 116 | 117 | $ python dist/testben.py 118 | 119 | Python 3.7 120 | test_import : 0.454 ms 121 | test_foo : 359.138 ms 122 | 123 | .. table:: 表-1. 不同 Python 版本的加密脚本性能测试 124 | :widths: auto 125 | 126 | ============== ========= ========= ========= ========= ========= ========= 127 | 时长(毫秒) 导入模块时间 导入模块时间2 执行函数时间 128 | -------------- -------------------- -------------------- -------------------- 129 | Python 版本 未加密 加密 未加密 加密 未加密 加密 130 | ============== ========= ========= ========= ========= ========= ========= 131 | 3.7 1.303 0.907 0.290 0.454 252.2 311.0 132 | 3.8 1.305 0.790 0.286 0.338 272.232 295.973 133 | 3.9 1.198 1.681 0.265 0.449 267.561 331.668 134 | 3.10 1.070 1.026 0.408 0.300 281.603 322.608 135 | 3.11 1.510 0.832 0.464 0.616 164.104 289.866 136 | ============== ========= ========= ========= ========= ========= ========= 137 | 138 | 可以看的出来,和之前的版本相比,关于执行函数的时间 Python 3.11 比加密后的脚本快了很多,可能和它的指令缓存和性能优化有关系。 139 | 140 | **RFT 模式性能** 141 | 142 | RFT 模式是直接把源代码的函数,类,方法和变量进行重命名,所以不应该会影响性能。这里我们比较的是加密后的脚本和 RFT 模式加密的脚本性能数据。下表中的数据是使用下面的测试脚本获得的:: 143 | 144 | $ rm -rf dist 145 | $ pyarmor gen testben.py benchmark.py benchmark2.py 146 | $ python dist/testben.py 147 | 148 | $ rm -rf dist 149 | $ pyarmor gen --enable-rft testben.py benchmark.py benchmark2.py 150 | $ python dist/testben.py 151 | 152 | .. table:: 表-2. 默认 RFT 模式性能测试 153 | :widths: auto 154 | 155 | ============== ========= ========= ========= ========= ================== 156 | 时长(毫秒) 导入模块时间 执行函数时间 备注 157 | -------------- -------------------- -------------------- ------------------ 158 | Python 版本 加密 RFT 模式 加密 RFT 模式 159 | ============== ========= ========= ========= ========= ================== 160 | 3.7 1.083 1.317 334.313 324.023 161 | 3.8 0.774 1.109 239.217 241.697 162 | 3.9 0.775 0.809 304.838 301.789 163 | 3.10 2.182 1.049 310.046 339.414 164 | 3.11 0.882 0.984 258.309 264.070 165 | ============== ========= ========= ========= ========= ================== 166 | 167 | 接下来,我们组合 RFT 模式和 :option:`--obf-code` 为 ``0`` 对脚本进行加密,然后和没有加密脚本的比较一下性能。下表中的数据是使用下面的测试脚本获得的:: 168 | 169 | $ rm -rf dist __pycache__ 170 | $ python testben.py 171 | 172 | $ pyarmor gen --enable-rft --obf-code=0 testben.py benchmark.py benchmark2.py 173 | $ python testben.py 174 | 175 | .. table:: 表-2.1 组合 RFT 模式性能测试 176 | :widths: auto 177 | 178 | ============== ========= ========= ========= ========= ================== 179 | 时长(毫秒) 导入模块时间 执行函数时间 备注 180 | -------------- -------------------- -------------------- ------------------ 181 | Python 版本 未加密 组合模式 未加密 组合模式 182 | ============== ========= ========= ========= ========= ================== 183 | 3.7 0.757 1.844 307.325 272.672 184 | 3.8 0.791 0.747 276.865 243.436 185 | 3.9 1.276 0.986 246.407 236.138 186 | 3.10 2.563 1.142 256.583 260.196 187 | 3.11 0.952 0.938 185.435 154.390 188 | ============== ========= ========= ========= ========= ================== 189 | 190 | **BCC 模式性能** 191 | 192 | BCC 模式是把部分函数转换成为 C 函数,应该模块装载时间略长一些,函数执行的时间能稍微快一些。下表中的数据是使用下面的测试脚本获得的:: 193 | 194 | $ rm -rf dist __pycache__ 195 | $ python testben.py 196 | $ python testben.py 197 | 198 | $ pyarmor gen --enable-bcc testben.py benchmark.py benchmark2.py 199 | $ python dist/testben.py 200 | $ python dist/testben.py 201 | 202 | .. table:: 表-3. 不同 Python 版本的 BCC 模式性能测试 203 | :widths: auto 204 | 205 | ============== ========= ========= ========= ========= ========= ========= 206 | 时长(毫秒) 导入模块时间 导入模块时间2 执行函数时间 207 | -------------- -------------------- -------------------- -------------------- 208 | Python 版本 未加密 加密 未加密 加密 未加密 加密 209 | ============== ========= ========= ========= ========= ========= ========= 210 | 3.7 1.086 1.177 0.342 0.391 344.640 271.426 211 | 3.8 1.099 1.397 0.351 0.400 291.244 251.520 212 | 3.9 1.229 1.076 0.538 0.362 306.594 254.458 213 | 3.10 1.267 0.999 0.255 0.796 302.398 247.154 214 | 3.11 1.146 1.056 0.273 0.536 206.311 189.582 215 | ============== ========= ========= ========= ========= ========= ========= 216 | 217 | **不同选项的性能** 218 | 219 | 使用不同选项的测试,为了便于比较,每一个选项尽可能的都是单独使用,除非特别情况必须组合使用。 220 | 221 | 例如,选项 :option:`--no-wrap` 的测试如下:: 222 | 223 | $ rm -rf dist __pycache__ 224 | $ pyarmor testben.py 225 | 226 | $ pyarmor gen --no-wrap testben.py benchmark.py benchmark2.py 227 | $ pyarmor dist/testben.py 228 | 229 | Python 3.7 230 | test_import : 0.971 ms 231 | test_foo : 306.261 ms 232 | 233 | .. list-table:: 表-4. 不同选项对性能和安全性的影响 234 | :header-rows: 1 235 | 236 | * - 选项 237 | - 性能影响 238 | - 安全性 239 | * - :option:`--no-wrap` 240 | - 增加性能 241 | - 降低安全性 242 | * - :option:`--obf-module` ``0`` 243 | - 对性能影响不大 244 | - 轻微降低安全性 245 | * - :option:`--obf-code` ``0`` 246 | - 显著增加性能 247 | - 显著降低安全性 248 | * - :option:`--obf-code` ``2`` 249 | - 降低性能 250 | - 显著增加安全性 251 | * - :option:`--enable-rft` 252 | - 基本不影响性能 253 | - 增强安全性 254 | * - :option:`--enable-themida` 255 | - 显著降低性能 256 | - 显著提高安全性,能有效防止反调试工具 257 | * - :option:`--mix-str` 258 | - 降低性能 259 | - 提高安全性,主要是保护字符串常量 260 | * - :option:`--assert-call` 261 | - 降低性能 262 | - 提高安全性,主要是防止注入和Monkey Trick 263 | * - :option:`--assert-import` 264 | - 轻微降低性能 265 | - 提高安全性,主要是防止注入和Monkey Trice 266 | * - :option:`--private` 267 | - 降低性能 268 | - 提高安全性,主要是防止模块属性被外部脚本读取 269 | * - :option:`--restrict` 270 | - 降低性能 271 | - 提高安全性,主要是防止模块属性被外部脚本读取 272 | 273 | .. include:: ../_common_definitions.txt 274 | -------------------------------------------------------------------------------- /docs/topic/repack.rst: -------------------------------------------------------------------------------- 1 | ========================== 2 | 详解可独立运行的加密脚本 3 | ========================== 4 | 5 | .. highlight:: console 6 | 7 | .. program:: pyarmor gen 8 | 9 | Pyarmor 的核心功能是加密脚本,加密完成之后生成可以独立运行的包完全是调用 PyInstaller_ 的相关功能。如果没有安装 PyInstaller_ 必须首先安装:: 10 | 11 | $ pip install pyinstaller 12 | 13 | PyInstaller_ 需要通过分析脚本源代码找到所有的依赖模块和包,并把这些依赖也自动的打到包里面。一旦加密之后, PyInstaller_ 就无法通过脚本找到依赖项目,所以直接打包加密脚本,执行的时候会导致模块找不到的问题。 14 | 15 | 为了解决这个问题,Pyarmor 8.0 提供了一个选项 :option:`--pack` ,通过对 PyInstaller_ 的打包过程进行一些特殊的处理,帮助 PyInstaller_ 正确找到加密脚本的所有依赖项目。 16 | 17 | 选项 :option:`--pack` 接受三种类型的值 18 | 19 | - `onefile` 打包成为单个可执行文件 20 | - `onedir` 打包到一个目录 21 | - specfile 以 ``.spec`` 为后缀的文件名称 22 | 23 | 其中前两者适用于对 PyInstaller_ 不太了解和熟悉的用户使用,这是 PyInstaller_ 提供的两种打包模式,单文件和单目录。 24 | 25 | 后者则是针对已经能够使用 PyInstaller_ 的 specfile 进行打包的用户 26 | 27 | 自动打包模式 28 | ============ 29 | 30 | 假设有一个这样的项目,其目录结构如下:: 31 | 32 | project/ 33 | ├── foo.py 34 | ├── foo.spec 35 | ├── util.py 36 | └── joker/ 37 | ├── __init__.py 38 | ├── card.py 39 | ├── queens.py 40 | └── config.json 41 | 42 | 我们使用下面的命令进行打包:: 43 | 44 | $ cd project 45 | $ pyarmor gen --pack onefile foo.py 46 | 47 | 那么,Pyarmor 是如何生成一个包含加密脚本的可执行文件呢? 48 | 49 | 1. Pyarmor 首先调用 PyInstaller_ ,让它分析没有加密脚本的 `foo.py` 依赖关系,并检查所有发现的依赖项目 50 | 2. Pyarmor 发现依赖模块 `util` 和包 `joker` 和脚本 `foo.py` 在相同的目录。然后 Pyarmor 使用命令行提供的加密选项自动加密 `foo.py` 以及 `util.py` 和包 `joker` ,并保存加密脚本到一个临时目录 `.pyarmor/pack/dist` 51 | 3. 对于没有和 `foo.py` 在相同目录下面的其他依赖模块和包,则添加到隐藏导入列表,这些一般都是 Python 的系统模块和包,不会自动加密 52 | 4. 最后 Pyarmor 再次调用 PyInstaller_ ,把临时目录 `.pyarmor/pack/dist` 下面的加密脚本,以及隐藏导入列表中的所有模块和包统统打包到一个可执行文件里面 53 | 54 | 现在,让我们运行一下最终输出的可执行文件 `dist/foo` 或者 `dist/foo.exe`:: 55 | 56 | $ ls dist/foo 57 | $ dist/foo 58 | 59 | 如果需要生成单个目录的包,只需要传递 `onedir` 给 :option:`--pack` 即可:: 60 | 61 | $ pyarmor gen --pack onedir foo.py 62 | $ ls dist/foo 63 | $ dist/foo/foo 64 | 65 | 使用 specfile 进行打包 66 | ---------------------- 67 | 68 | 在上面的示例项目中已经有一个 ``foo.spec`` 文件,可以打包没有加密的脚本为单个可执行文件,例如:: 69 | 70 | $ pyinstaller foo.spec 71 | $ dist/foo 72 | 73 | 在这种情况下,可以直接把 ``foo.spec`` 传递给 :option:`--pack` ,例如:: 74 | 75 | $ pyarmor gen --pack foo.spec -r foo.py util.py joker/ 76 | 77 | 那么,Pyarmor 是如何使用 specfile 打包加密脚本呢? 78 | 79 | 1. Pyarmor 首先根据命令行的加密选项对脚本进行加密,并保存到 `.pyarmor/pack/dist` 80 | 2. 然后根据 ``foo.spec`` 创建一个补丁文件 ``foo.patched.spec`` 81 | 3. 最后自动调用 PyInstaller_ ,使用 ``foo.patched.spec`` 来打包加密脚本 82 | 83 | 这个打过补丁的 specfile 可以在打包的过程中自动使用 `.pyarmor/pack/dist` 下面的加密脚本替换原来的脚本 84 | 85 | .. important:: 86 | 87 | 使用 specfile 打包的模式,无法自动加密依赖的脚本,需要人工在命令行指出需要加密的所有脚本,否则只有主脚本被加密。 88 | 89 | 检查打包的脚本是否被加密 90 | ------------------------ 91 | 92 | 在脚本 ``foo.py`` 或者 ``joker/__init__.py`` 增加一行 93 | 94 | .. code-block:: python 95 | 96 | print('this is __pyarmor__', __pyarmor__) 97 | 98 | 如果被加密了,那么可以正确打印出来。如果没有加密,就会抛出异常,因为只有加密脚本中才有内置名称 ``__pyarmor__`` 的定义。 99 | 100 | 使用其他 PyInstaller 选项 101 | ------------------------- 102 | 103 | 如果需要为应用程序增加图标,不显示控制台窗口等,只需要把 PyInstaller_ 的相关选项通过配置项 ``pack:pyi_options`` 传递给 Pyarmor 即可。 104 | 105 | 例如,使用 PyInstaller_ 选项 ``-w`` 不显示控制台窗口:: 106 | 107 | $ pyarmor cfg pack:pyi_options = " -w" 108 | 109 | 请注意 ``" -w"`` 的头部需要有一个额外空格,否则 shell 可能会报错 110 | 111 | 接下来我们添加另外一个选项 ``-i`` 设置图标,在选项 ``-i`` 和其值之间使用空格进行分隔,不要使用等号 ``=`` 。例如:: 112 | 113 | $ pyarmor cfg pack:pyi_options + " -i favion.ico" 114 | 115 | 在添加另外一个选项 ``--add-data``:: 116 | 117 | $ pyarmor cfg pack:pyi_options + "--add-data joker/config.json:joker" 118 | 119 | 在 Windows 系统下面,如果命令行出错,请使用 ``;`` 替换路径分隔符 ``:``:: 120 | 121 | C:/User/test> pyarmor cfg pack:pyi_options + "--add-data joker/config.json;joker" 122 | 123 | 上面的三个配置命令可以合并成为一条命令:: 124 | 125 | $ pyarmor cfg pack:pyi_options = " -w -i favion.ico --add-data joker/config.json:joker" 126 | 127 | .. seealso:: :ref:`pyarmor cfg` 128 | 129 | 使用更多的加密选项 130 | ------------------ 131 | 132 | 其他加密选项也都可以根据需要选用来增加安全性或者提高性能,例如:: 133 | 134 | $ pyarmor gen --pack onefile --private foo.py 135 | 136 | 例如,在 Darwin 系统,如果想让加密脚本能够同时工作在 Intel 和 Apple M1 框架下,可以传递额外的加密选项 ``--platform darwin.x86_64,darwin.arm64``:: 137 | 138 | $ pyarmor cfg pack:pyi_options = "--target-architecture universal2" 139 | $ pyarmor gen --pack onefile --platform darwin.x86_64,darwin.arm64 foo.py 140 | 141 | 需要注意的是不是所有的选项都可以和 :option:`--pack` 一起使用,例如,使用 :option:`--restrict` 选项会导致加密的包出现保护异常。 142 | 143 | 自己动手打包加密脚本 144 | ==================== 145 | 146 | 如果使用上面的方式出现问题,或者打包好的可执行文件出现执行错误,那么请使用下面的方式自己打包加密脚本。 147 | 148 | 这需要你了解 `如何使用 PyInstaller`__ 和 `如何使用 spec file`__ ,如果还不知道如何使用,请点击链接学习相关知识。 149 | 150 | __ https://pyinstaller.org/en/stable/usage.html 151 | __ https://pyinstaller.org/en/stable/spec-files.html 152 | 153 | 下面我们使用上面的例子来说明如何手动打包加密脚本 154 | 155 | * 首先使用 Pyarmor 加密这个脚本,把需要加密的脚本和目录全部在主脚本后面列出来,也可以使用其他加密选项,但是不要使用 :option:`-i` 或者 :option:`--prefix` [#]_:: 156 | 157 | $ cd project 158 | $ pyarmor gen -O obfdist -r foo.py util.py joker/ 159 | 160 | 检查加密脚本执行是否正常:: 161 | 162 | $ python obfdist/foo.py 163 | 164 | * 然后使用下面的命令生成生成 ``foo.spec`` [#]_:: 165 | 166 | $ pyi-makespec --onefile foo.py 167 | 168 | 检查是否可以正确打包:: 169 | 170 | $ pyinstaller foo.spec 171 | $ dist/foo 172 | 173 | * 接着修改 ``foo.spec`` ,插入下面的补丁代码到 ``pyz = PYZ`` 所在的行之前,这一步是重点 174 | 175 | .. code-block:: python 176 | 177 | # Pyarmor patch start: 178 | 179 | srcpath = '' 180 | obfpath = 'obfdist' 181 | 182 | def apply_pyarmor_patch(srcpath, obfpath): 183 | 184 | from PyInstaller.compat import is_win, is_cygwin 185 | extname = 'pyarmor_runtime' + ('.pyd' if is_win or is_cygwin else '.so') 186 | 187 | from glob import glob 188 | rtpkg = glob(os.path.join(obfpath, '*', extname)) 189 | if len(rtpkg) != 1: 190 | raise RuntimeError('No runtime package found') 191 | rtpkg = os.path.basename(os.path.dirname(rtpkg[0])) 192 | 193 | extpath = os.path.join(rtpkg, extname) 194 | 195 | if hasattr(a.pure, '_code_cache'): 196 | code_cache = a.pure._code_cache 197 | else: 198 | from PyInstaller.config import CONF 199 | code_cache = CONF['code_cache'].get(id(a.pure)) 200 | 201 | # Make sure both of them are absolute paths 202 | src = os.path.normcase(os.path.abspath(srcpath)) 203 | obf = os.path.abspath(obfpath) 204 | n = len(src) + 1 205 | 206 | count = 0 207 | for i in range(len(a.scripts)): 208 | if os.path.normcase(a.scripts[i][1]).startswith(src): 209 | x = os.path.join(obf, a.scripts[i][1][n:]) 210 | if os.path.exists(x): 211 | a.scripts[i] = a.scripts[i][0], x, a.scripts[i][2] 212 | count += 1 213 | if count == 0: 214 | raise RuntimeError('No obfuscated script found') 215 | 216 | for i in range(len(a.pure)): 217 | if os.path.normcase(a.pure[i][1]).startswith(src): 218 | x = os.path.join(obf, a.pure[i][1][n:]) 219 | if os.path.exists(x): 220 | code_cache.pop(a.pure[i][0], None) 221 | a.pure[i] = a.pure[i][0], x, a.pure[i][2] 222 | 223 | a.pure.append((rtpkg, os.path.join(obf, rtpkg, '__init__.py'), 'PYMODULE')) 224 | a.binaries.append((extpath, os.path.join(obf, extpath), 'EXTENSION')) 225 | 226 | apply_pyarmor_patch(srcpath, obfpath) 227 | 228 | # Pyarmor patch end. 229 | 230 | # Before this line 231 | # pyz = PYZ(...) 232 | 233 | * 最后直接使用打过补丁的 ``foo.spec`` 来打包,使用选项 ``--clean`` 避免补丁因为缓存的文件而失效:: 234 | 235 | $ pyinstaller --clean foo.spec 236 | 237 | 请根据你的具体情况,做如下修改 238 | 239 | * 使用实际目录设置 ``srcpath`` ,相对路径即可,在这个例子,是当前路径,直接设置为空字符串 240 | * 使用实际目录设置 ``obfpath`` ,相对路径即可,在这个例子中,加密目录是 ``obfdist`` 241 | 242 | **如何验证打包进去的是加密脚本** 243 | 244 | 在主脚本以及导入的其他包和模块增加一些调试语句进行判断,例如 245 | 246 | .. code-block:: python 247 | 248 | print('this is __pyarmor__', __pyarmor__) 249 | 250 | 如果不是加密脚本,这个语句会报错,只有在加密脚本中才能正常打印。 251 | 252 | .. rubric:: 备注 253 | 254 | .. [#] 选项 :option:`-i` 或者 :option:`--prefix` 会导致运行辅助包无法找到 255 | .. [#] 其他 `PyInstaller`_ 的选项也可以在这里使用 256 | 257 | 替换打包模式 258 | ============ 259 | 260 | .. deprecated:: 8.5.4 261 | 262 | 现在推荐使用选项 :option:`--pack` 的 ``onefile`` 或者 ``onedir`` 模式 263 | 264 | 首先调用 PyInstaller_ 将脚本打包成为单独的可执行文件或者打包到一个目录,然后把打包生成的可执行文件通过选项 :option:`--pack` 传递给 :ref:`pyarmor gen` 来实现:: 265 | 266 | $ pyinstaller foo.py 267 | $ pyarmor gen --pack dist/foo/foo foo.py 268 | 269 | Pyarmor 首先加密脚本,并把它们被存放到 :file:`.pyarmor/pack/dist` ,然后进行下面的额外处理: 270 | 271 | * 提取可执行文件中内容到一个临时目录 :file:`.pyarmor/pack/` 272 | * 使用加密脚本替换临时目录中同名的未加密脚本 273 | * 把加密脚本的 :term:`运行辅助文件` 增加到临时目录中 274 | * 根据把临时目录中所有内容重新生成可执行文件,并替换原来的可执行文件 275 | 276 | 需要注意的是,在这种方式下面,使用 PyInstaller 6.0+ 打包生成的可执行文件无法被正确处理,只能使用低版本 PyInstaller 进行打包。 277 | 278 | .. important:: 279 | 280 | 只有命令行列出的脚本会被加密,如果需要加密其他脚本或者子目录,在命令行列出它们。例如:: 281 | 282 | $ pyarmor gen --pack dist/foo/foo -r *.py dir1 dir2 ... 283 | 284 | Apple M1 的 segment fault 285 | ========================= 286 | 287 | 在 Apple M1 上打包,如果生产的加密脚本运行时候发生崩溃,请首先检查运行辅助包的签名:: 288 | 289 | $ codesign -v dist/foo/pyarmor_runtime_000000/pyarmor_runtime.so 290 | 291 | 如果签名非法,请重新进行签名:: 292 | 293 | $ codesign -f -s dist/foo/pyarmor_runtime_000000/pyarmor_runtime.so 294 | 295 | 如果使用了 :option:`--enable-bcc` 或者 :option:`--enable-jit` 进行加密,那么还需要启用 `Allow Execution of JIT-compiled Code Entitlement`__ 296 | 297 | .. seealso:: `Using the latest code signature format`__ 298 | 299 | __ https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_cs_allow-jit 300 | __ https://developer.apple.com/documentation/xcode/using-the-latest-code-signature-format/ 301 | 302 | .. include:: ../_common_definitions.txt 303 | -------------------------------------------------------------------------------- /docs/topic/rftmode.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | 深入了解 RFT 模式 3 | ================= 4 | 5 | .. highlight:: console 6 | 7 | .. program:: pyarmor gen 8 | 9 | Pyarmor 的 RFT 模式能够对模块中的函数,类,方法,属性,变量等进行重命名,相当于把源代码重新写了一下,所以这种加密方式不可逆的。 10 | 11 | 启用 RFT 模式之后,模块中的所有定义的名称都会发生变化,外部脚本将无法使用原来的名称进行导入。如果需要输出某些名称被外部脚本使用,需要在模块中定义 ``__all__`` ,这里面列出的名称会被保留。 12 | 13 | RFT 模式会修改下列名称 14 | 15 | * 函数 16 | * 类 17 | * 方法 18 | * 全局变量 19 | * 局部变量 20 | * 内置名称 21 | * 导入到名称 22 | 23 | RFT 模式不会修改的名称 24 | 25 | * 参数名称 26 | * 调用函数的时候使用的关键字名称 27 | * 使用模块属性 ``__all__`` 输出的所有名称 28 | * 所有以 ``__`` 开头的名称 29 | 30 | 使用 RFT 模式的时候,必须把所有使用的脚本和包在同一条命令进行加密。Pyarmor 会分析脚本之间的调用关系,对导入的名称也进行正确的重命名。例如:: 31 | 32 | src/ 33 | foo.py 34 | joker/ 35 | __init__.py 36 | card.py 37 | 38 | 使用 RFT 模式加密脚本 foo.py 以及包 joker 的命令如下:: 39 | 40 | pyarmor gen --enable-rft foo.py src/joker/ 41 | 42 | Pyarmor 使用内置的自动规则和人工配置的规则来分析脚本,并进行重命名。虽然 RFT 模式的自动规则可以处理大多数的情况,但是对于复杂的脚本和包,肯定会存在一些名称没有被正确处理。因为 Python 语言自身的特点,Pyarmor 并不认为可以找到算法自动处理所有的情况,所以对特殊情况,必须要进行人工配置规则。RFT 模式的目标是不断更新和完善自动规则,对于不能自动处理的部分,能够自动生成相关的参考配置,用户通过这些参考配置生成人工规则。 43 | 44 | 对于复杂的脚本,不要期望 Pyarmor 会自动进行处理所有情况。例如, 45 | 46 | .. code-block:: python 47 | 48 | foo().stack[2].count = 3 49 | (a+b).tostr().get() 50 | 51 | 是否对属性 ``stack`` , ``count`` , ``tostr`` 和 ``get`` 进行重命名呢?在有些情况下,使用静态的语法分析根本无法判断属性所在的变量类型,因为有些表达式的类型是动态执行的时候确定的,甚至同一个表达式会在不同的情况下返回不同的类型。 52 | 53 | 为了处理这种情况,RFT 模式提供了两种方法: 54 | 55 | - **rft-auto-exclude** 56 | 57 | 自动排除未知属性,这是默认方法。 58 | 59 | 这种方法是搜索所有的脚本中所有的属性链,如果属性所在的类型名称无法确定,那么把这个属性名称增加到排除属性表,所有被排除的属性在其他任何脚本中都不会被重命名。 60 | 61 | 排除属性表存放的文件名称是 ``.pyarmor/rft_exclude_table`` 62 | 63 | 第一次运行 RFT 模式加密脚本的时候,这个表是空的。RFT 模式依次扫描各个脚本,发现无法处理的属性,就把它添加到这个表中。当所有的脚本都加密完成之后,这个表里面的名称就是所有无法处理的属性名称。 64 | 65 | 这时候再次使用 RFT 模式使用相同的参数加密脚本,就可以保证所有的未知属性没有命名,基本不会出现名称绑定的错误。 66 | 67 | RFT 模式不会自动删除这个表 ``.pyarmor/rft_exclude_table`` ,而是不断增加新的未知属性到里面。如果有需要的话,可以把这个表删除掉,然后重新开始一个全新的重命名过程。 68 | 69 | 这种方法使用比较简单,但是可能会排除很多名称,查看排除属性表可以知道那些属性名称被保留下来了。 70 | 71 | - **rft-auto-include** 72 | 73 | 自动命名所有的类和方法。 74 | 75 | 这种方法是首先扫描全部脚本,把脚本中函数,类和方法全部都进行重命名,确保这些名称能被重命名。 76 | 77 | 对于那些属性链中无法处理的属性名称,保留下来不进行处理。 78 | 79 | 用户需要运行加密脚本,如果出现名称错误,那么把这些名称人工进行排除,或者使用人工规则进行排除。 80 | 81 | 不断重复上述步骤,直到没有名称错误出现。 82 | 83 | 这种方法能够重命名大部分的名称,但是需要更多额外的工作。 84 | 85 | 需要注意的是这种方式局部变量不会被重命名,但是它们会在后面的加密过程中被重命名,除非你使用命令 `pyarmor cfg mix_localnames=0` 禁止重命名局部变量。 86 | 87 | 启用 RFT 模式 88 | ============= 89 | 90 | 使用下面的命令启用 RFT mode:: 91 | 92 | $ pyarmor gen --enable-rft foo.py 93 | 94 | 也可以使用 :command:`pyarmor cfg` 通过配置文件启用:: 95 | 96 | $ pyarmor cfg enable_rft=1 97 | $ pyarmor gen foo.py 98 | 99 | 启用 **rft-auto-include** 方法通过禁用 ``rft_auto_exclude``:: 100 | 101 | $ pyarmor cfg rft_auto_exclude=0 102 | 103 | 重新启用 **rft-auto-exclude** 方法:: 104 | 105 | $ pyarmor cfg rft_auto_exclude=1 106 | 107 | 查看重命名之后的完整脚本 108 | ======================== 109 | 110 | 使用 RFT 模式修改后的脚本究竟能否满足安全需要?可能需要先看一下。 111 | 112 | 在 Python 3.9+ 启用 RFT 跟踪模式,可以输出相应的脚本:: 113 | 114 | $ pyarmor cfg trace_rft 1 115 | $ pyarmor gen --enable-rft foo.py 116 | $ ls .pyarmor/rft 117 | 118 | ``foo.py`` 转换后对应的脚本是 ``.pyarmor/rft/foo.py``:: 119 | 120 | $ cat .pyarmor/rft/foo.py 121 | 122 | 转换后的脚本表示的只是 RFT 模式所进行的修改,并不完全等价于加密后的脚本,它还会按照加密选项的设置进行下面的处理。例如 docstring 现在还保留着,但是如果使用了下面的配置项:: 123 | 124 | $ pyarmor cfg optimize 2 125 | 126 | 加密脚本最终会把所有 DocString 删除。 127 | 128 | .. note:: 129 | 130 | Python 3.8 以及之前的版本不支持该功能。 131 | 132 | 查看重命名的名称 133 | ================ 134 | 135 | 如果需要跟踪那些名称被重命名,可以同时启用日志跟踪和RFT 跟踪选项,这时候会生成跟踪日志 ``pyarmor.trace.log`` ,里面有所有的重命名日志:: 136 | 137 | $ pyarmor cfg enable_trace=1 trace_rft=1 138 | $ pyarmor gen --enable-rft foo.py 139 | $ grep trace.rft pyarmor.trace.log 140 | 141 | trace.rft foo:1 (import sys as pyarmor__1) 142 | trace.rft foo:12 (self.wScan->self.pyarmor__4) 143 | 144 | 第一个日志记录了 ``sys`` 被重命名为 ``pyarmor__1`` 145 | 146 | 第二条日志记录了属性 ``wScan`` 被重命名为 ``pyarmor__4`` 147 | 148 | 人工排除属性名称 149 | ================ 150 | 151 | 如果加密后的脚本出现名称错误,最简单的解决方式就是把这个名称人工排除,也就是说,在所有的脚本中保留这个名称(属性)而不进行重命名。下面的命令把名称 ``mouse_keybd`` 保留下来:: 152 | 153 | $ pyarmor cfg rft_excludes + "mouse_keybd" 154 | $ pyarmor gen --enable-rft foo.py 155 | 156 | 如果错误显示的名称是像 ``pyarmor__22`` 这样的格式,那么首先通过跟踪日志反向查找出原来的名称:: 157 | 158 | $ grep pyarmor__22 pyarmor.trace.log 159 | 160 | trace.rft foo:65 (self.height->self.pyarmor__22) 161 | trace.rft foo:81 (self.height->self.pyarmor__22) 162 | 163 | 例如,上例中 ``height`` 是 ``pyarmor__22`` 原来的名称,那么使用下面的命令保留这个名称:: 164 | 165 | $ pyarmor cfg rft_excludes + "height" 166 | $ pyarmor gen --enable-rft foo.py 167 | $ python dist/foo.py 168 | 169 | 如果需要把这些人工排除的名称清除,使用下面的命令:: 170 | 171 | $ pyarmor cfg rft_excludes "" 172 | 173 | 处理模块属性 ``__all__`` 174 | ======================== 175 | 176 | 模块属性 ``__all__`` 中定义的名称会被保留下来,可以输出名称供外部脚本使用。例如,下面的函数 ``foo`` 会被保留下来不进行重命名 177 | 178 | .. code-block:: python 179 | 180 | __all__ = ['foo'] 181 | 182 | def foo(msg): 183 | print(msg) 184 | 185 | def _private_foo(msg): 186 | print(msg) 187 | 188 | 如果输出的是一个类,那么类中的方法和属性也会被保留下来。 189 | 190 | 有时候可能需要重命名所有的类和方法,可以使用下面的选项让 RFT 模式忽略 ``__all__`` 中定义的名称:: 191 | 192 | $ pyarmor cfg rft_export__all__ 0 193 | 194 | 处理特殊的导入语句 195 | ================== 196 | 197 | 导入全部名称的语句 — ``from module import *`` — 会进行特殊的处理。 198 | 199 | 如果是从一个加密模块里面导入所有名称,那么 RFT 模式会直接解析源代码并直到属性 ``__all__`` 的定义。 200 | 201 | 如果是从一个外部模块中导入所有名称,那么 RFT 模式会尝试直接导入这个模块,并且尝试得到模式属性 ``__all__`` 。如果导入失败,会导致 RFT 模式报错退出。在这种情况下,需要设置环境变量 :envvar:`PYTHONPATH` 或者其他任何方式让 Python 可以导入这个模块。 202 | 203 | 如果模块属性 ``__all__`` 没有定义,那么模块的所有属性中不是以 ``_`` 开头的名称都会被导入进来。 204 | 205 | 自定义重命名规则 206 | ================ 207 | 208 | 自定义规则可用于重命名无法自动识别的类型的属性名称,自定义规则仅适用于 **rft-auto-include**:: 209 | 210 | $ pyarmor cfg rft_auto_exclude=0 211 | 212 | 定义全局重命名规则:: 213 | 214 | pyarmor cfg rft_rulers ^ "self.task.x self.task.?" 215 | 216 | 定义模块私有重命名规则:: 217 | 218 | pyarmor cfg -p joker.card rft_rulers "self.task.x self.task.?" 219 | 220 | 重命名规则格式:: 221 | 222 | 属性链模式 对应属性重命名方式 223 | 224 | 属性链模式是以 ``.`` 分开的 ``fnmatch`` 的模式字符串,然后使用此模式和属性链进行匹配 225 | 226 | 满足匹配的属性链按照后面的重命名方式进行处理 227 | 228 | - ``?``: 总是进行重命名 229 | - 其他值: 保留原来的名称 230 | 231 | 人工规则只用来修改属性名称,而忽略第一个名称 232 | 233 | 例如,下面的规则:: 234 | 235 | self.task.x self.task.? 236 | 237 | 应用于下面的脚本 238 | 239 | .. code-block:: python 240 | :linenos: 241 | :emphasize-lines: 8,9 242 | 243 | class Sdipmk: 244 | 245 | def __init__(self): 246 | self.width = 100 247 | self.height = 200 248 | 249 | def move(self, x, y, absolute=False): 250 | self.task.x = int(abs(x*65536/self.width)) if absolute else int(x) 251 | self.task.y = int(abs(y*65536/self.height)) if absolute else int(y) 252 | return Mouse(MS_MOVE, x, y) 253 | 254 | 使用下面的命令配置重命名规则:: 255 | 256 | $ pyarmor cfg rft_rulers "self.task.x self.task.?" 257 | 258 | 然后检查结果:: 259 | 260 | $ pyarmor gen --enable-rft foo.py 261 | $ grep trace.rft pyarmor.trace.log 262 | 263 | trace.rft foo:8 (self.task.x->self.task.pyarmor__2) 264 | 265 | 第 8 行的 ``self.task.x`` 被重命名为 ``self.task.pyarmor__2`` 266 | 267 | 让我们修改一下重命名规则,然后在看看结果:: 268 | 269 | $ pyarmor cfg rft_rulers "self.task.x self.?.?" 270 | $ grep trace.rft pyarmor.trace.log 271 | 272 | trace.rft foo:8 (self.task.x->self.pyarmor__1.pyarmor__2) 273 | 274 | 接下来增加一条新规则重命名 ``self.task.y`` ,注意使用 ``^`` 来增加规则:: 275 | 276 | $ pyarmor cfg rft_rulers ^"self.task.y self.?.?" 277 | $ grep trace.rft pyarmor.trace.log 278 | 279 | trace.rft foo:8 (self.task.x->self.pyarmor__1.pyarmor__2) 280 | trace.rft foo:9 (self.task.y->self.pyarmor__1.pyarmor__3) 281 | 282 | 这两条规则可以合并成为一条,这里使用 ``=`` 进行配置,会自动删除原来的所有规则:: 283 | 284 | $ pyarmor cfg rft_rulers = "self.task.* self.?.?" 285 | $ grep trace.rft pyarmor.trace.log 286 | 287 | trace.rft foo:8 (self.task.x->self.pyarmor__1.pyarmor__2) 288 | trace.rft foo:9 (self.task.y->self.pyarmor__1.pyarmor__3) 289 | 290 | .. 291 | 类型字典和变量类型 292 | ================== 293 | 294 | 模块,类在类型字典中定义,存放在 ``module_types``:: 295 | mod 296 | pkg 297 | pkg.mod 298 | pkg.mod.Cls 299 | pkg.mod[.Cls|.func].Cls 300 | 301 | 函数,变量和参数在变量字典中定义,存放在 ``variable_types``:: 302 | 303 | pkg.mod[.Cls|.func]*.[arg|var] 304 | 305 | 特殊的,Lambda 和 Comprehension 不会出现在 ``.func`` 中 306 | 307 | ``__init__.py`` 中的 ``__init__`` 在类型表和变量表中会被忽略 308 | 309 | 类型名称 Field.cls 的可选值:: 310 | 311 | int/float/str/Exception 等内置类型 312 | import 外部导入的名称 313 | module 模块类型 314 | class 类 315 | function 函数 316 | method ?方法,may be seem as function 317 | pkg.mod.Cls 自定义类型 318 | ? 不确定类型,这种类型的属性会加入到自动排除列表 319 | None 类型不存在,等价于不确定类型 320 | 321 | 类型为空表示需要重新确定的类型,只存在预处理的时候,扫描结束必须确定为以上列出的类型之一 322 | 323 | 如果需要输出名称,必须在模块中定义 ``__all__`` ,这里面列出的名称会被保留不变。需要注意的是这里列出的名称也可能是从别的模块中导入的。 324 | 325 | 两种不同的算法处理不可识别的变量类型: 326 | 327 | 1. 排除法 328 | 2. 包含法 329 | 330 | 第一种方法是所有不可识别类型的变量使用的属性,以及下一级的属性,全部设置为自动排除的名称,不进行重命名。 331 | 332 | 第二种方法是首先重命名每一个模块的类/函数和属性名称,如果不可识别的类型使用到已经重命名的字符串,那么,这些变量会被写入到配置文件,用户可以修改这些配置文件,然后重新运行重命名 333 | 334 | 第一步是分析所有模块的类型,生成 module_types 表,以及参数/变量类型表 variable_types 335 | 336 | 每一个模块和类都应该存在 module_types 337 | 每一个变量和参数都应该存在 variable_types 338 | 没有任何一个类型为空,不确定的类型设置成为 '?' 339 | 340 | 第二步是重构每一个脚本 341 | 342 | 相关选项和配置 343 | ============== 344 | 345 | 配置 ``rft_excludes`` 排除名称,列出的名称会被保留: 类,方法,函数,属性 346 | 配置 ``rft_rulers`` 人工规则,定义如何重命名属性链: x.y.z:?.%.! 347 | 348 | 名称引用 349 | -------- 350 | 351 | 使用模块名称和包名称来引用相关的脚本:: 352 | 353 | foo -> foo.py 354 | 355 | joker -> joker/__init__.py 356 | joker.card -> joker/card.py 357 | 358 | 属性链,用来匹配下列语句:: 359 | 360 | self.a.b = 1 ==> self.a.b 361 | self.a[2].b ==> self.a.b 362 | foo().a[2].b.c ==> foo.a.b.c 363 | (a+b).tostr() ==> ?.tostr 364 | (a+b).tostr().geta() ==> ?.tostr.geta 365 | 'a'.tolower().replace() ==> ?.tolower.replace 366 | b.a[3].k = 5 ==> b.a.k 367 | 368 | 使用 trace log:: 369 | 370 | pyarmor cfg enable_trace=1 trace_rft=1 371 | 372 | 查看 ``pyarmor.trace.log``:: 373 | 374 | trace.rft t1090:17 (exclude attrs "wintypes.DWORD") 375 | trace.rft t1090:32 (! self.dwFlags) 376 | trace.rft t1090:37 (wintypes.DWORD->pyarmor__27.DWORD) 377 | 378 | .. include:: ../_common_definitions.txt 379 | -------------------------------------------------------------------------------- /docs/tutorial/advanced.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 高级教程 3 | ========== 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | .. program:: pyarmor gen 13 | 14 | .. _using rftmode: 15 | 16 | 使用 RFT 模式加密脚本 :sup:`pro` 17 | ================================ 18 | 19 | :term:`RFT 模式` 是一种不可逆的加密模式,它相当于把源代码重新写了一遍,把其中的函数,类,方法和变量的名称进行了重命名。 20 | 21 | 使用选项 :option:`--enable-rft` 启用 RTF 模式 [#]_:: 22 | 23 | $ pyarmor gen --enable-rft foo.py 24 | 25 | 启用加密模式之后,这个脚本 26 | 27 | .. code-block:: python 28 | :linenos: 29 | 30 | import sys 31 | 32 | def sum2(a, b): 33 | return a + b 34 | 35 | def main(msg): 36 | a = 2 37 | b = 6 38 | c = sum2(a, b) 39 | print('%s + %s = %d' % (a, b, c)) 40 | 41 | if __name__ == '__main__': 42 | main('pass: %s' % data) 43 | 44 | 会被转换成为 45 | 46 | .. code-block:: python 47 | :linenos: 48 | 49 | pyarmor__17 = __assert_armored__(b'\x83\xda\x03sys') 50 | 51 | def pyarmor__22(a, b): 52 | return a + b 53 | 54 | def pyarmor__16(msg): 55 | pyarmor__23 = 2 56 | pyarmor__24 = 6 57 | pyarmor__25 = pyarmor__22(pyarmor__23, pyarmor__24) 58 | pyarmor__14('%s + %s = %d' % (pyarmor__23, pyarmor__24, pyarmor__25)) 59 | 60 | if __name__ == '__main__': 61 | pyarmor__16('pass: %s' % pyarmor__20) 62 | 63 | RFT 模式不会修改模块属性 ``__all__`` 中列出的名称,也不会修改函数所有的参数名称。例如 64 | 65 | .. code-block:: python 66 | :emphasize-lines: 3,5,9 67 | 68 | import re 69 | 70 | __all__ = ['make_scanner'] 71 | 72 | def py_make_scanner(context): 73 | parse_obj = context.parse_object 74 | parse_arr = context.parse_array 75 | 76 | make_scanner = py_make_scanner 77 | 78 | 上面的脚本会转换成为 79 | 80 | .. code-block:: python 81 | :emphasize-lines: 3,5,9 82 | 83 | pyarmor__3 = __assert_armored__(b'\x83e\x9d') 84 | 85 | __all__ = ['make_scanner'] 86 | 87 | def pyarmor__1(context): 88 | pyarmor__4 = context.parse_object 89 | pyarmor__5 = context.parse_array 90 | 91 | make_scanner = pyarmor__1 92 | 93 | 如果需要查看脚本转换的结果,可以使用下面的命令启用跟踪 RFT 模式 [#]_:: 94 | 95 | $ pyarmor cfg trace_rft=1 96 | $ pyarmor gen --enable-rft foo.py 97 | 98 | 启用这个功能之后转换后的脚本会保存到目录 ``.pyarmor/rft``:: 99 | 100 | $ cat .pyarmor/rft/foo.py 101 | 102 | 现在运行加密后的脚本:: 103 | 104 | $ python dist/foo.py 105 | 106 | 如果出现名称绑定的错误,可以尝试再次对其进行加密,这样可能会解决一些名称错误问题:: 107 | 108 | $ pyarmor gen --enable-rft foo.py 109 | $ python dist/foo.py 110 | 111 | 如果重新两次加密之后还是无法运行加密脚本,请参考 :doc:`../topic/rftmode` 来解决名称错误问题。 112 | 113 | .. [#] 这个功能仅在 :term:`Pyarmor 专家版` 中可用 114 | .. [#] 这个功能仅用于 Python 3.9+ 115 | 116 | .. _using bccmode: 117 | 118 | 使用 BCC 模式加密脚本 :sup:`pro` 119 | ================================ 120 | 121 | BCC 模式会把部分函数直接转换成为二进制代码,在根本上避免被还原成为 Python 函数。 122 | 123 | BCC 模式需要配置 :term:`C` 编译器,对于 `Linux` 和 `Darwin` 来说,一般不需要进行配置,只要默认的 ``gcc`` 和 ``clang`` 能工作就可以。在 `Windows` 环境下面,可以使用下面任意一种方式配置 ``clang.exe`` ,目前其它编译器还不支持: 124 | 125 | * 如果已经有 ``clang.exe`` ,只要在其它路径直接运行 ``clang.exe`` 不出错就可以。如果文件存在,但是无法在任意路径直接运行,可以配置环境变量 ``PYARMOR_CC`` 来指定这个文件,例如:: 126 | 127 | set PYARMOR_CC=C:\path\to\clang.exe 128 | 129 | * 从 `LLVM 官网 `_ 下载并安装预编译版本 130 | * 从 Pyarmor 官网下载 `clang-9.0.zip`__ ,压缩包大小约为 26M 左右,里面只有一个可执行文件,解压后存放在 :term:`根目录` 下面,默认是 ``%HOME%/.pyarmor`` 131 | 132 | __ https://pyarmor.dashingsoft.com/downloads/tools/clang-9.0.zip 133 | 134 | 配置好编译器之后,使用选项 :option:`--enable-bcc` 启用 BCC 模式 [#]_:: 135 | 136 | $ pyarmor gen --enable-bcc foo.py 137 | 138 | 模块级别的代码不会转换转换成为 :term:`C` 的函数,模块的任何函数如果使用了不被支持的特性,也不会转换成为 :term:`C` 函数,这么没有使用 BCC 模式加密的函数会根据选项使用其他方式进行加密。 139 | 140 | 为了查看那些函数被转换成为 :term:`C` 函数,使用下面的方式启用跟踪模式:: 141 | 142 | $ pyarmor cfg enable_trace=1 143 | $ pyarmor gen --enable-bcc foo.py 144 | 145 | 然后查看跟踪日志,日志中会显示那些转换的函数所在的脚本和行号:: 146 | 147 | $ ls pyarmor.trace.log 148 | $ grep trace.bcc pyarmor.trace.log 149 | 150 | trace.bcc foo:5:hello 151 | trace.bcc foo:9:sum2 152 | trace.bcc foo:12:main 153 | 154 | 运行加密后的脚本:: 155 | 156 | $ python dist/foo.py 157 | 158 | 如果加密脚本出错,可以根据错误信息里面报告的函数名称,那么通过设置让 BCC 模式不要把转换该函数。例如,如果运行的时候错误和函数 ``sum2`` 有关,那么不转换这个函数:: 159 | 160 | $ pyarmor cfg -p foo bcc:excludes "sum2" 161 | 162 | 选项 ``bcc:excludes`` 指定函数名称,选项 ``-p`` 指定模块名称。如果没有指定模块名称的话,其他所有模块中的同名函数也会被 BCC 模式忽略。增加更多的函数名称使用下面的命令格式:: 163 | 164 | $ pyarmor cfg -p foo bcc:excludes + "hello" 165 | 166 | 如果这样加密脚本还是不能运行,请参考 :doc:`../topic/bccmode` 来解决问题。 167 | 168 | .. [#] 这个功能仅在 :term:`Pyarmor 专家版` 中可用 169 | 170 | 定制错误处理方式 171 | ================ 172 | 173 | 当运行密钥已经过期,或者和当前设备不匹配,以及加密脚本保护异常,默认的处理方式是 抛出运行错误的异常,并显示错误信息:: 174 | 175 | $ pyarmor gen -e 2020-05-05 foo.py 176 | $ python dist/foo.py 177 | 178 | Traceback (most recent call last): 179 | File "dist/foo.py", line 2, in 180 | from pyarmor_runtime_000000 import __pyarmor__ 181 | File "dist/pyarmor_runtime_000000/__init__.py", line 2, in 182 | from .pyarmor_runtime import __pyarmor__ 183 | RuntimeError: this license key is expired (1:10937) 184 | 185 | 如果需要只显示错误信息,那么可以使用下面的方式进行配置:: 186 | 187 | $ pyarmor cfg on_error=1 188 | 189 | $ pyarmor gen -e 2020-05-05 foo.py 190 | $ python dist/foo.py 191 | 192 | this license key is expired (1:10937) 193 | 194 | 如果需要直接退出,不显示任何信息,可以进行下面的配置:: 195 | 196 | $ pyarmor cfg on_error=2 197 | 198 | $ pyarmor gen -e 2020-05-05 foo.py 199 | $ python dist/foo.py 200 | 201 | $ 202 | 203 | 可以使用下面的任意一个命令恢复默认处理方式:: 204 | 205 | $ pyarmor cfg on_error=0 206 | $ pyarmor cfg --reset on_error 207 | 208 | 这个设置仅仅影响的 Pyarmor 抛出的错误信息,不会影响脚本代码中本身抛出的异常 209 | 210 | .. note:: 211 | 212 | 如果加密脚本不是被 Python 直接执行,这个选项可能无法不显示错误直接退出。 213 | 214 | 过滤加密字符串 215 | ============== 216 | 217 | 默认情况下选项 :option:`--mix-str` 会加密脚本中所有长度大于 8 的字符串(但是不会加密任何 docstring)。 218 | 219 | 但是这样有时候可能会影响性能,如果需要的话,可以通过定制,只加密指定的敏感字符串。 220 | 221 | 如果只需要加密长度大于 10 的字符串,可以使用下面的设置:: 222 | 223 | $ pyarmor cfg mix.str:threshold 10 224 | 225 | 如果不需要加密两个字符串 ``__main__`` 和 ``xyz`` ,使用下面的命令进行过滤:: 226 | 227 | $ pyarmor cfg mix.str:excludes ^ "__main__ xyz" 228 | 229 | 如果不需要加密长度超过 1000 的字符串,可以使用格式为 ``/pattern/`` 的正则表达式来实现。例如:: 230 | 231 | $ pyarmor cfg mix.str:excludes ^ "/.{1000,}/" 232 | 233 | 重新恢复默认的过滤规则,使用下面的命令:: 234 | 235 | $ pyarmor cfg mix.str:excludes = "" 236 | 237 | 如果只需要加密长度在 8 和 32 之间的字符串,可以使用正则表达式和下面的命令:: 238 | 239 | $ pyarmor cfg mix.str:includes = "/.{8,32}/" 240 | 241 | 配置之后可以通过检查跟踪日志查看效果。 242 | 243 | 过滤需要保护的函数和模块 244 | ======================== 245 | 246 | 选项 :option:`--assert-call` 和 :option:`--assert-import` 能够对函数和模块进行保护,确保它们是被加密的,从而避免一些对加密脚本注入式的攻击。 247 | 248 | 但是有时候可能会做一些错误的判断,去保护一些没有加密的函数或者模块,这时候就需要增加一个过滤规则把这些没有加密的函数和模块排除出去。 249 | 250 | 例如,告诉 :option:`--assert-import` 忽略模块 ``json`` 和 ``inspect`` 使用下面的规则:: 251 | 252 | $ pyarmor cfg assert.import:excludes = "json inspect" 253 | 254 | 告诉 :option:`--assert-call` 忽略所有以 ``wintype_`` 开头的函数使用下面的规则:: 255 | 256 | $ pyarmor cfg assert.call:excludes "/wintype_.*/" 257 | 258 | 除了保护了不该保护的函数之外,另外一种错误是有些加密函数却没有被自动识别出来并进行保护,这就需要使用下面的方法进行修正。 259 | 260 | 使用内联标识符修正加密脚本 261 | ========================== 262 | 263 | 在加密脚本之前,Pyarmor 扫描每一行,如果发现该行有内联标识符 ``# pyarmor:`` , 那么会这一行进行额外处理:删除内联标识符和其后的一个空格,保留其后的所有内容。 264 | 265 | 例如,这个脚本经过处理之后 266 | 267 | .. code-block:: python 268 | :emphasize-lines: 3,4 269 | 270 | print('start ...') 271 | 272 | # pyarmor: print('this script is obfuscated') 273 | # pyarmor: check_something() 274 | 275 | 就会变成下面的样子 276 | 277 | .. code-block:: python 278 | :emphasize-lines: 3,4 279 | 280 | print('start ...') 281 | 282 | print('this script is obfuscated') 283 | check_something() 284 | 285 | 使用内联标识符修正加密脚本的一个实例就是使用 ``__assert_armored__`` 来保护额外的模块或者函数 286 | 287 | 默认情况下 :option:`--assert-import` 能够自动识别使用 ``import`` 语句导入的模块并进行保护,但是使用其他方法导入的模块,不会进行处理。例如,使用下面的方式导入的模块就不会进行处理 288 | 289 | .. code-block:: python 290 | 291 | m = __import__('abc') 292 | 293 | 如果我们需要确保模块 ``abc`` 是加密的,没有被替换的,可以使用加密脚本特有的内置函数 :func:`__assert_armored__` 来检查 ``m`` 294 | 295 | .. code-block:: python 296 | 297 | m = __import__('abc') 298 | __assert_armored__(m) 299 | 300 | 301 | 但是这样修改后的脚本存在一个问题是没有加密的时候,这个脚本无法正常运行,很不方便调试。因为函数 :func:`__assert_armored__` 只有在加密脚本中才存在。 302 | 303 | 内联标识符很好的解决了这个问题,我们对上面的脚本进行修正如下就可以完美解决这个问题 304 | 305 | .. code-block:: python 306 | :emphasize-lines: 2 307 | 308 | m = __import__('abc') 309 | # pyarmor: __assert_armored__(m) 310 | 311 | 同样,有时候 :option:`--assert-call` 也会遗漏一些需要保护的函数,这时候也可以使用内联标识符和 :func:`__assert_armored__` 进行人工保护。例如,下面的例子对函数 ``self.foo.meth`` 进行人工检查: 312 | 313 | .. code-block:: python 314 | :emphasize-lines: 2 315 | 316 | # pyarmor: __assert_armored__(self.foo.meth) 317 | self.foo.meth(x, y, z) 318 | 319 | 错误信息支持多语言 320 | ================== 321 | 322 | Pyarmor 提供错误信息的多语言功能,可以根据 :term:`客户设备` 的语言设置显示不同语言的错误信息。 323 | 324 | 为了支持多语言,首先创建 :file:`~/.pyarmor/messages.cfg` 325 | 326 | $ mkdir .pyarmor 327 | $ vi .pyarmor/messages.cfg 328 | 329 | 这是一个 ``.ini`` 格式的文件,增加一个节 ``runtime.message`` 和选项 ``languages``: 330 | 331 | .. code:: ini 332 | 333 | [runtime.message] 334 | 335 | languages = zh_CN zh_TW 336 | 337 | error_1 = invalid license 338 | error_2 = invalid license 339 | 340 | 这个例子中支持两种语言,其中语言代码和环境变量 :envvar:`LANG` 中的设置一样。错误信息我们只定制了前两个错误: 341 | 342 | * error_1: 许可证已经过期 343 | * error_2: 许可证不可用于当前设备 344 | 345 | 默认错误信息设置为 ``invalid license`` ,也就是说除了指定的语言之外,在其他语言环境都显示这个默认错误信息。 346 | 347 | 定制不同语言的错误消息分别使用两个节 ``runtime.message.zh_CN`` 和 ``runtime.message.zh_TW`` 348 | 349 | .. code:: ini 350 | 351 | [runtime.message] 352 | 353 | languages = zh_CN zh_TW 354 | 355 | error_1 = invalid license 356 | error_2 = invalid license 357 | 358 | [runtime.message.zh_CN] 359 | 360 | error_1 = 脚本超期 361 | error_2 = 未授权设备 362 | 363 | [runtime.message.zh_TW] 364 | 365 | error_1 = 腳本許可證已經過期 366 | error_2 = 腳本許可證不可用於當前設備 367 | 368 | 然后重新加密脚本:: 369 | 370 | $ pyarmor gen foo.py 371 | 372 | 当加密脚本运行的时候,它会检查当前设备的 :envvar:`LANG` 来设置默认的语言。如果当前环境的语言设置不是 ``zh_CN`` 或者 ``zh_TW`` ,那么使用默认的错误信息。 373 | 374 | 环境变量 :envvar:`PYARMOR_LANG` 可以用来指定加密脚本的使用的错误信息语言。如果这个环境变量设置,会忽略系统环境变量 :envvar:`LANG` 。 例如,使用下面的方式运行加密脚本将总是显示繁体中文的错误信息:: 375 | 376 | export PYARMOR_LANG=zh_TW 377 | python dist/foo.py 378 | 379 | .. _generating cross platform scripts: 380 | 381 | 生成跨平台加密脚本 382 | ================== 383 | 384 | .. versionadded:: 8.1 385 | 386 | 生成跨平台运行的加密脚本需要使用选项 :option:`--platform` 指定目标设备的平台名称,这里列出了所有支持的 :term:`运行平台` 名称。 387 | 388 | 例如,在一台 Darwin 开发设备上面生成可以运行在 Windows 下面的加密脚本,需要使用下面的命令:: 389 | 390 | $ pyarmor gen --platform windows.x86_64 foo.py 391 | 392 | Python 包 :mod:`pyarmor.cli.runtime` 提供了其他平台的预编译扩展模块,跨平台加密需要首先安装这个包,如果没有安装的话,会提示安装。 393 | 394 | 如果需要加密脚本可以运行在多个平台上,可以使用 :option:`--platform` 多次,指定每一个运行的平台。例如,下面的命令生成的加密脚本可以运行在多个 x86_64 的操作系统:: 395 | 396 | $ pyarmor gen --platform windows.x86_64 397 | --platform linux.x86_64 \ 398 | --platform darwin.x86_64 \ 399 | foo.py 400 | 401 | .. _support-multiple-python-versions: 402 | 403 | 支持多个 Python 版本的加密脚本 404 | ============================== 405 | 406 | .. versionadded:: 8.3 407 | 408 | 下面是加密一个脚本 `foo.py` 同时支持 Python 3.8 和 3.9 的基本步骤 409 | 410 | 首先要为每一个 Python 版本安装 Pyarmor:: 411 | 412 | $ python3.8 -m pip install pyarmor 413 | $ python3.9 -m pip install pyarmor 414 | 415 | 如果已经购买了 Pyarmor 的许可证,使用任意一个 Python 版本进行注册:: 416 | 417 | $ python3.8 -m pyarmor.cli reg pyarmor-regfile-xxxx.zip 418 | 419 | 同时需要启用内置插件 ``MultiPythonPlugin``:: 420 | 421 | $ python3.8 -m pyarmor.cli cfg plugins + "MultiPythonPlugin" 422 | 423 | 使用每一个 Python 版本分别加密脚本,保存在不同的目录:: 424 | 425 | $ python3.8 -m pyarmor.cli gen -O dist1 foo.py 426 | $ python3.9 -m pyarmor.cli gen -O dist2 foo.py 427 | 428 | 最后使用辅助脚本 ``merge.py`` 来合并两个输出目录:: 429 | 430 | $ python3.8 -m pyarmor.cli.merge -O dist dist1 dist2 431 | 432 | 最终输出的脚本在 ``dist``:: 433 | 434 | $ python3.8 dist/foo.py 435 | $ python3.9 dist/foo.py 436 | 437 | 使用公共的运行辅助包 438 | ==================== 439 | 440 | :term:`运行辅助包` 可以单独生成,并且在加密脚本的时候直接引用,这样就不需要在每次加密脚本的时候都生成 :term:`运行辅助包` 。 441 | 442 | 首先是使用命令 :ref:`pyarmor gen runtime` 生成共用的运行辅助包:: 443 | 444 | $ pyarmor gen runtime -O build/my_runtime1 445 | $ ls build/my_runtime1 446 | 447 | 然后在加密脚本的时候使用 :option:`--use-runtime` 引用这个运行辅助包:: 448 | 449 | $ pyarmor gen --use-runtime build/my_runtime1 foo.py 450 | 451 | 发布加密脚本的时候需要把 :term:`运行辅助包` 拷贝到加密脚本的输出路径 `dist`:: 452 | 453 | # 请把 pyarmor_runtime_000000 替换成为实际的名称 454 | $ ls build/my_runtime1/ 455 | $ cp -a build/my_runime1/pyarmor_runtime_000000 dist/ 456 | 457 | 如果需要生成支持多个平台的运行辅助包,使用下面的选项:: 458 | 459 | $ pyarmor gen --platform windows.x86_64,linux.x86_64 build/my_runtime3 460 | 461 | 如果使用 :term:`外部密钥` ,那么生成加密脚本和运行辅助包的时候都需要使用选项 :option:`--outer` 。例如:: 462 | 463 | $ pyarmor gen runtime --outer -O build/my_outer_runtime 464 | $ pyarmor gen --outer --use-runtime build/my_outer_runtime foo.py 465 | 466 | # 拷贝运行辅助包到加密脚本输出目录 467 | $ cp -a build/my_outer_runtime/pyarmor_runtime_000000 dist/ 468 | 469 | # 生成外部密钥 470 | $ pyarmor gen key -e .10 471 | $ mv dist/pyarmor.rkey dist/pyarmor_runtime_000000 472 | 473 | .. include:: ../_common_definitions.txt 474 | -------------------------------------------------------------------------------- /docs/tutorial/customization.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | 定制和扩展 3 | ============ 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | .. program:: pyarmor gen 13 | 14 | Pyarmor 使用下面的方式进行定制和扩展 15 | 16 | - 使用命令 :ref:`pyarmor cfg` 修改默认配置 17 | - 使用 :term:`加密插件` 对加密过程和输出文件进行扩展和定制 18 | - 使用 :term:`脚本补丁` 对运行时刻的加密脚本进行扩展和定制 19 | 20 | 设置运行辅助包的名称 21 | ==================== 22 | 23 | .. versionadded:: 8.2 [#]_ 24 | 25 | 默认情况下运行辅助包的名称是 ``pyarmor_runtime_xxxxxx`` 26 | 27 | 这个名称可以被配置成为任何合法的包名称。例如设定名称为 ``my_runtime``:: 28 | 29 | pyarmor cfg package_name_format "my_runtime" 30 | 31 | .. [#] 试用版本不可以修改运行辅助包的名称,修改后的加密脚本无法运行 32 | 33 | 自定义需要保护的函数和模块 34 | ========================== 35 | 36 | .. versionadded:: 8.2 37 | 38 | Pyarmor 8.2 新增加一个配置项 ``auto_mode`` 用来实现自定义需要保护的函数和模块,它的默认值为 ``and`` ,这时候过滤方式和以前的版本是一样的。 ``and`` 的含义是所有的操作对象除了是自动识别之外,还必须满足 ``includes`` 和 ``excludes`` 条件。 39 | 40 | 如果修改其值为 ``or`` ,则表示除了自动识别的函数和模块之外,还需要保护 ``includes`` 里面的函数。例如,下面的命令,,除了保护自动识别的函数之外,还额外保护函数 ``foo`` 和 ``koo``:: 41 | 42 | $ pyarmor cfg ast.call:auto_mode "or" 43 | $ pyarmor cfg ast.call:includes "foo koo" 44 | 45 | $ pyarmor gen --assert-call foo.py 46 | 47 | 下面的命令可以用来保护没有使用 ``import`` 语句直接导入的加密模块 ``joker.card``:: 48 | 49 | $ pyarmor cfg ast.import:auto_mode "or" 50 | $ pyarmor cfg ast.import:includes "joker.card" 51 | 52 | $ pyarmor gen --assert-import joker/ 53 | 54 | 使用加密插件修正运行辅助包的依赖项 55 | ================================== 56 | 57 | .. versionadded:: 8.2 58 | 59 | 在使用 Dawin 的设备中,如果 Python 没有安装在标准路径,那么运行加密脚本的时候可能会因为找不到依赖的 Python 动态库而出现装载错误。 60 | 61 | 如果需要运行加密脚本的环境在这种设备,那么在加密脚本的时候需要修正运行辅助包的依赖库位置。 62 | 63 | 首先查看一些运行辅助包中动态库 ``pyarmor_runtime.so`` 的依赖项:: 64 | 65 | $ otool -L dist/pyarmor_runtime_000000/pyarmor_runtime.so 66 | 67 | dist/pyarmor_runtime_000000/pyarmor_runtime.so: 68 | 69 | pyarmor_runtime.so (compatibility version 0.0.0, current version 1.0.0) 70 | ... 71 | @rpath/lib/libpython3.9.dylib (compatibility version 3.9.0, current version 3.9.0) 72 | ... 73 | 74 | 如果 :term:`客户设备` 上面没有 ``@rpath/lib/libpython3.9.dylib`` ,而是 ``@rpath/lib/libpython3.9.so`` ,那么加密脚本无法被装载。 75 | 76 | 这时候可以通过插件来修正这个问题,首先创建一个插件脚本 :file:`.pyarmor/conda.py`: 77 | 78 | .. code-block:: python 79 | 80 | __all__ = ['CondaPlugin'] 81 | 82 | class CondaPlugin: 83 | 84 | def _fixup(self, target): 85 | from subprocess import check_call 86 | check_call('install_name_tool -change @rpath/lib/libpython3.9.dylib @rpath/lib/libpython3.9.so %s' % target) 87 | check_call('codesign -f -s - %s' % target) 88 | 89 | @staticmethod 90 | def post_runtime(ctx, source, target, platform): 91 | if platform.startswith('darwin.'): 92 | print('using install_name_tool to fix %s' % target) 93 | self._fixup(target) 94 | 95 | 启用这个插件脚本,然后重新加密脚本:: 96 | 97 | $ pyarmor cfg plugins + "conda" 98 | $ pyarmor gen foo.py 99 | 100 | 请根据具体的环境修改上面的插件脚本以满足需要。 101 | 102 | .. seealso:: :ref:`plugins` 103 | 104 | 使用脚本补丁绑定脚本到 Docker 105 | ============================= 106 | 107 | .. versionadded:: 8.2 108 | 109 | 假设我们要把脚本 ``app.py`` 绑定运行在两个 Docker 上面,它们的 id 分别是 ``docker-a1`` , ``docker-b2`` 110 | 111 | 那么,首先创建一个 :term:`脚本补丁` ``.pyarmor/hooks/app.py`` 112 | 113 | .. code-block:: python 114 | 115 | def _pyarmor_check_docker(): 116 | cid = None 117 | with open("/proc/self/cgroup") as f: 118 | for line in f: 119 | if line.split(':', 2)[1] == 'name=systemd': 120 | cid = line.strip().split('/')[-1] 121 | break 122 | 123 | docker_ids = __pyarmor__(0, None, b'keyinfo', 1).decode('utf-8') 124 | if cid is None or cid not in docker_ids.split(','): 125 | raise RuntimeError('license is not for this machine') 126 | 127 | _pyarmor_check_docker() 128 | 129 | 然后加密脚本,同时把 Docker 的信息存储到 :term:`运行密钥` 中:: 130 | 131 | $ pyarmor gen --bind-data "docker-a1,docker-b2" app.py 132 | 133 | 运行加密脚本以验证其效果,可以增加一些 print 语句在脚本补丁中进行调试。 134 | 135 | .. seealso:: :ref:`hooks` :func:`__pyarmor__` 136 | 137 | 使用其他网络时间服务来检查脚本有效期 138 | ==================================== 139 | 140 | .. versionadded:: 8.2 141 | 142 | 默认情况下 Pyarmor 是请求 NTP 服务器来验证加密脚本的有效期,如果 NTP 端口没有开放,也可以通过 :term:`脚本补丁` 使用其他网络时间服务器来进行验证。 143 | 144 | 首先创建脚本补丁 ``.pyarmor/hooks/foo.py`` 145 | 146 | .. code-block:: python 147 | 148 | def _pyarmor_check_worldtime(host, path): 149 | from http.client import HTTPSConnection 150 | expired = __pyarmor__(1, None, b'keyinfo', 1) 151 | conn = HTTPSConnection(host) 152 | conn.request("GET", path) 153 | res = conn.getresponse() 154 | if res.code == 200: 155 | data = res.read() 156 | s = data.find(b'"unixtime":') 157 | n = data.find(b',', s) 158 | current = int(data[s+11:n]) 159 | if current > expire: 160 | raise RuntimeError('license is expired') 161 | else: 162 | raise RuntimeError('got network time failed') 163 | _pyarmor_check_worldtime('worldtimeapi.org', '/api/timezone/Europe/Paris') 164 | 165 | 然后加密脚本,有效期的设置使用本地时间:: 166 | 167 | $ pyarmor gen -e .30 foo.py 168 | 169 | 这样就可以使用定制的代码检查网络时间。 170 | 171 | .. seealso:: :ref:`hooks` :func:`__pyarmor__` 172 | 173 | 保护运行辅助模块 174 | ================ 175 | 176 | .. versionadded:: 8.2 177 | 178 | 下面的例子说明如何检查运行辅助模块 ``pyarmor_runtime.so`` 的文件内容来确保其没有被修改 179 | 180 | 首先创建一个补丁脚本 :file:`.pyarmor/hooks/foo.py`: 181 | 182 | .. code-block:: python 183 | :linenos: 184 | :emphasize-lines: 7 185 | 186 | def check_pyarmor_runtime(value): 187 | from pyarmor_runtime_000000 import pyarmor_runtime 188 | with open(pyarmor_runtime.__file__, 'rb') as f: 189 | if sum(bytearray(f.read())) != value: 190 | raise RuntimeError('unexpected %s' % filename) 191 | 192 | check_pyarmor_runtime(EXCEPTED_VALUE) 193 | 194 | 第 7 行的 ``EXCEPTED_VALUE`` 需要被替换成为实际值,但是这里存在一个问题。每一次加密之后运行辅助模块 ``pyarmor_runtime.so`` 是不同的,所以必须在生成运行辅助模块的同时得到其文件字节总和。这个我们可以通过加密插件来实现,在生成辅助文件之后,自动计算字节总和,然后修改补丁脚本 195 | 196 | .. code-block:: python 197 | 198 | # Plugin script: .pyarmor/myplugin.py 199 | 200 | __all__ = ['RuntimePlugin', 'CondaPlugin'] 201 | 202 | class RuntimePlugin: 203 | 204 | @staticmethod 205 | def post_runtime(ctx, source, target, platform): 206 | with open(target, 'rb') as f: 207 | value = sum(bytearray(f.read())) 208 | with open('.pyarmor/hooks/foo.py', 'r') as f: 209 | source = f.read() 210 | source = source.replace('EXPECTED_VALUE', str(value)) 211 | with open('.pyarmor/hooks/foo.py', 'r') as f: 212 | f.write(source) 213 | 214 | class CondaPlugin: 215 | ... 216 | 217 | 然后启用这个插件:: 218 | 219 | $ pyarmor cfg plugins + "myplugin" 220 | 221 | 最后生成加密脚本,并进行验证:: 222 | 223 | $ pyarmor gen foo.py 224 | $ python dist/foo.py 225 | 226 | 这个例子只是演示如何去做,并不能在实际项目中使用。任何公开源码的检查方式一般都可以找到相应的方法绕过,所以请编写自己私有的检查脚本,这样才能真正的提高安全性。 227 | 228 | .. seealso:: :ref:`hooks` 229 | 230 | 在外部密钥中增加注释 231 | ==================== 232 | 233 | .. versionadded:: 8.2 234 | 235 | 加密脚本检查 :term:`外部密钥` 文件的时候会忽略头部的任何可打印的字符,所以可以在外部密钥文件的开始增加注释,来对这个密钥进行备注说明。 236 | 237 | Pyarmor 提供了加密插件可以用来对外部密钥添加注释,下面这个例子会把所有的绑定信息打印到屏幕,并且把有效期写入到外部密钥中: 238 | 239 | .. code-block:: python 240 | 241 | # Plugin script: .pyarmor/myplugin.py 242 | 243 | from datetime import datetime 244 | 245 | __all__ = ['CommentPlugin'] 246 | 247 | class CommentPlugin: 248 | 249 | @staticmethod 250 | def post_key(ctx, keyfile, **keyinfo): 251 | expired = None 252 | for name, value in keyinfo.items(): 253 | print(name, value) 254 | if name == 'expired': 255 | expired = datetime.fromtimestamp(value).isoformat() 256 | 257 | if expired: 258 | print('patching runtime key') 259 | comment = '# expired date: %s\n' % expired 260 | with open(keyfile, 'rb') as f: 261 | keydata = f.read() 262 | with open(keyfile, 'wb') as f: 263 | f.write(comment.encode()) 264 | f.write(keydata) 265 | 266 | 启用这个插件,然后生成一个外部密钥:: 267 | 268 | $ pyarmor cfg plugins + "myplugin" 269 | $ pyarmor gen key -e 2023-05-06 270 | 271 | 查看外部密钥中的注释:: 272 | 273 | $ head -n 1 dist/pyarmor.rkey 274 | 275 | .. seealso:: :ref:`plugins` 276 | 277 | .. include:: ../_common_definitions.txt 278 | -------------------------------------------------------------------------------- /docs/tutorial/getting-started.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 入门教程 3 | ========== 4 | 5 | .. highlight:: console 6 | 7 | .. contents:: 内容 8 | :depth: 2 9 | :local: 10 | :backlinks: top 11 | 12 | Pyarmor 是什么 13 | ============== 14 | 15 | |Pyarmor| 是一个用于加密和保护 |Python| 脚本的工具。它能够在运行时刻保护 |Python| 脚本代码不被泄露,设置加密后脚本的使用期限,绑定加密脚本到硬盘、网卡等硬件设备。 16 | 17 | 功能特点: 18 | 19 | - **无缝替换**: 加密后的脚本依然是一个有效的 `.py` 文件,在大多数情况下可以直接替换原来的 `.py` 脚本,而不影响脚本的使用。 20 | - **均衡加密**: 提供了丰富的加密选项来平衡安全性和性能,能够满足大多数应用对安全性和性能的要求。 21 | - **不可逆加密**: 能够直接重命名源代码中的函数,类,方法,变量和参数。 22 | - **转换成为 C 代码**: 能够把模块中部分函数转换成为 C 代码,然后使用高优化选项直接编译 C 代码为机器指令来保护 Python 函数 23 | - **限制加密脚本的使用范围**: 可以绑定加密脚本到指定的设备或者设置加密脚本的有效期 24 | - **Themida 保护**: 使用 Themida 保护加密脚本(仅 Windows 平台可用) 25 | 26 | 安装 27 | ==== 28 | 29 | Pyarmor_ 是一个发布在 PyPI_ 的 Python 包,最方便的方式就是直接使用命令 :command:`pip` 进行安装。 30 | 31 | 在 Linux 或者 Apple 平台,直接打开终端,然后执行下面的命令:: 32 | 33 | $ pip install -U pyarmor 34 | 35 | 在 Windows 下面,使用 :kbd:`Win-r` 弹出命令输入框,输入 :command:`cmd` 之后回车打开控制台,在控制台输入安装命令: 36 | 37 | .. code-block:: doscon 38 | 39 | C:\> pip install -U pyarmor 40 | 41 | 安装完成之后,输入命令 :command:`pyarmor --version` 并回车执行。如果安装成功,会显示出 Pyarmor 的版本信息。 42 | 43 | 不是所有的平台都被 Pyarmor 支持,所有支持的平台和架构请查看 :doc:`../reference/environments` 44 | 45 | 加密脚本 46 | ======== 47 | 48 | .. program:: pyarmor gen 49 | 50 | 下面是最简单的加密命令,用来加密一个脚本 :file:`foo.py`:: 51 | 52 | $ pyarmor gen foo.py 53 | 54 | 子命令 ``gen`` 能够被替换成为 ``g`` 或者 ``generate``:: 55 | 56 | $ pyarmor g foo.py 57 | $ pyarmor generate foo.py 58 | 59 | 这个命令会生成一个加密脚本 :file:`dist/foo.py` ,这也是一个正常的 Python 脚本,可以直接使用 Python 解释器执行:: 60 | 61 | $ python dist/foo.py 62 | 63 | 查看所有生成的文件:: 64 | 65 | $ ls dist/ 66 | ... foo.py 67 | ... pyarmor_runtime_000000 68 | 69 | 除了加密脚本之外,可以看到还有另外一个目录 :file:`pyarmor_runtime_000000` ,这是运行加密脚本所依赖的一个 :term:`Python 包` 。 70 | 71 | 发布加密脚本 72 | ============ 73 | 74 | 只拷贝加密脚本 :file:`dist/foo.py` 本身到 :term:`客户设备` 是无法运行的,必须把输出目录下面的 :term:`运行辅助包` 一起拷贝过去才可以。 75 | 76 | 为什么呢?看一下加密脚本 :file:`dist/foo.py` 的内容就明白了: 77 | 78 | .. code-block:: python 79 | 80 | from pyarmor_runtime_000000 import __pyarmor__ 81 | __pyarmor__(__name__, __file__, ...) 82 | 83 | 加密脚本需要从 ``pyarmor_runtime_000000`` 导入函数 ``__pyarmor__`` ,这个包也是加密脚本的依赖包,加密脚本可以被当作一个正常脚本和依赖包 :mod:`pyarmor_runtime_000000` 来使用。 84 | 85 | .. important:: 86 | 87 | 因为依赖包 :mod:`pyarmor_runtime_000000` 包含使 :term:`扩展模块` ,所以加密脚本只能在相同系统,使用相同版本的 Python 才能运行。如果 :term:`客户设备` 的运行环境不一样,需要使用其他跨平台加密选项。 88 | 89 | .. note:: 90 | 91 | 不需要安装 Pyarmor 到 :term:`客户设备` ,运行加密脚本不需要 Pyarmor 92 | 93 | 加密包 94 | ====== 95 | 96 | 现在来加密一个包,使用选项 :option:`-O` 设置另外一个输出目录 :file:`dist2`:: 97 | 98 | $ pyarmor gen -O dist2 src/mypkg 99 | 100 | 查看加密结果:: 101 | 102 | $ ls dist2/ 103 | ... mypkg 104 | ... pyarmor_runtime_000000 105 | 106 | $ ls dist2/mypkg/ 107 | ... __init__.py 108 | 109 | 测试一下导入加密后的包 :file:`dist2/mypkg`:: 110 | 111 | $ cd dist2/ 112 | $ python -C 'import mypkg' 113 | 114 | 如果包里面还有其他子目录需要加密,那么使用选项 :option:`-r` 来启用递归搜索模式:: 115 | 116 | $ pyarmor gen -O dist2 -r src/mypkg 117 | 118 | 发布加密包 119 | ========== 120 | 121 | 虽然可以把整个目录 :file:`dist2` 直接拷贝到 :term:`客户设备` ,但是还有一种更好的方式,使用选项 :option:`-i` 把运行辅助包保存到包目录内部:: 122 | 123 | $ pyarmor gen -O dist3 -r -i src/mypkg 124 | 125 | 查看输出目录:: 126 | 127 | $ ls dist3/ 128 | ... mypkg 129 | 130 | $ ls dist3/mypkg/ 131 | ... __init__.py 132 | ... pyarmor_runtime_000000 133 | 134 | 现在所有需要拷贝的文件都在加密包 :file:`dist3/mypkg` 内部,只需要整个包目录拷贝到 :term:`客户设备` 上面就可以了。 135 | 136 | .. note:: 137 | 138 | 可以比较一下 :file:`dist3/mypkg/__init__.py` 和上一节生成到的加密文件 :file:`dist2/mypkg/__init__.py` 的内容更多的了解这个选项的作用。 139 | 140 | 封装加密包 141 | ------------ 142 | 143 | 再说一次,加密脚本就是正常的 Python 脚本,所以其他用来封装 Python 脚本的工具,例如 distutils, setuptools,以及 wheel 都可以用来封装加密脚本。 144 | 145 | 假设包 ``mypkg`` 的目录结构如下:: 146 | 147 | projects/ 148 | └── src/ 149 | └── mypkg/ 150 | ├── __init__.py 151 | ├── utils.py 152 | └── config.json 153 | 154 | 首先创建一个输出目录 :file:`projects/dist6` 用来保存加密包:: 155 | 156 | $ cd projects 157 | $ mkdir dist6 158 | 159 | 然后把所有数据文件到拷贝过去:: 160 | 161 | $ cp -a src/mypkg dist6/ 162 | 163 | 接下来生成加密包,把所有加密后的 ``.py`` 文件保存到输出目录 ``.py``:: 164 | 165 | $ pyarmor gen -O dist6 -i src/mypkg 166 | 167 | 最终的输出如下:: 168 | 169 | projects/ 170 | ├── README.md 171 | └── src/ 172 | └── mypkg/ 173 | ├── __init__.py 174 | ├── utils.py 175 | └── config.json 176 | └── dist6/ 177 | └── mypkg/ 178 | ├── __init__.py 179 | ├── utils.py 180 | ├── config.json 181 | └── pyarmor_runtime_000000/__init__.py 182 | 183 | 比较一下 :file:`src/mypkg` 和 :file:`dist6/mypkg` ,唯一的区别是后者多了一个目录 ``pyarmor_runtime_000000`` ,最后要做的就是使用你熟悉的方式封装 :file:`dist6/mypkg` 184 | 185 | 还不了解如何封装 Python 包?请参考这里学习 `Python Packaging User Guide`_ 186 | 187 | .. _Python Packaging User Guide: https://packaging.python.org 188 | 189 | 设置加密脚本有效期 190 | ================== 191 | 192 | 使用选项 :option:`-e` 可以方便的设置加密脚本的有效期。例如,设置加密脚本有效期为30天:: 193 | 194 | $ pyarmor gen -O dist4 -e 30 foo.py 195 | 196 | 运行一下加密脚本 :file:`dist4/foo.py` 来验证一下:: 197 | 198 | $ python dist4/foo.py 199 | 200 | 也可以使用另外一种格式 ``YYYY-MM-DD`` 来设置有效期,例如:: 201 | 202 | $ pyarmor gen -O dist4 -e 2020-12-31 foo.py 203 | 204 | 运行一下 :file:`dist4/foo.py` 进行验证:: 205 | 206 | $ python dist4/foo.py 207 | 208 | 发布有时间限制的加密脚本和上面的方法是一样的,直接拷贝整个输出目录 :file:`dist4/` 到 :term:`客户设备` 209 | 210 | 从 v8.5.0 开始,默认是检查本地时间。如果需要检查网络时间,需要指定远程服务器。例如:: 211 | 212 | $ pyarmor cfg nts=pool.ntp.org 213 | 214 | 实际上,这就是以前的版本的默认配置,不过这种配置有时候会报错 `RuntimeError: Resource temporarily unavailable` ,解决方案就是使用 HTTP 服务器来验证时间。例如:: 215 | 216 | $ pyarmor cfg nts=http://worldtimeapi.org/api 217 | 218 | 绑定加密脚本到指定设备 219 | ====================== 220 | 221 | 使用 Pyarmor 8.4.6+ 可以通过命令 `python -m pyarmor.cli.hdinfo` 直接得到:term:`客户设备` 的硬件信息如下:: 222 | 223 | Default Harddisk Serial Number: 'HXS2000CN2A' 224 | Default Mac address: '00:16:3e:35:19:3d' 225 | Default IPv4 address: '128.16.4.10' 226 | 227 | Pyarmor 8.4.6 之前的版本可以通过命令 `pyarmor-7 hdinfo` 查询硬件信息。 228 | 229 | 使用选项 :option:`-b` 来绑定硬件信息到加密角本。例如,绑定 :file:`dist5/foo.py` 到网卡以太网地址:: 230 | 231 | $ pyarmor gen -O dist5 -b 00:16:3e:35:19:3d foo.py 232 | 233 | 使用相同的选项来绑定 IPv4 地址和硬盘序列号:: 234 | 235 | $ pyarmor gen -O dist5 -b 128.16.4.10 foo.py 236 | $ pyarmor gen -O dist5 -b HXS2000CN2A foo.py 237 | 238 | 组合多种硬件信息使用下面的格式:: 239 | 240 | $ pyarmor gen -O dist5 -b "00:16:3e:35:19:3d HXS2000CN2A" foo.py 241 | 242 | 只有设备的硬件信息都符合绑定的信息,加密脚本才能运行,否则报错退出。 243 | 244 | 发布绑定到设备的加密脚本和上面的方法是一样的,直接拷贝整个输出目录 :file:`dist4/` 到 :term:`客户设备` 245 | 246 | 关于加密脚本必须要知道的 247 | ======================== 248 | 249 | 运行加密脚本需要一个 :term:`扩展模块` :mod:`pyarmor_runtime` ,它在运行辅助包 ``pyarmor_runtime_000000`` 目录下面:: 250 | 251 | $ ls dist6/mypkg/pyarmor_runtime_000000 252 | ... __init__.py 253 | ... pyarmor_runtime.so 254 | 255 | 使用二进制的扩展模块意味着加密脚本需要有为各个平台的预编译的扩展模块 :mod:`pyarmor_runtime` ,所以加密脚本 256 | 257 | * 只能运行在那些已经有预编译扩展模块的平台,所有支持的平台请参考 :doc:`../reference/environments` 258 | * 只能使用相同版本 CPython interpreter 解释器来运行,例如使用 Python 3.8 加密的脚本,无法被 Python 3.9 运行 259 | * 一般不能被第三方解释器,例如 PyPy, IronPython 或者 Jython 等来运行 260 | 261 | 还有,在 Android 系统下面, ``.py`` 脚本可以在任意目录下面运行,但是扩展模块是动态库,就必须在系统特定的目录下面才能运行。 262 | 263 | 下一步的教程 264 | ============ 265 | 266 | 根据你的需要进行选择下一步的教程 267 | 268 | 这里有完整的 :doc:`安装教程 ` 包含如下内容: 269 | 270 | * 从 Github 库直接安装 Pyarmor 271 | * 如何从 Python 脚本中调用 Pyarmor 272 | * 完整卸载 273 | 274 | 接下来是 :doc:`obfuscation` 包含的内容有: 275 | 276 | * 使用更多选项加密脚本和包 277 | * 使用外部密钥文件限制加密脚本的运行 278 | * 本地化错误信息 279 | * 生成不需要 Python 环境就可以独立运行的加密脚本 280 | 281 | 还有 :doc:`advanced` ,有些功能在试用版中无法使用 282 | 283 | * 如何使用两种不可逆的加密模式: RFT 模式和 BCC 模式 :sup:`pro` 284 | * 定制错误退出方式 285 | * 国际化错误消息 286 | * 加密跨平台的加密脚本 287 | 288 | 很多用户可能对这里的内容感兴趣 :doc:`../how-to/security` 289 | 290 | 如何阅读本手册 291 | ============== 292 | 293 | |Pyarmor| 有完备的文档系统,这里是帮助用户如何快速找到需要的相关内容 294 | 295 | * :doc:`第一部分: 基础教程 <../part-1>` 适合第一次使用 Pyarmor 的用户,这里以实例的形式一步接着一步的说明了加密脚本和包的最常用的场景。也可以先看看 :doc:`getting-started` 296 | 297 | * :doc:`第二部分: 应用实践 <../part-2>` 针对每一个特定的需求,说明在 Pyarmor 中应该如何去做,使用什么样的命令和选项去实现。阅读这部分内容需要对 Pyarmor 和 Python 都有一定的了解。 298 | 299 | * :doc:`第三部分: 技术手册 <../part-3>` 从技术层的角度详细列出了所有的概念定义,命令手册,配置选项和错误信息代码。它适用于使用 Pyarmor 的高级用户,需要查找相关的参数和配置,了解这些配置项的可用值和不同值的作用和含义。 300 | 301 | * :doc:`第四部分: 深入了解 <../part-4>` 这部分针对 Pyarmor 提供的功能,从如何实现的层面进行了详细的解释。阅读这部分内容需要完全掌握了 Pyarmor 使用到的主要概念,以及对 Python 脚本的执行过程有相当了解。它适用于需要对 Pyarmor 进行扩展和定制,以满足更高一层需求的用户。 302 | 303 | * :doc:`第五部分: 许可模式和许可证类型 <../licenses>` 描述了 |Pyarmor| 的最终用户许可协议, |Pyarmor| 的许可模式,不同的许可类型,以及如何购买 |Pyarmor| 许可证。 304 | 305 | 想找某一个特定的关键字,搜一下 :ref:`总索引 `, 或者浏览 :ref:`文档总目录 ` 306 | 307 | 还是没有找到? 看一下 :ref:`如何在 Github 上提问 `. 308 | 309 | .. include:: ../_common_definitions.txt 310 | -------------------------------------------------------------------------------- /docs/tutorial/installation.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | 完整安装教程 3 | ============== 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | 前提条件 13 | ======== 14 | 15 | Pyarmor_ 需要 Python 以及 C 库,缺少它们 Pyarmor 无法正常启动运行。 16 | 17 | .. 18 | 在 Linux 平台,如有必要请安装 Python 运行动态库。例如,使用下面的命令安装 Python 3.10 的运行动态库:: 19 | 20 | $ apt install libpython3.10 21 | 22 | 在 Darwin 平台,确保文件 ``@rpath/lib/libpythonX.Y.dylib`` 存在,这里 ``X.Y`` 表示 Python 的版本。例如:: 23 | 24 | @rpath/lib/libpython3.10.dylib 25 | 26 | ``@rpath`` 是下列路径之一: 27 | 28 | - @executable_path/.. 29 | - @loader_path/.. 30 | - /System/Library/Frameworks/Python.framework/Versions/3.10 31 | - /Library/Frameworks/Python.framework/Versions/3.10 32 | 33 | 如果没有这个文件,请安装必要的包或者使用必要的选项重新编译 Python ,或者使用 `install_name_tool` 适配当前的 Python 环境,参阅 :doc:`../question` 中如何解决 Apple 下面的奔溃问题。 34 | 35 | .. _install-pypi: 36 | 37 | 从 PyPI 直接安装 38 | ================ 39 | 40 | Pyarmor_ 发布在 PyPI_ 上面,最方便的方式就是使用命令 :command:`pip` 直接安装。 41 | 42 | 在 Linux 和 MacOS,直接打开命令终端并运行下面的命令:: 43 | 44 | $ pip install pyarmor 45 | 46 | 在 Windows 环境下,需要使用 :kbd:`Win-r` 打开命令输入框,然后输入 :command:`cmd` 打开命令窗口,并运行下面的命令: 47 | 48 | .. code-block:: doscon 49 | 50 | C:\> pip install pyarmor 51 | 52 | 安装完成之后,输入命令 :command:`pyarmor --version` 并回车。如果安装成功,会显示安装的 Pyarmor 的版本信息。 53 | 54 | 如果需要跨平台发布加密脚本,根据需要安装相应平台的辅助运行包:: 55 | 56 | $ pip install pyarmor.cli.core.windows 57 | $ pip install pyarmor.cli.core.themida 58 | $ pip install pyarmor.cli.core.linux 59 | $ pip install pyarmor.cli.core.darwin 60 | $ pip install pyarmor.cli.core.freebsd 61 | $ pip install pyarmor.cli.core.android 62 | $ pip install pyarmor.cli.core.alpine 63 | 64 | 并不是所有的平台都支持 Pyarmor,所有支持的运行平台请查看 :doc:`../reference/environments` 65 | 66 | .. note:: 67 | 68 | 如果无需使用老版本 Pyarmor 7 的功能,直接安装 :mod:`pyarmor.cli` 而不是安装 :mod:`pyarmor` 可以显著减少下载时间。例如:: 69 | 70 | $ pip install pyarmor.cli 71 | 72 | 安装的命令 73 | ---------- 74 | 75 | * :program:`pyarmor` 是最重要的一个,所有的工作基本都由它来完成,详细使用方法请参考 :doc:`../reference/man` 76 | * :program:`pyarmor-7` 是为了和老版本兼容的命令,它等价于 Pyarmor 7.x 的修正版本。 77 | * :program:`pyarmor-auth` 是集团版许可证支持运行不受限制 Docker 容器的辅助命令。 78 | 79 | 使用 Python 解释器直接运行 Pyarmor 80 | ---------------------------------- 81 | 82 | :program:`pyarmor` 等价于下面的命令:: 83 | 84 | $ python -m pyarmor.cli 85 | 86 | 从 Github 上面进行安装 87 | ====================== 88 | 89 | .. deprecated:: 8.2.9 90 | 91 | 也可以直接从 `Pyarmor Github`__ 安装 Pyarmor。下载库到本地,然后使用 pip 进行安装:: 92 | 93 | $ git clone https://github.com/dashingsoft/pyarmor 94 | $ cd pyarmor 95 | $ pip install . 96 | 97 | 你可以直接下载一个库的压缩文件 `tar.gz`__ 或者 `zip`__ ,解压之后在使用 pip 进行安装。 98 | 99 | .. note:: 100 | 101 | 从 8.2.9 开始不推荐使用这种方式进行安装,这种方式可能因为无法正常运行。 102 | 103 | __ https://github.com/dashingsoft/pyarmor 104 | __ https://github.com/dashingsoft/pyarmor/archive/master.tar.gz 105 | __ https://github.com/dashingsoft/pyarmor/archive/master.zip 106 | 107 | 在离线设备上安装 108 | ================ 109 | 110 | 所有的 Pyarmor 需要的包发布在 PyPI_ ,需要从上面下载必要的包,然后拷贝到离线设备。 111 | 112 | 首先安装 :mod:`pyarmor.cli.core` 113 | 114 | 其次安装 :mod:`pyarmor` 或者 :mod:`pyarmor.cli` 115 | 116 | 例如,在 64位 Linxu 平台下面为 Python 3.10 安装 Pyarmor:: 117 | 118 | $ pip install pyarmor.cli.core-3.2.9-cp310-none-manylinux1_x86_64.whl 119 | $ pip install pyarmor-8.2.9.zip 120 | 121 | 在 Android 或者 FreeBSD 系统,因为在包 :mod:`pyarmor.cli.core` 没有预编译的 wheel ,所以需要下载额外的包 :mod:`pyarmor.cli.core.android` 或者 :mod:`pyarmor.cli.core.freebsd` 。例如,在 Android 系统,运行下面的命令离线安装 Pyarmor:: 122 | 123 | $ pip install pyarmor.cli.core-3.2.9.zip 124 | $ pip install pyarmor.cli.core.android-3.2.9-cp310-none-any.whl 125 | $ pip install pyarmor-8.2.9.zip 126 | 127 | 在一些特殊架构 `ppc64le`, `mips32el`, `mips64el`, `riscv64`, `loongarch64` 上,也没有 Wheel 直接可用,需要安装 `pyarmor.cli.core.linux` (glibc) 或者 `pyarmor.cli.core.alpine` (musl)。例如:: 128 | 129 | $ pip install pyarmor.cli.core-8.5.9.zip 130 | $ pip install pyarmor.cli.core.linux-6.5.2-cp310-none-any.whl 131 | $ pip install pyarmor.cli-8.5.9.zip 132 | 133 | 如果需要跨平台加密,还需要安装相应的包 `pyarmor.cli.core.NAME` 134 | 135 | - :mod:`pyarmor.cli.core.freebsd` 136 | - :mod:`pyarmor.cli.core.android` 137 | - :mod:`pyarmor.cli.core.windows` 138 | - :mod:`pyarmor.cli.core.themida` 139 | - :mod:`pyarmor.cli.core.linux` 140 | - :mod:`pyarmor.cli.core.alpine` 141 | - :mod:`pyarmor.cli.core.darwin` 142 | 143 | 例如,需要使用 Themida 保护的运行包,就需要安装:: 144 | 145 | $ pip install pyarmor.cli.themida-3.2.9-cp310-none-any.whl 146 | 147 | 在 Linux 平台加密运行在 Windows 平台的包,需要安装:: 148 | 149 | $ pip install pyarmor.cli.windows-3.2.9-cp310-none-any.whl 150 | 151 | 如果不需要使用老版本的命令 `pyarmor-7` ,推荐安装 :mod:`pyarmor.cli` 而不是 :mod:`pyarmor` ,前者需要下载的文件显著小于后者。例如:: 152 | 153 | $ pip install pyarmor.cli-8.2.9.zip 154 | 155 | Termux 平台的额外补丁 156 | ===================== 157 | 158 | 在 Termux 平台,安装完成之后还需要额外对扩展模块打补丁。例如:: 159 | 160 | $ patchelf --add-needed libpython3.11.so.0.1 /data/data/com.termux/files/usr/lib/python3.11/site-packages/pyarmor/cli/core/android/aarch64/pytransform3.so 161 | $ patchelf --add-needed libpython3.11.so.0.1 /data/data/com.termux/files/usr/lib/python3.11/site-packages/pyarmor/cli/core/android/aarch64/pyarmor_runtime.so 162 | 163 | 有时候可以还需要额外的配置。例如:: 164 | 165 | $ patchelf --set-rpath /data/data/com.termux/files/usr/lib /path/to/{pytransform3,pyarmor_runtime}.so 166 | 167 | 否则,运行 `pyarmor` 会报错 `dlopen failed: cannot locate symbol "PyFloat_Type"` 168 | 169 | 在 Python 脚本中调用 Pyarmor 170 | ============================ 171 | 172 | 首先创建一个任意的脚本,例如 :file:`tool.py` 173 | 174 | .. code-block:: python 175 | 176 | from pyarmor.cli.__main__ import main_entry 177 | 178 | args = ['gen', '-O', 'dist', '--platform', 'linux.x86_64,windows.x86_64', 'foo.py'] 179 | main(args) 180 | 181 | 然后运行这个脚本:: 182 | 183 | $ python tool.py 184 | 185 | 上面的例子等价于执行下面的命令:: 186 | 187 | $ pyarmor gen -O dist --platform linux.x86_64,windows.x86_64 foo.py 188 | 189 | 以上只是一个示例说明,具体使用的加密选项和参数可以通过各种方式进行传递。 190 | 191 | 完全卸载 192 | ======== 193 | 194 | 使用下面的命令可以完全卸载 Pyarmor:: 195 | 196 | $ pip uninstall pyarmor 197 | $ pip uninstall pyarmor.cli.core 198 | 199 | # 下面的包不一定都安装,根据安装情况卸载相应的包 200 | 201 | $ pip uninstall pyarmor.cli.runtime 202 | $ pip uninstall pyarmor.cli.core.windows 203 | $ pip uninstall pyarmor.cli.core.themida 204 | $ pip uninstall pyarmor.cli.core.linux 205 | $ pip uninstall pyarmor.cli.core.darwin 206 | $ pip uninstall pyarmor.cli.core.freebsd 207 | $ pip uninstall pyarmor.cli.core.android 208 | $ pip uninstall pyarmor.cli.core.alpine 209 | 210 | $ rm -rf ~/.pyarmor 211 | $ rm -rf ./.pyarmor 212 | 213 | .. note:: 214 | 215 | ``~`` 是当前登录用户的个人目录,可以通过查看环境变量 ``HOME`` 得到具体位置。使用不同用户登陆,路径 ``~`` 可能不一样。 216 | 217 | .. include:: ../_common_definitions.txt 218 | -------------------------------------------------------------------------------- /docs/tutorial/obfuscation.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | 基础教程 3 | ========== 4 | 5 | .. contents:: 内容 6 | :depth: 2 7 | :local: 8 | :backlinks: top 9 | 10 | .. highlight:: console 11 | 12 | .. program:: pyarmor gen 13 | 14 | 本教程仅适用于 Pyarmor 8.0+,使用下面的命令来查看版本信息:: 15 | 16 | $ pyarmor --version 17 | 18 | 如果 Pyarmor 的版本小于 8,那么请选择相应版本的文档(在页面的左下角可以选择版本),或者升级 Pyarmor 到正确版本。 19 | 20 | 在整个教程中,使用的例子结构如下:: 21 | 22 | project/ 23 | ├── foo.py 24 | ├── queens.py 25 | └── joker/ 26 | ├── __init__.py 27 | ├── queens.py 28 | └── config.json 29 | 30 | Pyarmor 使用 :ref:`pyarmor gen` 加密不同的脚本,它提供了丰富的选项,以满足不同应用关于性能和安全方面的各种需求。 31 | 32 | 这里仅仅介绍的是常用的一些功能,关于完整的命令选项请查阅 :doc:`../reference/man` 33 | 34 | 调试模式和跟踪日志 35 | ================== 36 | 37 | 当加密过程出现问题的时候,检查控制台的输出日志可以帮助发现问题所在,而使用选项 ``-d`` 启用调试模式会生成调试日志 :file:`pyarmor.debug.log` ,显示更多的信息帮助发现错误:: 38 | 39 | $ pyarmor -d gen foo.py 40 | $ cat pyarmor.debug.log 41 | 42 | 跟踪日志用来记录那些函数被 Pyarmor 使用什么方式进行了保护,它通过下面的方式启用:: 43 | 44 | $ pyarmor cfg enable_trace=1 45 | 46 | 启用之后,每一次执行命令 :ref:`pyarmor gen` 都会生成一个跟踪日志文件 :file:`pyarmor.trace.log` 记录相关的保护信息。例如:: 47 | 48 | $ pyarmor gen foo.py 49 | $ cat pyarmor.trace.log 50 | 51 | trace.co foo:1: 52 | trace.co foo:5:hello 53 | trace.co foo:9:sum2 54 | trace.co foo:12:main 55 | 56 | 每一行开头的 ``trace.co`` 表示是默认的加密模式,后面的函数名称表示该函数使用默认的方式进行加密。 57 | 58 | 使用下面的方式禁用跟踪日志:: 59 | 60 | $ pyarmor cfg enable_trace=0 61 | 62 | .. program:: pyarmor gen 63 | 64 | 使用更多的选项加密脚本 65 | ====================== 66 | 67 | 对于脚本,可以使用这些选项来增加安全性:: 68 | 69 | $ pyarmor gen --enable-jit --mix-str --assert-call --private foo.py 70 | 71 | 选项 :option:`--enable-jit` 通过使用动态指令生成技术处理某些敏感数据来增加安全性。 72 | 73 | 选项 :option:`--mix-str` [#]_ 能够加密脚本中所有长度大于 8 的字符串。 74 | 75 | 选项 :option:`--assert-call` 能够确保加密脚本中的函数不会被替换。 76 | 77 | 选项 :option:`--private` 能够确保加密脚本的属性不能直接被 Python 解释器直接导入查看。 78 | 79 | 例如, 80 | 81 | .. code-block:: python 82 | :emphasize-lines: 1,10 83 | 84 | data = "abcdefgxyz" 85 | 86 | def fib(n): 87 | a, b = 0, 1 88 | while a < n: 89 | print(a, end=' ') 90 | a, b = b, a+b 91 | 92 | if __name__ == '__main__': 93 | fib(n) 94 | 95 | 字符串常量 ``abcdefgxyz`` 和函数 ``fib`` 会被以如下方式进行保护 96 | 97 | .. code-block:: python 98 | :emphasize-lines: 1,10 99 | 100 | data = __mix_str__(b"******") 101 | 102 | def fib(n): 103 | a, b = 0, 1 104 | while a < n: 105 | print(a, end=' ') 106 | a, b = b, a+b 107 | 108 | if __name__ == '__main__': 109 | __assert_call__(fib)(n) 110 | 111 | 如果函数 ``fib`` 是加密函数,那么 ``__assert_call__(fib)`` 返回原来的函数,否则抛出保护异常。 112 | 113 | 为了查看那些函数和字符串被保护,可以启用并检查跟踪日志:: 114 | 115 | $ pyarmor cfg enable_trace=1 116 | $ pyarmor gen --mix-str --assert-call fib.py 117 | $ cat pyarmor.trace.log 118 | 119 | trace.assert.call fib:10:'fib' 120 | trace.mix.str fib:1:'abcxyz' 121 | trace.mix.str fib:9:'__main__' 122 | trace.co fib:1: 123 | trace.co fib:3:fib 124 | 125 | .. [#] :option:`--mix-str` 在试用版中不可用 126 | 127 | 使用更多的选项加密包 128 | ==================== 129 | 130 | 如果是加密 :term:`Python 包` ,使用其他两个选项:: 131 | 132 | $ pyarmor gen --enable-jit --mix-str --assert-call --assert-import --restrict joker/ 133 | 134 | 选项 :option:`--assert-import` 可以检查导入的模块,确保是没有被替换的加密模块。 135 | 136 | 选项 :option:`--restrict` 可以确保加密模块只能在加密脚本内部使用,而不能被外部脚本导入。 137 | 138 | 默认情况下 ``__init__.py`` 中定义的函数和名称是可以被外部脚本使用的,其他模块定义的函数和名称是不能被外部脚本使用。让我们创建一个测试脚本 :file:`dist/a.py` 来验证一下 139 | 140 | .. code-block:: python 141 | 142 | import joker 143 | print('import joker OK') 144 | from joker import queens 145 | print('import joker.queens OK') 146 | 147 | 运行这个测试脚本:: 148 | 149 | $ cd dist 150 | $ python a.py 151 | ... import joker OK 152 | ... RuntimeError: unauthorized use of script 153 | 154 | 如果需要导出包中的其他模块,要么不使用选项 :option:`--restrict` ,要么单独配置模块 ``joker.queens`` 不使用约束模式:: 155 | 156 | $ pyarmor cfg -p joker.queens restrict_module=0 157 | 158 | 再次加密和测试一下,这次应该可以正常运行:: 159 | 160 | $ pyarmor gen --restrict joker/ 161 | 162 | $ cd dist/ 163 | $ python a.py 164 | ... import joker OK 165 | ... import joker.queens 166 | 167 | 拷贝数据文件 168 | ============ 169 | 170 | 很多包都有数据文件,并且运行的时候需要这些数据文件,但是默认情况下不会被 Pyarmor 拷贝到输出路径。 171 | 172 | 但是 Pyarmor 提供了三种方法可以解决这个问题 173 | 174 | 1. 在加密之前先把整个包全部拷贝到输出目录,然后加密脚本,这样加密脚本仅仅覆盖原来的 ``.py`` 文件,而数据文件保留不变:: 175 | 176 | $ mkdir dist/joker 177 | $ cp -a joker/* dist/joker 178 | $ pyarmor gen -O dist -r joker/ 179 | 180 | 2. 通过配置选项,让 Pyarmor 自动拷贝所有数据文件:: 181 | 182 | $ pyarmor cfg data_files=* 183 | $ pyarmor gen -O dist -r joker/ 184 | 185 | 如果只需要拷贝 ``*.yaml`` 和 ``*.json`` 文件,使用下面的配置命令:: 186 | 187 | $ pyarmor cfg data_files="*.yaml *.json" 188 | 189 | 3. 自己编写 :term:`加密插件` 来拷贝需要的数据文件 190 | 191 | 周期性检查运行密钥 192 | ================== 193 | 194 | 使用下面的命令生成的加密脚本,运行的时候会每隔一个小时对运行密钥进行一次检查:: 195 | 196 | $ pyarmor gen --period 1 foo.py 197 | 198 | 绑定加密脚本到多个设备 199 | ====================== 200 | 201 | 使用选项 :option:`-b` 多次可以绑定加密脚本到多个设备。 202 | 203 | 例如,两台设备 A 和 B,以太网地址分别是 ``66:77:88:9a:cc:fa`` 和 ``f8:ff:c2:27:00:7f`` ,使用下面的命令绑定加密脚本到这两台设备:: 204 | 205 | $ pyarmor gen -b "66:77:88:9a:cc:fa" -b "f8:ff:c2:27:00:7f" foo.py 206 | 207 | 使用外部文件存放运行密钥 208 | ======================== 209 | 210 | 在加密脚本的时候指定使用外部密钥:: 211 | 212 | $ pyarmor gen --outer foo.py 213 | 214 | 在这种情况下,加密后的脚本无法直接运行:: 215 | 216 | $ python dist/foo.py 217 | 218 | 需要先使用 :ref:`pyarmor gen key` 创建一个外部密钥:: 219 | 220 | $ pyarmor gen key -e 3 221 | 222 | 上面的命令会生成外部密钥文件 ``dist/pyarmor.rkey`` ,拷贝这个文件到运行辅助包:: 223 | 224 | $ cp dist/pyarmor.rkey dist/pyarmor_runtime_000000/ 225 | 226 | 这样可以正常运行加密脚本 :file:`dist/foo.py`:: 227 | 228 | $ python dist/foo.py 229 | 230 | 让我们在生成一个新的运行密钥文件,存放在另外一个目录:: 231 | 232 | $ pyarmor gen key -O dist/key2 -e 10 233 | 234 | $ ls dist/key2/pyarmor.rkey 235 | 236 | 拷贝这个新文件到运行辅助包去替换原来的:: 237 | 238 | $ cp dist/key2/pyarmor.rkey dist/pyarmor_runtime_000000/ 239 | 240 | 外部运行密钥必须至少包含一个约束条件,要么是有效期,要么是设备信息。 241 | 242 | 外部运行密钥的名称默认是 ``pyarmor.rkey`` 243 | 244 | 外部运行密钥也可以存放在其他路径,请参阅 :term:`外部密钥` 中的说明 245 | 246 | 如何本地化错误信息 247 | ================== 248 | 249 | 运行错误信息可以使用本地语言进行替换定制,例如加密脚本过期之后可以提示用户自己设定的错误信息。 250 | 251 | 首先在当前目录创建子目录 :file:`.pyarmor` ,然后在其中创建文件 :file:`messages.cfg`:: 252 | 253 | $ mkdir .pyarmor 254 | $ vi .pyarmor/messages.cfg 255 | 256 | 编辑这个文件。这是一个 ``.ini`` 格式的文件,按照自己的需要修改 ``error_N`` 后面对应的错误信息 257 | 258 | .. code-block:: ini 259 | 260 | [runtime.message] 261 | 262 | error_1 = 脚本许可证已经过期 263 | error_2 = 脚本许可证不可用于当前设备 264 | error_3 = 缺少运行许可文件 265 | error_4 = 非法使用脚本 266 | 267 | error_5 = 脚本不支持当前 Python 版本 268 | error_6 = 脚本不支持当前系统 269 | 270 | error_7 = 加密模块的数据格式不正确 271 | error_8 = 加密函数的数据格式不正确 272 | 273 | 然后需要重新加密脚本:: 274 | 275 | $ pyarmor gen foo.py 276 | 277 | 如果你想所有的运行密钥错误都显示同样的错误信息,而其他类型的错误还是使用默认值, 278 | 那么,修改成为下面的样子 279 | 280 | .. code-block:: ini 281 | 282 | [runtime.message] 283 | 284 | error_1 = 未授权使用脚本 285 | error_2 = 未授权使用脚本 286 | 287 | 然后重新加密脚本使之生效。 288 | 289 | 生成可独立运行的加密脚本 290 | ======================== 291 | 292 | 这里的打包是指生成可以在没有 Python 环境独立运行的可执行文件。 293 | 294 | Pyarmor 需要使用 PyInstaller_ 打包好的可执行文件,请首先安装 PyInstaller_:: 295 | 296 | $ pip install pyinstaller 297 | 298 | 单个可执行文件模式 299 | ------------------ 300 | 301 | .. versionadded:: 8.5.4 302 | 303 | 生成一个单独的可执行文件直接使用下面的命令即可:: 304 | 305 | $ pyarmor gen --pack onefile foo.py 306 | 307 | 这个命令会加密 ``foo.py`` ,然后加密和 ``foo.py`` 同目录的,并且被引用到的其他模块或者包,最后调用 PyInstaller_ 把加密脚本打包成为一个单独的可执行文件 ``dist/foo``:: 308 | 309 | $ ls dist/foo 310 | $ dist/foo 311 | 312 | 需要注意的是对于脚本引用到系统模块,以及任何不在当前目录下的模块和包,都没有进行加密处理。 313 | 314 | .. important:: 315 | 316 | 在命令行中的脚本 ``foo.py`` 必须是没有加密的,加密的脚本是无法直接打包的 317 | 318 | 单个目录模式 319 | ------------ 320 | 321 | .. versionadded:: 8.5.4 322 | 323 | 生成单个目录模式的包直接使用下面的命令即可:: 324 | 325 | $ pyarmor gen --pack onedir foo.py 326 | 327 | 这个命令基本和上面的命令类似,只是在最后调用 PyInstaller_ 的时候传入的是单个目录的模式。查看最后的输出目录:: 328 | 329 | $ ls dist/foo 330 | $ dist/foo/foo 331 | 332 | 使用 spec 文件打包加密脚本 333 | -------------------------- 334 | 335 | .. versionadded:: 8.5.8 336 | 337 | 如果已经有现成的 spec 文件能够成功打包没有加密的脚本,例如:: 338 | 339 | $ pyinstaller foo.spec 340 | $ dist/foo 341 | 342 | 可以直接把 ``foo.spec`` 传递给 :option:`--pack` ,来自动打包加密脚本。例如:: 343 | 344 | $ pyarmor gen --pack foo.spec -r foo.py joker/ 345 | $ dist/foo 346 | 347 | 在这种模式无法自动加密依赖的脚本,需要人工在命令行指出需要加密的所有脚本,否则不会被加密。 348 | 349 | 如果打包出现问题,或者需要使用更多打包方面的功能,请参阅 :doc:`../topic/repack` 350 | 351 | .. include:: ../_common_definitions.txt 352 | --------------------------------------------------------------------------------