= Pyarmor =

Pyarmor is a command line tool used to obfuscate python scripts, bind obfuscated scripts to fixed machine or expire obfuscated scripts. It protects Python scripts by the following ways:

 * Obfuscate code object to protect constants and literal strings.
 * Obfuscate byte code of each code object.
 * Clear f_locals of frame as soon as code object completed execution.
 * Expired obfuscated scripts, or bind to fixed machine.

Look at what happened after `foo.py` is obfuscated by Pyarmor. Here are the files list in the output path `dist`

{{{
    foo.py

    pytransform.py
    _pytransform.so, or _pytransform.dll in Windows, _pytransform.dylib in MacOS

    pyshield.key
    pyshield.lic
    product.key
    license.lic
}}}

`dist/foo.py` is obfuscated script, the content is

{{{#!python
    from pytransfrom import pyarmor_runtime
    pyarmor_runtime()

    __pyarmor__(__name__, __file__, b'\x06\x0f...')
}}}

All the other extra files called `Runtime Files`, which are required to run or import obfuscated scripts. So long as runtime files are in any Python path, obfuscated script `dist/foo.py` can be used as normal Python script. That is to say, ```the original python scripts can be replaced with obfuscated scripts seamlessly.```

== Obfuscate Scripts ==

How to obfuscate python scripts by Pyarmor?

First compile Python script to code object
{{{#!c
    char *filename = "foo.py";
    char *source = read_file( filename );
    PyCodeObject *co = Py_CompileString( source, "<frozen foo>", Py_file_input );
}}}

Next change this code object as the following ways

 * Wrap byte code `co_code` within a `try...finally` block
 {{{
    wrap header:

            LOAD_GLOBALS    N (__armor_enter__)     N = length of co_consts
            CALL_FUNCTION   0
            POP_TOP
            SETUP_FINALLY   X (jump to wrap footer) X = size of original byte code

    changed original byte code:

            Increase oparg of each absolute jump instruction by the size of wrap header

            Obfuscate original byte code

            ...

    wrap footer:

            LOAD_GLOBALS    N + 1 (__armor_exit__)
            CALL_FUNCTION   0
            POP_TOP
            END_FINALLY

 }}}

 * Append function names `__armor_enter`, `__armor_exit__` to `co_consts`

 * Increase `co_stacksize` to 4 if it's less than 4

 * Set CO_OBFUSCAED (0x80000000) flag in `co_flags`

 * Change all code objects in the `co_consts` recursively

Then serialize this reformed code object, obfuscate it to protect constants and literal strings

{{{#!c
    char *string_code = marshal.dumps( co );
    char *obfuscated_code = obfuscate_algorithm( string_code );
}}}

Finally generate obfuscated script

{{{#!c
    sprintf( buf, "__pyarmor__(__name__, __file__, b'%s')", obfuscated_code );
    save_file( "dist/foo.py", buf );
}}}

The obfuscated script is a normal Python script, it looks like this

{{{#!python
    __pyarmor__(__name__, __file__, b'\x01\x0a...')
}}}

== Run Obfuscated Scripts ==

What happens to run obfuscated script `dist/foo.py` by Python Interpreter?

The first 2 lines, which called `Bootstrap Code`

{{{#!python
    from pytransfrom import pyarmor_runtime
    pyarmor_runtime()
}}}

It will fulfil the following tasks

 * Validate `dist/license.lic`, check whether it's expired or not etc.
 * Add 3 functions to module `builtins`:
   * `__pyarmor__`
   * `__armor_enter__`
   * `__armor_exit__`

The next code line in `dist/foo.py` is
{{{#!python
    ...

    __pyarmor__(__name__, __file__, b'\x01\x0a...')
}}}

`__pyarmor__` is called, it will import original module from obfuscated code
{{{#!c
    static PyObject *
    __pyarmor__(char *name, char *pathname, unsigned char *obfuscated_code)
    {
        char *string_code = restore_obfuscated_code( obfuscated_code );
        PyCodeObject *co = marshal.loads( string_code );
        return PyImport_ExecCodeModuleEx( name, co, pathname );
    }
}}}

After that, in the runtime of this python process

 * `__armor_enter__` is called as soon as any code object is executed, it will restore byte-code of this code object

 {{{#!c
    static PyObject *
    __armor_enter__(PyObject *self, PyObject *args)
    {
        // Got code object
        PyFrameObject *frame = PyEval_GetFrame();
        PyCodeObject *f_code = frame->f_code;

        // Increase refcalls of this code object
        // Borrow co_names->ob_refcnt as call counter
        // Generally it will not increased  by Python Interpreter
        PyObject *refcalls = f_code->co_names;
        refcalls->ob_refcnt ++;

        // Restore byte code if it's obfuscated
        if (IS_OBFUSCATED(f_code->co_flags)) {
            restore_byte_code(f_code->co_code);
            clear_obfuscated_flag(f_code);
        }

        Py_RETURN_NONE;
    }

 }}}

 * `__armor_exit__` is called so long as code object completed execution, it will obfuscate byte-code again

 {{{#!c
    static PyObject *
    __armor_exit__(PyObject *self, PyObject *args)
    {
        // Got code object
        PyFrameObject *frame = PyEval_GetFrame();
        PyCodeObject *f_code = frame->f_code;

        // Decrease refcalls of this code object
        PyObject *refcalls = f_code->co_names;
        refcalls->ob_refcnt --;

        // Obfuscate byte code only if this code object isn't used by any function
        // In multi-threads or recursive call, one code object may be referenced
        // by many functions at the same time
        if (refcalls->ob_refcnt == 1) {
            obfuscate_byte_code(f_code->co_code);
            set_obfuscated_flag(f_code);
        }

        // Clear f_locals in this frame
        clear_frame_locals(frame);

        Py_RETURN_NONE;
    }
 }}}

== Usage ==

Install
{{{
    pip install pyarmor
}}}

Obfuscate Scripts
{{{
    cd /path/to/pyarmor
    python pyarmor.py obfuscate --src=examples/simple --entry=queens.py
}}}

Run Obfuscated Scripts
{{{
    cd dist
    python queens.py
}}}

Bind obfuscated scripts to fixed machine and expire it on some day. By default the obfuscated scripts can run in any machine and never expired,
this behavior can be changed by replacing runtime file `dist/license.lic`
{{{
    cd /path/to/pyarmor
    python pyarmor.py licenses --expired "2018-12-31" --bind-mac "70:f1:a1:23:f0:94" Jondy
    cp licenses/Jondy/license.lic dist/

    cd dist
    python queens.py
}}}

== Support Platforms ==

 * Python 2.5, 2.6, 2.7 and Python3
 * win32, win_amd64, linux_i386, linux_x86_64, macosx_intel
 * Embedded Platform: Raspberry Pi, Banana Pi, TS-4600 / TS-7600

Besides, pyarmor works well with [[py2exe]] and [[PyInstaller]]. Here are some [[https://github.com/dashingsoft/pyarmor/blob/master/src/examples/README.md|examples]].

== Other Links ==
 * [[http://pyarmor.dashingsoft.com|Homepage]]
 * [[https://github.com/dashingsoft/pyarmor|Source Code]]
 * [[https://pypi.org/project/pyarmor/|pypi]]
 * [[https://github.com/dashingsoft/pyarmor/blob/master/src/user-guide.md|User Guide]]

----
CategoryDistutilsCookbook