Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/unverbuggt/mkdocs-encryptcontent-plugin

A MkDocs plugin that encrypt/decrypt markdown content with AES
https://github.com/unverbuggt/mkdocs-encryptcontent-plugin

aes encryption-decryption highlightjs jinja2 mkdocs-plugin protected-resources python

Last synced: 12 days ago
JSON representation

A MkDocs plugin that encrypt/decrypt markdown content with AES

Lists

README

        

[![PyPI Version](https://img.shields.io/pypi/v/mkdocs-encryptcontent-plugin.svg)](https://pypi.org/project/mkdocs-encryptcontent-plugin/)
[![PyPI downloads](https://img.shields.io/pypi/dm/mkdocs-encryptcontent-plugin.svg)](https://pypi.org/project/mkdocs-encryptcontent-plugin/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

# mkdocs-encryptcontent-plugin

This plugin allows you to have password protected articles and pages in MKdocs.

The content is encrypted with AES-256 in Python using PyCryptodome and decrypted in the browser with Crypto-JS or Webcrypto.

*It has been tested in Python Python 3.8+*

**Usecase**

> I want to be able to protect the content of the page with a password.
>
> Define a password to protect each page independently or a global password to protect them all.
>
> If a global password exists, all articles and pages are protected with this password.
>
> If a password is defined in an article or a page, it is always used even if there is a global password.
>
> If a password is defined as an empty character string, content encryption is disabled.
>
> Additionally password levels can be defined in mkdocs.yml or external yaml file with user/password credentials.

## New features (compared to version 2.5.x)

* Stronger cryptography (PBKDF2 for key derivation)
* Faster cryptography (Webcrypto as alternative to crypto-js)
* Allow password inventory or levels in mkdocs.yml or external file for credential handling
* Allow user name + password as credentials
* If credential is reused in different levels it also decrypts all of their content
* Optional adding of credentials to URLs for sharing
* Optional tamper check by signing generated files with Ed25519
* New [Documentation](https://unverbuggt.github.io/mkdocs-encryptcontent-plugin/) and [Test bench](https://unverbuggt.github.io/mkdocs-encryptcontent-plugin/testbench/)

## Upgrading from version 2.5.x

The `use_secret` functionality was discontinued in the plugin configuration and as a meta tag.

In order to use environment variables in user names or passwords, use the
[special yaml tag](https://www.mkdocs.org/user-guide/configuration/#special-yaml-tags) `!ENV`.

## Todos for 3.1.x
* outsource some functionality to separate plugins, like:
* Filename obfuscation
* Signing of generated files
* find a better way for search decryption
* add better alternative to PBKDF2
* optional server side keystore (allows throtteling)
* still no waterproof solution...
* ...to be defined

# Table of Contents
* [Installation](#Installation)
* [Usage](#Usage)
* [Password inventory](#Password-inventory)
* [Password inventory in external file](#Password-inventory-in-external-file)
* [Global password protection](#Global-password-protection)
* [Global passwords in inventory](#Global-passwords-in-inventory)
* [Secret from environment](#Secret-from-environment)
* [Default vars customization](#Default-vars-customization)
* [Translations](#Translations)
* [Custom per-page strings](#Custom-per-page-strings)
* [Obfuscate pages](#Obfuscate-pages)
* [Example plugin configuration](#Example-plugin-configuration)
* [Features](#Features)
* [Override default templates](#Override-default-templates)
* [Add button](#Add-button)
* [Tag encrypted page](#Tag-encrypted-page)
* [Remember password](#Remember-password)
* [Share link generation](#Share-link-generation)
* [Incomplete Share links](#Incomplete-Share-links)
* [Storage of additional variables in keystore](#Storage-of-additional-variables-in-keystore)
* [Modify generated pages](#Modify-generated-pages)
* [Encrypt something](#Encrypt-something)
* [Inject decrypt-form.tpl to theme](#Inject-decrypt-form.tpl-to-theme)
* [Mix encrypted and normal content](#Mix-encrypted-and-normal-content)
* [Search encryption](#Search-encryption)
* [Search index encryption](#Search-index-encryption)
* [Search index encryption for mkdocs-material](#Search-index-encryption-for-mkdocs-material)
* [Javascript extensions](#Javascript-extensions)
* [Reload user-defined scripts](#Reload-user-defined-scripts)
* [HighlightJS support](#HighlightJS-support)
* [Arithmatex support](#Arithmatex-support)
* [Mermaid.js support](#Mermaid.js-support)
* [mkdocs-glightbox](#mkdocs-glightbox)
* [Security](#Security)
* [Crypto-js or crypto-es or webcrypto?](#Crypto-js-or-crypto-es-or-webcrypto?)
* [File name obfuscation](#File-name-obfuscation)
* [Signing of generated files](#Signing-of-generated-files)

# Installation

Install the package with pip:

```bash
pip install mkdocs-encryptcontent-plugin
```

Install the package from source with pip:

```bash
cd mkdocs-encryptcontent-plugin/
python setup.py sdist bdist_wheel
pip install --force-reinstall --no-deps dist/mkdocs_encryptcontent_plugin-3.0.3-py3-none-any.whl
```

Enable the plugin in your `mkdocs.yml`:

```yaml
plugins:
- search: {}
- encryptcontent: {}
```
> **NOTE:** If you have no `plugins` entry in your configuration file yet, you'll likely also want to add the `search` plugin.
> MkDocs enables it by default if there is no `plugins` entry set, but now you have to enable it explicitly.

# Usage

Add a tag like `password: secret password` to your pages [Meta-Data](https://www.mkdocs.org/user-guide/writing-your-docs/#yaml-style-meta-data) to protect them.

Alternatively add a meta tag like `level: secret` to use one or more secrets defined at the
plugin's `password_inventory` or `password_file` in your "mkdocs.yml" (see below).

## Password inventory

With the `password_inventory` you can define protection levels (assigned with the meta tag `level` in markdown files).

```yaml
plugins:
- encryptcontent:
password_inventory:
classified: 'password1'
confidential:
- 'password2'
- 'password3'
secret:
user4: 'password4'
user5: 'password5'
```

These levels may be only one password (f.ex. classified), a list of multiple passwords (f.ex. confidential)
or multiple username/password pairs (f.ex. secret).
It is possible to reuse credentials at different levels.

>Note that a "list of multiple passwords" comes with a downside: All entries may be tested because unlike "user/password pairs"
>there is no hint to determine the distinct entry to try
>(At least I found no hint that wouldn't make it easier for a brute force attacker).
>This means, that a high `kdf_pow` could cause long waiting time even if the right password was entered.

The plugin will generate one secret key for each level, which is then used to encrypt the assigned sites.

To indicate that your Markdown file should be encrypted for level "secret", add the following metadata at the beginning of the file:

```markdown
---
level: secret
---
This is the first paragraph of the document.
```

### Password inventory in external file

You can define password levels in an external yaml file and link it with `password_file`.
The intention is to separate sensitive information from configuration options.

```yaml
plugins:
- encryptcontent:
password_file: 'passwords.yml'
```

passwords.yml:
```yaml
classified: 'password1'
confidential:
- 'password2'
- 'password3'
secret:
user4: 'password4'
user5: 'password5'
```

## Global password protection

Add `global_password: your_password` in plugin configuration variable, to protect all pages with this password by default

```yaml
plugins:
- encryptcontent:
global_password: 'your_password'
```

If the password meta tag is defined in a markdown file, it will **ALWAYS** override the global password.

> **NOTE** Keep in mind that if the `password:` tag exists without value in a page, it will **not be protected** !
> Use this to **disable** `global_password` on specific pages.

### Global passwords in inventory

You can add the special level `_global`, which will be applied globally on all sites like this:

```yaml
plugins:
- encryptcontent:
password_inventory:
_global: 'either define one password'
_global:
- 'or define'
- 'multiple passwords'
_global:
user1: 'or use user'
user2: 'and password pairs'
```

> **NOTE** Add the meta tag `level:` (without a value) to pages which should be excluded from global password level.
> Also note that it is always possible to set the page to a different level than the global one with the `level` meta tag.

## Secret from environment

It is possible to read values from environment variable
(as discribed [here](https://www.mkdocs.org/user-guide/configuration/#environment-variables)).
This replaces the deprecated `use_secret` option from previous versions.

```yaml
plugins:
- encryptcontent:
password_inventory:
secret:
user1: !ENV PASSWORD1_FROM_ENV
user2: !ENV [PASSWORD2_FROM_ENV, 'Password if PASSWORD2_FROM_ENV undefined or empty']
user3: !ENV [PASSWORD3_FROM_ENV, FALLBACK_PASSWORD3_FROM_ENV, 'Password if neither PASSWORD3_FROM_ENV nor FALLBACK_PASSWORD3_FROM_ENV defined']
```

## Default vars customization

Optionally you can use some extra variables in plugin configuration to customize default strings.

```yaml
plugins:
- encryptcontent:
title_prefix: '[LOCK]'
summary: 'another informational message to encrypted content'
placeholder: 'another password placeholder'
decryption_failure_message: 'another informational message when decryption fails'
encryption_info_message: "another information message when you don't have access !"
input_class: 'md-search__form md-search__input'
button_class: 'md-search__icon'
```

Default prefix title is `[Protected]`.

Default summary message is `This content is protected with AES encryption.`.

Default password palceholder is `Provide password and press ENTER`.

Default decryption failure message is `Invalid password.`.

Defaut encryption information message is `Contact your administrator for access to this page.`.

> **NOTE** Adding a prefix to the title does not change the default navigation path !

## Translations

If the plugin is used in conjunction with the [static-i18n](https://ultrabug.github.io/mkdocs-static-i18n/)
plugin you can provide `translations` for the used `i18n_page_locale`.

```yaml
- encryptcontent:
#...
translations:
de:
title_prefix: '[Verschlüsselt] '
summary: 'Der Inhalt dieser Seite ist mit AES verschlüsselt. '
placeholder: 'Mit Strg+Enter wird das Passwort global gesetzt'
password_button_text: 'Entschlüsseln'
decryption_failure_message: 'Falsches passwort.'
encryption_info_message: 'Bitte wenden Sie sich an den Systemadministrator um auf diese Seite zuzugreifen.'
```

### Custom per-page strings

You can set the meta tag `encryption_summary` to customize `summary` and `encryption_info_message` on every page.

## Obfuscate pages

If you want to make it harder for search engines to scrape you pages content,
you can set `obfuscate: SomeNotSoSecretPassword` meta tag in markdown.

The page then will display `summary` and `encryption_info_message` together with a button labeled with
`password_button_text`. In order to view the pages content one hast to press the button first.

If a `password` or `level` is set, then the `obfuscate` feature will be disabled.
If you want to use `obfuscate` in a configuration where `global_password` or `_global` level is defined,
you'll need to set the `password:` or rather `level:` meta tag (with no password/level defined) to undefine the password on this page.

The keys to all obfuscated pages are also saved in every keystore, so they are decrypted if someone entered
correct credentials.

## Example plugin configuration

```yaml
plugins:
- encryptcontent:
title_prefix: ''
summary: ''
placeholder: 'Password'
placeholder_user: User
password_button_text: 'ENTER'
decryption_failure_message: 'Wrong user name or password.'
encryption_info_message: 'Legitimation required.'
translations:
de:
title_prefix: ''
summary: ''
placeholder: 'Passwort'
placeholder_user: Benutzer
password_button_text: 'ENTER'
decryption_failure_message: 'Falscher Benutzer oder Passwort.'
encryption_info_message: 'Legitimation erforderlich.'
html_template_path: "decrypt-form.tpl.html" # override default html template
password_button: True
input_class: 'w3-input' # CSS class used for input username and password
button_class: 'w3-button w3-theme-l1 w3-hover-theme' # CSS class for password_button
hljs: False
arithmatex: False
mermaid2: False
remember_keys: true # keys from keystore will temporarily saved to sessionStorage
remember_password: false # the entered credentials are not saved
remember_prefix: encryptcontent_plugin_ # use different prefixes if other sites are running on the same domain
encrypted_something: # additionally encrypt some html elements
#myNav: [div, id]
myToc: [div, id]
myTocButton: [div, id]
search_index: 'dynamically' # dynamically encrypt mkdocs search index
webcrypto: true # use browsers webcrypto support
#selfhost: true # use self-hosted crypto-js
#selfhost_download: true # download crypt-js for self-hosting
#selfhost_dir: 'theme_override' # where to download crypto-js
#reload_scripts:
# - '#theme'
password_file: 'passwords.yml' # file with password inventory
#kdf_pow: 4 # default for crypto-js: 4, default for webcrypto: 5
sign_files: 'encryptcontent-plugin.json' # save ed25519 signatures here
#hash_filenames: # add hash to file names of assets (to make them impossible to guess
# extensions:
# - 'png'
# - 'jpg'
# - 'jpeg'
# - 'svg'
# except:
# - 'logo.svg'
```

# Features

## Override default templates

Related to [issue #32](https://github.com/unverbuggt/mkdocs-encryptcontent-plugin/issues/32)

You can override the default templates with your own templates by providing an actual replacement
path in the `html_template_path` *(HTML)* and `js_template_path` *(JS)* directives.
Overridden templates **completely** replace the default templates. You **must** therefore copy the
default templates as a starting point to keep this plugin working.

```yaml
plugins:
- encryptcontent:
html_template_path: "/root/real/path/mkdocs_poc/my_html_template.html"
js_template_path: "/root/real/path/mkdocs_poc/my_js_template.js"
form_class: 'md-content__inner md-typeset'
input_class: 'md-input'
button_class: 'md-button md-button--primary'
```

Use `form_class`, `input_class` and `button_class` to optionally set a CSS class for the password input field and the button.

When overriding the default templates, you can add and use your own Jinja variables
and enrich your template, by defining `html_extra_vars` and `js_extra_vars` directives in key/value format.
Added values can be used in your Jinja templates via the variable `extra`.

```yaml
plugins:
- encryptcontent:
html_extra_vars:
my_extra: "extra value"
:
js_extra_vars:
my_extra: "extra value"
:
```

For example, you can modify your HTML template, to add a new title with your own text variable.

```jinja
[ ... ]

{{ extra.my_extra }}


[ ... ]
```

> **NOTE** Issues related to template override will not be addressed.

## Add button

Add `password_button: True` in plugin configuration variable, to add a button to the right of the password field.

When enabled, it allows to decrypt the content just like the classic keypress ENTER.

Optionally, you can add `password_button_text: 'custom_text_button'` to customize the button text.

```yaml
plugins:
- encryptcontent:
password_button: True
password_button_text: 'custom_text_button'
```

## Tag encrypted page

> **Enable by default**

Related to [issue #7](https://github.com/unverbuggt/mkdocs-encryptcontent-plugin/issues/7)

This feature adds an additional attribute `encrypted` with value `True` to the mkdocs type `mkdocs.nav.page` object.

You can add `tag_encrypted_page: False` in plugin configuration, to disable tagging of encrypted pages.

When enabled, it is possible to use the `encrypted` attribute in the jinja template of your theme, as a condition to perform custom modification.

```jinja
{%- for nav_item in nav %}
{% if nav_item.encrypted %}

{% endif %}
{%- endfor %}
```

For example, you can use conditional check to add a custom class:

```jinja
{{ nav_item.title }}
```

## Remember password

Related to [issue #6](https://github.com/unverbuggt/mkdocs-encryptcontent-plugin/issues/6)

By default the plugin will save the decrypted AES keys to session storage of the browser (can be disabled by setting `remember_keys: False`).
This is enabled for convenience, so you are able to browse between multiple encrypted pages without having to re-enter the password.

Additionally it is possible to save the entered user/password in session storage (setting `remember_password: True`). Use this for
additional convenience during `mkdocs serve`, because the AES key are regenerated every time MkDocs starts
(rendering the old ones invalid and requiring to re-enter a valid credential again).

To avoid problems when multiple sites are hosted within the same domain, it is possible to customize the name of
the keys saved to storage with `remember_prefix`.

> **This feature is not really secure !** decryption keys are store in clear text inside session storage.
>
> Instead of using these features, I recommend to use a password manager with its browser plugin.
> For example **KeepassXC** allows you to detect the password field
> `mkdocs-content-password` and fill it automatically in a much more secure way.

It is also possible to save the used credentials permanently to local storage (setting `session_storage: False`), but
this should only be done for testing purposes. The local storage of a browser is most likely readable
for every other process that can access the file system.

The session storage however should only be located in memory and be forgotten after the browser tab is closed.

```yaml
plugins:
- encryptcontent:
remember_keys : True
remember_password: False
remember_prefix: secretsite_
```

## Share link generation

It is possible to share valid credentials by adding them to the hash part of the URL.
The plugin can also generate share links for certain pages if the meta tag `sharelink: true`
is defined in markdown.
It will use the first credential for the pages level or the pages password.
The credentials for auto-generated links are base64url encoded.

```yaml
plugins:
- encryptcontent:
sharelinks: True
sharelinks_output: 'sharelinks.txt' # generated share links are saved here
```
> One condition applies: The user name must not contain the ":" character (Passwords may use that character).

However if `sharelinks: True` is enabled in the plugin configuration you can generate share links yourself:\
`https://your-domain.com/your-site/protected-page/#!username:password` for user/password or\
`https://your-domain.com/your-site/protected-page/#!password` for only password.

> Then another condition applies: If non-aphanumeric characters are used in user/password,
> they need to be URLencoded (f.ex. %20 = space character). Some browsers may do that automatically (Do a copy/paste from the browsers address bar then).

### Incomplete Share links

Since version 3.0.3 it is possible to leave out one part of the password when share links are generated via meta tag.
To do this use the ":" character in a password to divide the part that is incorporated to the share link and the part that remains secret,
like "PartThatIsEncodedToTheShareLink:PartThatRemainsSecret".
The feature is enabled by setting the option `sharelinks_incomplete: true`.
If the password that is read from the share link ends with the ":" character, then an additional password input field is displayed for entering the secret part.

> If the feature is used, then passwords must not end with the ":" character.

## Storage of additional variables in keystore

Since version 3.0.3 it is possible to set arbitrary session store variables after decryption.
These can be used by javascript functions f. ex. to read API tokens or show the user name or set visibility of menu entries.
The variables are encrypted together with the content keys in the keystore.

```yaml
plugins:
- encryptcontent:
additional_storage_file: 'additional_storage.yaml'
```

The file may contain a `userpass:` dictionary, wich refers to user names
and/or a `password:` dictionary, that refers to password-only credentials.
It may contain a arbitrary number of key/value pairs,
but keep in mind to check if the key name is reasonable and not already used by other parts of the page
(as it will be overwritten after decryption).

```yaml
userpass:
alice:
username: Alice McAliceface
userid: 1
token: uCZDqa2vzuSPFX-o9TebinUAnD9sePJqpPYhavMkCH7gGo7lhjWRS17-HgBys9UKnFR37zXO4Q_f1_ywZJqBlA==

password:
Head32_Sculpture_bovine_:
username: Head McPassword
```
## Modify generated pages

### Encrypt something

Related to [issue #9](https://github.com/unverbuggt/mkdocs-encryptcontent-plugin/issues/9)

Add `encrypted_something: {}` in the plugin configuration variable, to encrypt something else on the page.

The syntax of this new variable **MUST** follow the yaml format of a dictionary.
Child elements of `encrypted_something` are build with a key `` in string format and a list as value.
The list has to be constructed with the name of an HTML tag `` as first item
and `id` or `class` as the second item.

```yaml
plugins:
- encryptcontent:
encrypted_something:
: [, <'class' or 'id'>]
```

The `` key identifies the name of a specific element of the page that will be
searched by Beautiful Soup.
The first value of the list (``) identifies the type of HTML tag in which the name is present.
The second value of the list (`'id'` or `'class'`) specifies the type of the attribute which contains
the unique name in the html tag.

Prefer to use an `'id'`, however depending on the template of your theme, it is not always possible to use the id.
So we can use the class attribute to define your unique name inside the html tag.
Beautiful Soup will encrypt all HTML elements discovered with the class.

When the feature is enabled every element is decrypted upon successfull content decryption on the page.

> Use this to hide the table of contents on the page or sub pages in the menu

By default **every child item** are encrypted and the encrypted elements have set
`style="display:none;"` to hide their content.

#### How to use it?! Examples

You can use the `page.encrypted` tag to add attributes to html tags in the HTML templates of your theme
or identify usable tag ids or classes that are already in the theme.

Then add these elements in the format of a yaml dictionary under the variable `encrypted_something`.

1. For example, encrypt ToC in a theme where ToC is under 'div' element like this :

```jinja


```

Set your configuration like this :

```yaml
plugins:
- encryptcontent:
encrypted_something:
mkdocs-encrypted-toc: [div, id]
```

2. Other example, with multiple targets. You are using a custom version of Material theme and
want to encrypt ToC content and Footer.

Material generates 2 `` structures with the same template `toc.html`, so you need to use a `class`
instead of an id for this part.
The footer part, is generated by the `footer.html` template in a classic div so an `id` is sufficient

After modification, your template looks like this :

toc.html:
```jinja

...

    ...

```
footer.html:
```jinja