Repository: ItalyPaleAle/hereditas Branch: master Commit: 097f9d2b9d91 Files: 120 Total size: 280.5 KB Directory structure: gitextract_d_jnmmz2/ ├── .eslintignore ├── .eslintrc.js ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── docs-ci.yaml │ ├── docs-production.yaml │ └── docs-staging.yaml ├── .gitignore ├── .npmignore ├── .vscode/ │ └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── app/ │ ├── components/ │ │ ├── NavBar.svelte │ │ ├── PassphraseBox.svelte │ │ ├── RequestAuthentication.svelte │ │ └── UserProfile.svelte │ ├── layout/ │ │ └── App.svelte │ ├── lib/ │ │ ├── Base64Utils.js │ │ ├── Box.js │ │ ├── Credentials.js │ │ ├── CryptoUtils.js │ │ ├── StorageService.js │ │ └── Utils.js │ ├── main.css │ ├── main.html │ ├── main.js │ ├── postcss.config.js │ ├── robots.txt │ ├── routes.js │ ├── stores.js │ ├── tailwind.config.js │ ├── views/ │ │ ├── ContentView.svelte │ │ ├── ListView.svelte │ │ └── UnlockView.svelte │ └── webpack.config.js ├── auth0/ │ ├── 01-whitelist.js │ ├── 02-notify.js │ └── 03-wait-logic.js ├── bin/ │ ├── run │ └── run.cmd ├── cli/ │ ├── commands/ │ │ ├── auth0/ │ │ │ └── sync.js │ │ ├── build.js │ │ ├── init.js │ │ ├── pack.js │ │ ├── regenerate-token.js │ │ ├── url/ │ │ │ ├── add.js │ │ │ ├── list.js │ │ │ └── rm.js │ │ ├── user/ │ │ │ ├── add.js │ │ │ ├── list.js │ │ │ └── rm.js │ │ ├── wait-time/ │ │ │ ├── get.js │ │ │ └── set.js │ │ └── webhook/ │ │ ├── get.js │ │ └── set.js │ ├── index.js │ └── lib/ │ ├── Auth0Management.js │ ├── Builder.js │ ├── Config.js │ ├── Content.js │ ├── Crypto.js │ ├── Utils.js │ └── aes-kw.js ├── docs-source/ │ ├── .gitignore │ ├── config.yaml │ ├── content/ │ │ ├── _index.md │ │ ├── advanced/ │ │ │ ├── auth0-manual-configuration.md │ │ │ ├── building-self-contained-binaries.md │ │ │ ├── configuration-file.md │ │ │ └── index-file.md │ │ ├── cli/ │ │ │ └── __template.md │ │ ├── guides/ │ │ │ ├── auth0-setup.md │ │ │ ├── build-static-web-app.md │ │ │ ├── create-box.md │ │ │ ├── deploy-box.md │ │ │ ├── get-started.md │ │ │ ├── login-notifications.md │ │ │ └── managing-users.md │ │ ├── introduction/ │ │ │ ├── quickstart-video.md │ │ │ └── security-model.md │ │ └── menu/ │ │ └── __template.md │ ├── generate-cli-docs.js │ ├── sync-assets.sh │ ├── themes/ │ │ └── book/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── archetypes/ │ │ │ └── docs.md │ │ ├── assets/ │ │ │ ├── _markdown.scss │ │ │ ├── _utils.scss │ │ │ ├── _variables.scss │ │ │ └── book.scss │ │ ├── layouts/ │ │ │ ├── 404.html │ │ │ ├── docs/ │ │ │ │ ├── baseof.html │ │ │ │ ├── list.html │ │ │ │ └── single.html │ │ │ ├── partials/ │ │ │ │ └── docs/ │ │ │ │ ├── brand.html │ │ │ │ ├── git-footer.html │ │ │ │ ├── html-head.html │ │ │ │ ├── inject/ │ │ │ │ │ ├── body.html │ │ │ │ │ ├── head.html │ │ │ │ │ ├── menu-after.html │ │ │ │ │ └── menu-before.html │ │ │ │ ├── menu-bundle.html │ │ │ │ ├── menu-filetree.html │ │ │ │ ├── menu.html │ │ │ │ ├── mobile-header.html │ │ │ │ ├── shared.html │ │ │ │ └── toc.html │ │ │ └── posts/ │ │ │ ├── baseof.html │ │ │ ├── list.html │ │ │ └── single.html │ │ ├── source │ │ └── theme.toml │ ├── workers-site/ │ │ ├── .cargo-ok │ │ ├── .gitignore │ │ ├── assets.js │ │ ├── cache-config.js │ │ ├── index.js │ │ └── package.json │ └── wrangler.toml └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ # Docs docs-source # Auth0 rules (they follow a different style) auth0 # Test data testfolder ================================================ FILE: .eslintrc.js ================================================ module.exports = { env: { es6: true, node: true, browser: true }, extends: 'eslint:recommended', parserOptions: { ecmaVersion: 2019, sourceType: 'module' }, plugins: [ 'html', 'svelte3' ], overrides: [ { files: '**/*.svelte', processor: 'svelte3/svelte3' } ], globals: { // See https://github.com/eslint/eslint/issues/11524 BigInt: true }, settings: { 'svelte3/ignore-styles': () => true, 'html': { 'indent': 0, 'report-bad-indent': 'warn', 'html-extensions': [ '.html' ] } }, rules: { 'indent': [ 'error', 4, { SwitchCase: 1, MemberExpression: 1, ArrayExpression: 1, ObjectExpression: 1 } ], 'linebreak-style': [ 'error', 'unix' ], 'quotes': [ 'error', 'single' ], 'semi': [ 'error', 'never' ], 'quote-props': [ 'warn', 'as-needed' ], 'no-var': [ 'error' ], 'prefer-const': [ 'warn' ], 'no-unused-vars': [ 'error', { args: 'none' } ], 'brace-style': [ 'error', 'stroustrup', { allowSingleLine: false } ], 'eol-last': [ 'error', 'always' ], 'space-before-function-paren': [ 'error', { anonymous: 'never', named: 'never', asyncArrow: 'always' } ], 'keyword-spacing': [ 'error', { before: true, after: true } ], 'key-spacing': [ 'error', { beforeColon: false, afterColon: true, mode: 'strict' } ], 'comma-spacing': [ 'error' ], 'arrow-spacing': [ 'error' ], 'array-bracket-spacing': [ 'error', 'never', { singleValue: false, objectsInArrays: true, arraysInArrays: true } ], 'curly': [ 'error' ], 'space-infix-ops': [ 'error', { int32Hint: false } ], 'space-unary-ops': [ 'error', { words: true, nonwords: false } ], 'space-before-blocks': [ 'error' ], 'object-curly-spacing': [ 'error', 'never' ], 'space-in-parens': [ 'error', 'never' ], 'prefer-arrow-callback': [ 'warn' ], 'no-return-await': [ 'error' ], 'no-console': [ 'warn' ], 'no-nested-ternary': [ 'error' ], 'no-unneeded-ternary': [ 'warn' ], 'no-unexpected-multiline': [ 'error' ], 'lines-around-directive': [ 'error', 'always' ], // Need to disable this because it causes issues with Svelte 'no-multiple-empty-lines': 'off', 'operator-linebreak': [ 'error', 'after' ] } } ================================================ FILE: .github/FUNDING.yml ================================================ github: ItalyPaleAle patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: italypaleale tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ['https://keybase.io/italypaleale'] ================================================ FILE: .github/workflows/docs-ci.yaml ================================================ # Docs: CI: deploys to dev # Required secrets: # CF_ACCOUNT_ID: Account ID for Cloudflare Workers # CF_API_TOKEN: API token for Cloudflare (for the Workers CLI) # AZURE_STORAGE_ACCOUNT: Name of the Azure Storage Account # AZCOPY_SPA_APPLICATION_ID: Application ID (Client ID) for the Service Principal with access to the Azure Storage account # AZCOPY_SPA_CLIENT_SECRET: Client Secret for the Service Principal # AZCOPY_SPA_TENANT_ID: Tenant ID of the application (Service Principal) name: 'Docs: CI' on: push: branches: - master jobs: build-and-deploy: runs-on: 'ubuntu-20.04' env: # Version of Hugo to use HUGO_VERSION: '0.68.1' steps: - name: 'Check out code' uses: 'actions/checkout@v2' - name: 'Install Node.js' uses: 'actions/setup-node@v1' with: node-version: '14.x' - name: 'Install npm deps' run: | npm ci - name: 'Install Hugo' run: | cd docs-source mkdir -p .bin cd .bin echo "Using Hugo ${HUGO_VERSION}" curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz" -o hugo.tar.gz tar -zxf hugo.tar.gz - name: 'Build site' run: | # Build the site cd docs-source node generate-cli-docs.js .bin/hugo - name: 'Install azcopy and authenticate' run: | cd docs-source mkdir -p .bin curl -Ls "https://aka.ms/downloadazcopy-v10-linux" -o ".bin/azcopy.tar.gz" (cd .bin && tar -xvzf azcopy.tar.gz --strip 1) .bin/azcopy --version .bin/azcopy login --service-principal --application-id $AZCOPY_SPA_APPLICATION_ID --tenant-id $AZCOPY_SPA_TENANT_ID env: # Service Principal credentials AZCOPY_SPA_APPLICATION_ID: ${{ secrets.AZCOPY_SPA_APPLICATION_ID }} AZCOPY_SPA_CLIENT_SECRET: ${{ secrets.AZCOPY_SPA_CLIENT_SECRET }} AZCOPY_SPA_TENANT_ID: ${{ secrets.AZCOPY_SPA_TENANT_ID }} - name: 'Upload static assets to Azure Storage' run: | cd docs-source # Upload assets to Azure Storage ./sync-assets.sh # Delete the assets from disk so they're not uploaded to Cloudflare or published as artifact for asset in $ASSETS; do rm -rvf "$asset"; done env: # List of assets to upload ASSETS: 'public/images public/svg' # Container in Azure Storage CONTAINER: 'hereditas-dev' # Use azcopy downloaded above AZCOPYCMD: '.bin/azcopy' # Storage Account name AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }} - name: 'Publish docs as artifact' uses: 'actions/upload-artifact@v2' with: name: 'docs-dev' path: 'docs-source/public' - name: 'Deploy to dev environment' uses: cloudflare/wrangler-action@1.3.0 with: apiToken: ${{ secrets.CF_API_TOKEN }} workingDirectory: 'docs-source' env: CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} ================================================ FILE: .github/workflows/docs-production.yaml ================================================ # Docs: Deploys to production, triggered manually # Required secrets: # CF_ACCOUNT_ID: Account ID for Cloudflare Workers # CF_API_TOKEN: API token for Cloudflare (for the Workers CLI) # CF_ZONE_ID: Zone ID for the Cloudflare domain # AZURE_STORAGE_ACCOUNT: Name of the Azure Storage Account # AZCOPY_SPA_APPLICATION_ID: Application ID (Client ID) for the Service Principal with access to the Azure Storage account # AZCOPY_SPA_CLIENT_SECRET: Client Secret for the Service Principal # AZCOPY_SPA_TENANT_ID: Tenant ID of the application (Service Principal) name: 'Docs: Production' on: workflow_dispatch: jobs: build-and-deploy: runs-on: 'ubuntu-20.04' env: # Version of Hugo to use HUGO_VERSION: '0.68.1' steps: - name: 'Check out code' uses: 'actions/checkout@v2' - name: 'Install Node.js' uses: 'actions/setup-node@v1' with: node-version: '14.x' - name: 'Install npm deps' run: | npm ci - name: 'Install Hugo' run: | cd docs-source mkdir -p .bin cd .bin echo "Using Hugo ${HUGO_VERSION}" curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz" -o hugo.tar.gz tar -zxf hugo.tar.gz - name: 'Build site' run: | # Build the site cd docs-source node generate-cli-docs.js .bin/hugo - name: 'Install azcopy and authenticate' run: | cd docs-source mkdir -p .bin curl -Ls "https://aka.ms/downloadazcopy-v10-linux" -o ".bin/azcopy.tar.gz" (cd .bin && tar -xvzf azcopy.tar.gz --strip 1) .bin/azcopy --version .bin/azcopy login --service-principal --application-id $AZCOPY_SPA_APPLICATION_ID --tenant-id $AZCOPY_SPA_TENANT_ID env: # Service Principal credentials AZCOPY_SPA_APPLICATION_ID: ${{ secrets.AZCOPY_SPA_APPLICATION_ID }} AZCOPY_SPA_CLIENT_SECRET: ${{ secrets.AZCOPY_SPA_CLIENT_SECRET }} AZCOPY_SPA_TENANT_ID: ${{ secrets.AZCOPY_SPA_TENANT_ID }} - name: 'Upload static assets to Azure Storage' run: | cd docs-source # Upload assets to Azure Storage ./sync-assets.sh # Delete the assets from disk so they're not uploaded to Cloudflare or published as artifact for asset in $ASSETS; do rm -rvf "$asset"; done env: # List of assets to upload ASSETS: 'public/images public/svg' # Container in Azure Storage CONTAINER: 'hereditas-prod' # Use azcopy downloaded above AZCOPYCMD: '.bin/azcopy' # Storage Account name AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }} - name: 'Publish docs as artifact' uses: 'actions/upload-artifact@v2' with: name: 'docs-prod' path: 'docs-source/public' - name: 'Deploy to production environment' uses: cloudflare/wrangler-action@1.3.0 with: apiToken: ${{ secrets.CF_API_TOKEN }} environment: 'production' workingDirectory: 'docs-source' env: CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }} ================================================ FILE: .github/workflows/docs-staging.yaml ================================================ # Docs: Deploys to staging, triggered manually # Required secrets: # CF_ACCOUNT_ID: Account ID for Cloudflare Workers # CF_API_TOKEN: API token for Cloudflare (for the Workers CLI) # CF_ZONE_ID: Zone ID for the Cloudflare domain # AZURE_STORAGE_ACCOUNT: Name of the Azure Storage Account # AZCOPY_SPA_APPLICATION_ID: Application ID (Client ID) for the Service Principal with access to the Azure Storage account # AZCOPY_SPA_CLIENT_SECRET: Client Secret for the Service Principal # AZCOPY_SPA_TENANT_ID: Tenant ID of the application (Service Principal) name: 'Docs: Staging' on: workflow_dispatch: jobs: build-and-deploy: runs-on: 'ubuntu-20.04' env: # Version of Hugo to use HUGO_VERSION: '0.68.1' steps: - name: 'Check out code' uses: 'actions/checkout@v2' - name: 'Install Node.js' uses: 'actions/setup-node@v1' with: node-version: '14.x' - name: 'Install npm deps' run: | npm ci - name: 'Install Hugo' run: | cd docs-source mkdir -p .bin cd .bin echo "Using Hugo ${HUGO_VERSION}" curl -fsSL "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz" -o hugo.tar.gz tar -zxf hugo.tar.gz - name: 'Build site' run: | # Build the site cd docs-source node generate-cli-docs.js .bin/hugo - name: 'Install azcopy and authenticate' run: | cd docs-source mkdir -p .bin curl -Ls "https://aka.ms/downloadazcopy-v10-linux" -o ".bin/azcopy.tar.gz" (cd .bin && tar -xvzf azcopy.tar.gz --strip 1) .bin/azcopy --version .bin/azcopy login --service-principal --application-id $AZCOPY_SPA_APPLICATION_ID --tenant-id $AZCOPY_SPA_TENANT_ID env: # Service Principal credentials AZCOPY_SPA_APPLICATION_ID: ${{ secrets.AZCOPY_SPA_APPLICATION_ID }} AZCOPY_SPA_CLIENT_SECRET: ${{ secrets.AZCOPY_SPA_CLIENT_SECRET }} AZCOPY_SPA_TENANT_ID: ${{ secrets.AZCOPY_SPA_TENANT_ID }} - name: 'Upload static assets to Azure Storage' run: | cd docs-source # Upload assets to Azure Storage ./sync-assets.sh # Delete the assets from disk so they're not uploaded to Cloudflare or published as artifact for asset in $ASSETS; do rm -rvf "$asset"; done env: # List of assets to upload ASSETS: 'public/images public/svg' # Container in Azure Storage CONTAINER: 'hereditas-staging' # Use azcopy downloaded above AZCOPYCMD: '.bin/azcopy' # Storage Account name AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE_ACCOUNT }} - name: 'Publish docs as artifact' uses: 'actions/upload-artifact@v2' with: name: 'docs-staging' path: 'docs-source/public' - name: 'Deploy to staging environment' uses: cloudflare/wrangler-action@1.3.0 with: apiToken: ${{ secrets.CF_API_TOKEN }} environment: 'staging' workingDirectory: 'docs-source' env: CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }} ================================================ FILE: .gitignore ================================================ # Test data testfolder # OClif manifest oclif.manifest.json # Created by https://www.gitignore.io/api/node,macos,linux,windows,visualstudiocode # Edit at https://www.gitignore.io/?templates=node,macos,linux,windows,visualstudiocode ### Linux ### *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### macOS ### # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json ### VisualStudioCode Patch ### # Ignore all local history of files .history ### Windows ### # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # End of https://www.gitignore.io/api/node,macos,linux,windows,visualstudiocode ================================================ FILE: .npmignore ================================================ .DS_Store assets/ .vscode/ docs-source/ testfolder/ azure-pipelines.yaml ================================================ FILE: .vscode/settings.json ================================================ { "files.exclude": { "**/node_modules": true, "coverage": true, "coverage.lcov": true }, "files.trimTrailingWhitespace": true, "editor.tabSize": 4, "files.insertFinalNewline": true, "eslint.options": { "configFile": ".eslintrc.yml" }, "eslint.validate": [ "javascript", "html", "svelte" ] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE.md ================================================ # License Copyright © 2019-2020 Alessandro Segala @ItalyPaleAle. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ````text GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ```` ================================================ FILE: README.md ================================================ # Hereditas [![Open in Visual Studio Code](https://open.vscode.dev/badges/open-in-vscode.svg)](https://open.vscode.dev/ItalyPaleAle/hereditas) [![Version](https://img.shields.io/npm/v/hereditas.svg)](https://npmjs.org/package/hereditas) [![Downloads/week](https://img.shields.io/npm/dw/hereditas.svg)](https://npmjs.org/package/hereditas) [![License](https://img.shields.io/npm/l/hereditas.svg)](https://github.com/ItalyPaleAle/hereditas/blob/master/package.json) ## What happens to your digital life after you're gone? ![Hereditas logo](./assets/hereditas-logo.png) Hereditas, which means *inheritance* in Latin, is a static website generator that builds **fully-trustless digital legacy boxes**, where you can store information for your relatives to access in case of your sudden death or disappearance. For example, you could use this to pass information such as passwords, cryptographic keys, cryptocurrency wallets, sensitive documents, etc. ## Learn more Read the [Hereditas announcement](https://withblue.ink/2019/03/18/what-happens-to-your-digital-life-after-youre-gone-introducing-hereditas.html?utm_source=web&utm_campaign=hereditas-github) to understand more on why we need Hereditas. You can also watch this short [intro video](https://www.youtube.com/watch?v=lZEKgB5dzQ4). ## Get started and documentation ❓ [**What is Hereditas**](https://hereditas.app) 🚀 [**Get started guide**](https://hereditas.app/guides/get-started.html) 🔐 [**Security model**](https://hereditas.app/introduction/security-model.html) 📘 [**Documentation and CLI reference**](https://hereditas.app) ## Screenshot ![Screenshot of Hereditas 0.2](./screenshot.png) ## Warning: alpha quality software **Hereditas is currently alpha quality software; use at your own risk.** While we've developed Hereditas with security always as the top priority, this software leverages a lot of cryptographic primitives under the hood. We won't release a stable (e.g. "1.0") version of Hereditas until we're confident that enough people and cryptography experts have audited and improved the code. **Your help is highly appreciated.** If you are an expert on security or cryptography, please help us reviewing the code and let us know what you think - including if everything looks fine, or if you found a bug. Responsible disclosure: if you believe you've found a security issue that could compromise current users of Hereditas, please [report it confidentially](https://www.npmjs.com/advisories/report?package=hereditas). ## License Copyright © 2020, Alessandro Segala @ItalyPaleAle This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You can read the full text of the license in the [LICENSE.md](./LICENSE.md) file. ================================================ FILE: app/components/NavBar.svelte ================================================ ================================================ FILE: app/components/PassphraseBox.svelte ================================================ {#await $box.fetchIndex()}

Fetching index, please wait…

{:then response}
{#if unlockError}

This passphrase isn't correct

{/if}
{:catch error}

Error while fetching the index: {error}

{/await} ================================================ FILE: app/components/RequestAuthentication.svelte ================================================ {#if $authError}

Authentication error

Error description: {$authError}

Try authenticating again {:else}

Authenticate with this Hereditas box

Authenticate {/if} ================================================ FILE: app/components/UserProfile.svelte ================================================

Hello, {$profile.name}!

{#if $hereditasProfile.role == 'owner'}

You're the owner of this Hereditas box, so you can unlock it at any time.

{:else} {#if $hereditasProfile.token}

You can now access to the content of this Hereditas.

{:else} {/if} {/if} ================================================ FILE: app/layout/App.svelte ================================================
================================================ FILE: app/lib/Base64Utils.js ================================================ // Based on: https://github.com/danguer/blog-examples/blob/master/js/base64-binary.js /* Copyright (c) 2011, Daniel Guerrero All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DANIEL GUERRERO BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * Uses the new array typed in javascript to binary base64 encode/decode * at the moment just decodes a binary base64 encoded * into either an ArrayBuffer (decodeArrayBuffer) * or into an Uint8Array (decode) * * References: * https://developer.mozilla.org/en/JavaScript_typed_arrays/ArrayBuffer * https://developer.mozilla.org/en/JavaScript_typed_arrays/Uint8Array */ const keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' /* will return a Uint8Array type */ export function DecodeArrayBuffer(input) { input = RemovePaddingChars(input) const bytes = (input.length / 4) * 3 const ab = new ArrayBuffer(bytes) Decode(input, ab) return ab } function RemovePaddingChars(input) { for (let i = 0; i < Math.min(2, input.length); i++) { if (input.charAt(input.length - 1) == keyStr[64]) { input = input.substring(0, input.length - 1) } } return input } export function Decode(input, arrayBuffer) { input = RemovePaddingChars(input) const bytes = parseInt((input.length / 4) * 3, 10) let uarray let chr1, chr2, chr3 let enc1, enc2, enc3, enc4 let i = 0 let j = 0 if (arrayBuffer) { uarray = new Uint8Array(arrayBuffer) } else { uarray = new Uint8Array(bytes) } input = input.replace(/[^A-Za-z0-9+/=]/g, '') for (i = 0; i < bytes; i += 3) { //get the 3 octects in 4 ascii chars enc1 = keyStr.indexOf(input.charAt(j++)) enc2 = keyStr.indexOf(input.charAt(j++)) enc3 = keyStr.indexOf(input.charAt(j++)) enc4 = keyStr.indexOf(input.charAt(j++)) chr1 = (enc1 << 2) | (enc2 >> 4) chr2 = ((enc2 & 15) << 4) | (enc3 >> 2) chr3 = ((enc3 & 3) << 6) | enc4 uarray[i] = chr1 if (enc3 != 64) { uarray[i + 1] = chr2 } if (enc4 != 64) { uarray[i + 2] = chr3 } } return uarray } ================================================ FILE: app/lib/Box.js ================================================ import {Decrypt, buf2str, UnwrapKey, DeriveKeyArgon2, DeriveKeyPBKDF2} from './CryptoUtils' import {DecodeArrayBuffer} from './Base64Utils' /** * Manages the Hereditas box */ export class Box { constructor() { this._masterKey = null this._contents = null this._indexFetchingPromise = null this._encryptedIndex = null } /** * Returns true if the box is unlocked * * @returns {boolean} */ isUnlocked() { return this._masterKey && this._contents } /** * Lock the box again, removing the key and the decrypted index from memory */ lock() { this._masterKey = null this._contents = null } /** * Returns decrypted index * @returns {Array} */ getContents() { return this.isUnlocked() ? this._contents : [] } /** * Fetches a content from the box, then decrypts it before returning. * * @param {Object} info - Info for the content to retrieve. Must contain the `dist` and `tag` properties. * @returns {Promise} Promise that resolves with info object containing the decrypted content, as a binary ArrayBuffer in the `info.data` property, or an utf-8 encoded string in the `info.text` property (if `info.display` is "text" or "html"). * @async */ fetchContent(info) { // If the box is locked, return if (!this.isUnlocked()) { return Promise.reject('Box is locked') } // Ensure we have what we need if (!info || !info.dist || !info.tag) { return Promise.reject('Content not found') } // Return the promise let iv = null let data = null return fetch(info.dist) // Grab the encrypted contents as ArrayBuffer .then((response) => response.arrayBuffer()) // Decrypt the data .then((buffer) => { // The first 40 bytes are the wrapped key, and the next 12 bytes are the IV const wrappedKey = buffer.slice(0, 40) iv = buffer.slice(40, 52) data = buffer.slice(52) // Un-wrap the key return UnwrapKey(this._masterKey, wrappedKey) }) .then((key) => { // Get the tag const tag = DecodeArrayBuffer(info.tag) return Decrypt(key, iv, data, tag) .then((data) => { // Clone the info object info = JSON.parse(JSON.stringify(info)) // If it's text, decode it if (info.display == 'text' || info.display == 'html') { info.text = buf2str(new Uint8Array(data)) } else { info.data = data } return info }) }) } /** * Fetches the index from the box. * * @returns {Promise} Promise that resolves (with no value) when the index has been fetched * @async */ fetchIndex() { // If we have the index already, do nothing if (this._encryptedIndex) { return Promise.resolve() } // If we're already fetching the index, return the promise if (this._indexFetchingPromise) { return this._indexFetchingPromise } // Fetch the index this._indexFetchingPromise = fetch('_index') // Grab the contents as ArrayBuffer .then((response) => response.arrayBuffer()) // Store the results in the object .then((buffer) => { // Read the data from the response this._encryptedIndex = { // The first 40 bytes are the wrapped key, and the next 12 bytes are the IV wrappedKey: buffer.slice(0, 40), iv: buffer.slice(40, 52), data: buffer.slice(52) } // Request is done this._indexFetchingPromise = null }) // Return the promise return this._indexFetchingPromise } /** * Attempts to decrypt the data using the passphrase and the app token * * @param {string} passphrase - Passphrase typed by the user * @param {string} appToken - Encryption token for the app * @async * @throws Throws an exception if the decryption fails, which usually means that the key/passphrase is wrong */ unlock(passphrase, appToken) { if (!passphrase || !appToken) { return Promise.reject('Empty passphrase or app token') } // If we haven't fetched the index yet, return if (!this._encryptedIndex) { return Promise.resolve(false) } // Convert from Base64 to ArrayBuffer const keySalt = DecodeArrayBuffer(process.env.KEY_SALT) const indexTag = DecodeArrayBuffer(process.env.INDEX_TAG) // Key derivation function: PBKDF2 or Argon2 let kdf if (process.env.KEY_DERIVATION_FUNCTION == 'pbkdf2') { kdf = DeriveKeyPBKDF2 } else if (process.env.KEY_DERIVATION_FUNCTION == 'argon2') { kdf = DeriveKeyArgon2 } else { throw Error('Invalid key derivation function requested') } // Try decrypting the index: this will verify the passphrase too return Promise.resolve() // First: derive the encryption key .then(() => kdf(passphrase + appToken, keySalt)) .then((masterKey) => { this._masterKey = masterKey }) // Un-wrap the key .then(() => UnwrapKey(this._masterKey, this._encryptedIndex.wrappedKey)) // Decrypt the index .then((key) => Decrypt(key, this._encryptedIndex.iv, this._encryptedIndex.data, indexTag)) .then((data) => { // Convert the buffer to string const str = buf2str(new Uint8Array(data)) // Store the contents this._contents = JSON.parse(str) }) // Exceptions likely mean that the key/passphrase are wrong .catch((err) => { // eslint-disable-next-line no-console console.error('Error while unlocking the box:', err) // Ensure the box remains locked this.lock() // Bubble up throw Error('Failed to unlock to box') }) } } ================================================ FILE: app/lib/Credentials.js ================================================ import {RandomString} from './Utils' import storage from './StorageService' import IdTokenVerifier from 'idtoken-verifier' /** * During the authentication process we need to use nonce's to protect against certain kinds of attacks. */ class Nonce { constructor() { this._nonceKeyName = 'hereditas-nonce' this._nonceLength = 7 } /** * Generates a new nonce and stores it in the session storage * * @returns {string} A nonce */ generate() { // Generate a nonce const nonce = RandomString(this._nonceLength) // Store the nonce in the session storage.sessionStorage.setItem(this._nonceKeyName, nonce) return nonce } /** * Retrieves the last nonce from session storage * * @returns {string} A nonce */ retrieve() { const read = storage.sessionStorage.getItem(this._nonceKeyName) const regExp = new RegExp('^[A-Za-z0-9_\\-]{' + this._nonceLength + '}$') if (!read || !read.match(regExp)) { return null } return read } } /** * Managed the authentication flow, and validates the JWT token. */ export class Credentials { constructor() { this._sessionKeyName = 'hereditas-jwt' this._tokenValidated = false this._nonce = new Nonce() this._profile = null } /** * Returns the authorization URL to point users to, storing the nonce * * @returns {string} Authorization URL */ authorizationUrl() { // Generate a nonce const nonce = this._nonce.generate() // URL-encode the return URL const appUrl = encodeURIComponent(window.location.href) // Generate the URL const authIssuer = process.env.AUTH_ISSUER const authClientId = process.env.AUTH_CLIENT_ID return `${authIssuer}/authorize?client_id=${authClientId}&response_type=id_token&redirect_uri=${appUrl}&scope=openid%20profile&nonce=${nonce}&response_mode=fragment` } /** * Returns the profile object from the JWT token * * @returns {Object} Profile for the authenticated user * @async */ async getProfile() { // If we have a pre-parsed and pre-validated profile in memory, return that if (this._profile) { return this._profile } // Get the token const jwt = this.getToken() if (!jwt) { return {} } // Get the profile out of the token let profile try { profile = await this._validateToken(jwt) if (!profile) { profile = {} } this._profile = profile return profile } catch (e) { this._profile = {} throw e } } /** * Returns the JWT token for the session * * @returns {string|null} JWT Token, or null if no token */ getToken() { const read = storage.sessionStorage.getItem(this._sessionKeyName) if (!read || !read.length) { return null } let data try { data = JSON.parse(read) } catch (error) { // eslint-disable-next-line no-console console.error('Error while parsing JSON from sessionStorage', error) throw Error('Could not get the token from session storage') } if (!data || !data.jwt) { return null } return data.jwt } /** * Stores the JWT token for the session * * @param {string} jwt - JWT Token * @async */ async setToken(jwt) { // Delete the profile in memory this._profile = null // First, validate the token const profile = await this._validateToken(jwt) if (!profile) { throw Error('Token validation failed') } // Store the token storage.sessionStorage.setItem(this._sessionKeyName, JSON.stringify({jwt})) // Set the profile in memory this._profile = profile } /** * Validates a token * * @param {string} jwt - JWT token to validate * @returns {Promise} Extracted payload * @private */ async _validateToken(jwt) { // Ensure issuer ends with / const issuer = process.env.AUTH_ISSUER + (process.env.AUTH_ISSUER.charAt(process.env.AUTH_ISSUER.length - 1) != '/' ? '/' : '') // Validate the token const verifier = new IdTokenVerifier({ issuer, audience: process.env.AUTH_CLIENT_ID }) const payload = await new Promise((resolve, reject) => { verifier.verify(jwt, this._nonce.retrieve(), (error, payload) => { if (error) { // eslint-disable-next-line no-console console.error('Validation error', error) return reject('Invalid token') } // Check if the payload contains the Hereditas namespace if (!payload[process.env.ID_TOKEN_NAMESPACE]) { // eslint-disable-next-line no-console console.error('Token doesn\'t contain the Hereditas namespace') return reject('Token doesn\'t contain the Hereditas namespace') } resolve(payload) }) }) return payload } } // The default export is an instance (singleton) of Credentials const credentials = new Credentials() export default credentials ================================================ FILE: app/lib/CryptoUtils.js ================================================ // Inspired by https://gist.github.com/tscholl2/dc7dc15dc132ea70a98e8542fefffa28#file-aes-js /** * Encodes a utf8 string as a byte array. * @param {String} str * @returns {Uint8Array} */ export function str2buf(str) { return new TextEncoder('utf-8').encode(str) } /** * Decodes a byte array as a utf8 string. * @param {Uint8Array} buffer * @returns {String} */ export function buf2str(buffer) { return new TextDecoder('utf-8').decode(buffer) } /** * Conctatenates two ArrayBuffer's * * @param {ArrayBuffer} buffer1 - First buffer * @param {ArrayBuffer} buffer2 - Second buffer * @returns {ArrayBuffer} The buffer with the data concatenated */ function concatBuffers(buffer1, buffer2) { const result = new Uint8Array(buffer1.byteLength + buffer2.byteLength) result.set(new Uint8Array(buffer1), 0) result.set(new Uint8Array(buffer2), buffer1.byteLength) return result.buffer } /** * Given a passphrase and a salt, this generates a crypto key * using Argon2. * @param {string} passphrase * @param {ArrayBuffer} salt * @returns {Promise} * @async */ export function DeriveKeyArgon2(passphrase, salt) { // Import argon2 dynamically to reduce bundle size, if it's not necessary const saltArr = new Uint8Array(salt) return import('argon2-browser') .then((argon2) => argon2.hash({ pass: passphrase, salt: saltArr, type: argon2.ArgonType.Argon2id, time: process.env.ARGON2_ITERATIONS, mem: process.env.ARGON2_MEMORY, hashLen: 32, parallelism: 1 })) .then((res) => window.crypto.subtle.importKey( 'raw', res.hash, {name: 'AES-KW', length: 256}, false, ['unwrapKey'] ) ) } /** * Given a passphrase and a salt, this generates a crypto key * using `PBKDF2` with SHA-512 and N iterations. * @param {string} passphrase * @param {ArrayBuffer} salt * @returns {Promise} * @async */ export function DeriveKeyPBKDF2(passphrase, salt) { return Promise.resolve() .then(() => window.crypto.subtle.importKey( 'raw', str2buf(passphrase), 'PBKDF2', false, ['deriveKey'] ) ) .then((k) => window.crypto.subtle.deriveKey( {name: 'PBKDF2', salt, iterations: process.env.PBKDF2_ITERATIONS, hash: 'SHA-512'}, k, {name: 'AES-KW', length: 256}, false, ['unwrapKey'] ) ) } /** * Given a key and ciphertext (in the form of a string) as given by `encrypt`, * this decrypts the ciphertext and returns the original plaintext * @param {CryptoKey} key - Encryption key * @param {ArrayBuffer} iv - IV * @param {ArrayBuffer} data - Data to decrypt * @param {ArrayBuffer} tag - AES-GCM tag * @returns {Promise} Decrypted text as string * @async * @throws Throws an error if the decryption fails, likely meaning that the key was wrong. */ export function Decrypt(key, iv, data, tag) { return window.crypto.subtle.decrypt( {name: 'AES-GCM', iv}, key, concatBuffers(data, tag) ) } /** * Unwraps a key wrapped with AES-KW (per RFC 3349) * * @param {CryptoKey} wrappingKey - Key used to wrap/unwrap the key * @param {ArrayBuffer} ciphertext - Wrapped key * @returns {Promise} Unwrapped key * @async * @throws Throws an error if the decryption fails, likely meaning that the key was wrong. */ export function UnwrapKey(wrappingKey, ciphertext) { return window.crypto.subtle.unwrapKey( 'raw', ciphertext, wrappingKey, {name: 'AES-KW'}, {name: 'AES-GCM'}, false, ['decrypt'] ) .then((key) => { return key }) } ================================================ FILE: app/lib/StorageService.js ================================================ // This module is based on https://github.com/Acanguven/StorageService/blob/master/storage.js // License: MIT https://github.com/Acanguven/StorageService/blob/master/LICENSE /** * This class allows access to localStorage and sessionStorage. * If they are not supported in the current browser, automatically falls back to a cookie-based storage */ export class StorageService { /** * Initializes the object. */ constructor() { this.localStorage = this._isSupported('localStorage') ? window.localStorage : new CookieStore() this.sessionStorage = this._isSupported('sessionStorage') ? window.sessionStorage : new CookieStore(true) } /** * Tests if the type of storage is supported in the current browser * * @param {"localStorage"|"sessionStorage"} type - Name of the storage to test * @returns {boolean} True if the browser supports the kind of storage * @private */ _isSupported(type) { const testKey = '__isSupported' const storage = window[type] try { storage.setItem(testKey, '1') storage.removeItem(testKey) return true } catch (error) { return false } } } /** * Interface that implements the protocol of localStorage/sessionStorage while keeping the data in memory. */ export class MemoryStore { /** * Initializes the object. */ constructor() { this._store = {} } getItem(name) { return this._store[name] || null } setItem(name, value) { this._store[name] = value } removeItem(name) { delete this._store[name] } } /** * Interface that implements the protocol of localStorage/sessionStorage while keeping the data in a cookie. */ export class CookieStore { /** * Initializes the object. * * @param {bool} isSessionStorage - True if this object is for session storage (controls cookies' expiry) */ constructor(isSessionStorage) { this._objectStore = {} this._expireDate = isSessionStorage ? ' path=/' : ' expires=Tue, 19 Jan 2038 03:14:07 GMT path=/' this._updateObject() } getItem(name) { return this._objectStore[name] || null } setItem(name, value) { if (!name) { return } document.cookie = escape(name) + '=' + escape(value) + this._expireDate this._updateObject() } removeItem(name) { if (!name) { return } document.cookie = escape(name) + this._expireDate delete this._objectStore[name] } _updateObject() { const couples = document.cookie.split(/\s*\s*/) for (let i = 0; i < couples.length; i++) { const couple = couples[i].split(/\s*=\s*/) if (couple.length > 1) { const key = unescape(couple[0]) this._objectStore[key] = unescape(couple[1]) } } } } const storage = new StorageService() export default storage ================================================ FILE: app/lib/Utils.js ================================================ /** * Returns a random string, useful for example as nonce. * * @param {number} length - Length of the string * @returns {string} Random string */ export function RandomString(length = 7) { const bytes = new Uint8Array(length) const random = window.crypto.getRandomValues(bytes) const result = [] const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-_' random.forEach((c) => { result.push(charset[c % charset.length]) }) return result.join('') } /** * Returns a Promise that resolves after a certain amount of time, in ms */ export function WaitPromise(time) { return new Promise((resolve) => { setTimeout(resolve, time || 0) }) } ================================================ FILE: app/main.css ================================================ /* Tailwind */ @tailwind base; @tailwind components; @tailwind utilities; /* Default styles */ h1 { @apply text-2xl mb-2; } h2 { @apply text-xl mb-2; } h3 { @apply text-lg mb-2; } a { @apply text-blue-600 underline; } /* Rendered content */ section.rendered { @apply my-2 mx-4 px-3 py-1 bg-white border border-blue-500; } .rendered h1, .rendered h2, .rendered h3, .rendered h4, .rendered h5, .rendered h6 { @apply mt-4 mb-2; } .rendered p { @apply my-2; } .rendered pre { @apply mx-2; } .rendered code { @apply w-full whitespace-pre-wrap text-sm; } .rendered ul { @apply list-disc list-inside; } .rendered ol { @apply list-decimal list-inside; } .rendered ul li, .rendered ol li { @apply pl-6; } .rendered blockquote { @apply italic px-6 text-sm; } ================================================ FILE: app/main.html ================================================ Hereditas ================================================ FILE: app/main.js ================================================ // Style import './main.css' // JavaScript modules import App from './layout/App.svelte' import credentials from './lib/Credentials' import qs from 'qs' import {Box} from './lib/Box' // Import stores import {profile, hereditasProfile, box, authError} from './stores' function getHash() { let hash = window.location.hash if (hash && hash.length > 2) { // Remove the leading # and / characters if (hash.charAt(0) == '#') { hash = hash.substr(1) } if (hash.charAt(0) == '/') { hash = hash.substr(1) } const parsed = qs.parse(hash, { depth: 1, parameterLimit: 20, ignoreQueryPrefix: true, }) // Remove the information from the URL (for security, in case it contains an id_token) history.replaceState(undefined, undefined, '#') return parsed } else { return null } } function checkAuthError(hash) { // Check if we have an error from the authentication server if (hash && hash.error) { // Check for the error type if (hash.error == 'unauthorized') { return hash.error_description || 'Unauthorized' } else { return hash.error_description || hash.error } } return null } async function handleSession(hash) { // Check if we have an id_token if (hash && hash.id_token) { // Validate and store the JWT // If there's an error, redirect to auth page try { await credentials.setToken(hash.id_token) } catch (error) { // eslint-disable-next-line no-console console.error('Token error', error) throw Error('Token error') } } // If we have credentials stored, redirect the user to the authentication page if (!credentials.getToken()) { return false } // Get the profile // If there's no session or it has expired, redirect to auth page let profileData try { profileData = await credentials.getProfile() } catch (error) { // eslint-disable-next-line no-console console.error('Token error', error) throw Error('Token error') } return profileData } const app = (async function() { let _profile let _hereditasProfile = null let _box = null // Parse the hash if any const hash = getHash() // Check if we have an error from the authentication server let unrecoverableError = checkAuthError(hash) if (!unrecoverableError) { // Load profile and check session try { _profile = await handleSession(hash) } catch (err) { _profile = null unrecoverableError = err } // Hereditas profile (from the profile) if (_profile) { // Hereditas profile (from the profile) _hereditasProfile = _profile[process.env.ID_TOKEN_NAMESPACE] || {} // Check if we have an app token if (_hereditasProfile.token) { try { // Create a new box and fetch the index _box = new Box() // Fetch the index asynchronously and do not wait for completion _box.fetchIndex() } catch (err) { // eslint-disable-next-line no-console console.error('Error while requesting box\'s data', err) } } } } // Store the profile, hereditasProfile and box into Svelte stores profile.set(_profile) hereditasProfile.set(_hereditasProfile) box.set(_box) authError.set(unrecoverableError) // Crete a Svelte app by loading the main view return new App({ target: document.body }) })() export default app ================================================ FILE: app/postcss.config.js ================================================ const path = require('path') const production = !process.env.ROLLUP_WATCH module.exports = { plugins: [ require('postcss-import')(), require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.js')), require('autoprefixer'), ...(production ? [require('@fullhuman/postcss-purgecss')({ // Specify the paths to all of the template files in your project content: [ path.resolve(__dirname, 'main.html'), path.resolve(__dirname, '**/*.svelte'), path.resolve(__dirname, '**/*.html'), ], // Whitelist styles that might be in the content generated from markdown whitelist: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'ul', 'ol', 'li', 'strong', 'b', 'em', 'i', 'a', 'img', 'pre', 'code', 'hr', 'blockquote'], // Include any special characters you're using in this regular expression defaultExtractor: content => content.match(/[\w-/:]+(?
  1. home >
  2. {#each content.path as path, i}
  3. {path} >
  4. {/each}
  5. {content.name}
{#if content.display == 'text'}
{content.text}
{:else if content.display == 'html'}
{@html content.text}
{:else if content.display == 'image'} {content.name} {:else} Download: {content.name} {/if} {:catch error} Error: {error} {/await} ================================================ FILE: app/views/ListView.svelte ================================================
{#each list.folders as folder (folder)} {/each} {#each list.files as file (file.dist)} {/each}
Name
{folder}
{file.name}
================================================ FILE: app/views/UnlockView.svelte ================================================ {#if $profile} {:else} {/if}

About this page

{@html welcome}
================================================ FILE: app/webpack.config.js ================================================ const MiniCssExtractPlugin = require('mini-css-extract-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const SriPlugin = require('webpack-subresource-integrity') const CopyPlugin = require('copy-webpack-plugin') const {DefinePlugin} = require('webpack') const path = require('path') const fs = require('fs') const marked = require('marked') const mode = process.env.NODE_ENV || 'production' const prod = mode === 'production' const htmlMinifyOptions = { collapseWhitespace: true, conservativeCollapse: true, removeComments: true, collapseBooleanAttributes: true, decodeEntities: true, html5: true, keepClosingSlash: false, processConditionalComments: true, removeEmptyAttributes: true } // Welcome content let welcomeContent = '' if (fs.existsSync('welcome.md')) { let welcomeMarkdown = fs.readFileSync('welcome.md', 'utf8') // Remove the front matter, if any if (welcomeMarkdown.startsWith('---')) { welcomeMarkdown = welcomeMarkdown.replace(/^---$.*^---$/ms, '') } welcomeContent = marked(welcomeMarkdown) } /** * Returns a configuration object for webpack * * @param {Object} appParams - Params for the application * @returns {Object} Configuration object for webpack */ function webpackConfig(appParams) { return { entry: { hereditas: [path.resolve(__dirname, 'main.js')], }, resolve: { mainFields: ['svelte', 'browser', 'module', 'main'], extensions: ['.mjs', '.js', '.svelte'], modules: [path.resolve(__dirname, '../node_modules')] }, resolveLoader: { modules: [path.resolve(__dirname, '../node_modules')] }, output: { path: path.resolve(process.cwd(), appParams.distDir), filename: '[name].[hash].js', chunkFilename: '[name].[id].[hash].js', crossOriginLoading: 'anonymous' }, module: { // Do not parse wasm files noParse: /\.wasm$/, rules: [ { test: /\.(svelte)$/, exclude: [], use: { loader: 'svelte-loader', options: { emitCss: true, } } }, { test: /\.css$/, use: [ 'style-loader', {loader: 'css-loader', options: {importLoaders: 1}}, 'postcss-loader', ] }, { test: /\.wasm$/, loaders: ['base64-loader'], type: 'javascript/auto' } ] }, // Fixes for argon2-browser node: { __dirname: false, fs: 'empty', Buffer: false, process: false }, mode, plugins: [ // Constants new DefinePlugin({ 'process.env.AUTH_ISSUER': JSON.stringify(appParams.authIssuer), 'process.env.AUTH_CLIENT_ID': JSON.stringify(appParams.authClientId), 'process.env.ID_TOKEN_NAMESPACE': JSON.stringify(appParams.idTokenNamespace), 'process.env.KEY_SALT': JSON.stringify(appParams.keySalt.toString('base64')), 'process.env.INDEX_TAG': JSON.stringify(appParams.indexTag.toString('base64')), 'process.env.KEY_DERIVATION_FUNCTION': JSON.stringify(appParams.kdf), 'process.env.PBKDF2_ITERATIONS': JSON.stringify(appParams.pbkdf2Iterations), 'process.env.ARGON2_ITERATIONS': JSON.stringify(appParams.argon2Iterations), 'process.env.ARGON2_MEMORY': JSON.stringify(appParams.argon2Memory), 'process.env.WELCOME_MD': JSON.stringify(welcomeContent) }), // Extract CSS new MiniCssExtractPlugin({ filename: '[name].[hash].css' }), // Generate the index.html file new HtmlWebpackPlugin({ filename: 'index.html', template: path.resolve(__dirname, 'main.html'), chunks: ['hereditas'], minify: prod ? htmlMinifyOptions : false }), // Enable subresource integrity check new SriPlugin({ hashFuncNames: ['sha384'], enabled: prod, }), // Copy files new CopyPlugin({ patterns: [ {from: path.resolve(__dirname, 'robots.txt'), to: ''}, ] }), ], devtool: prod ? false : 'source-map', performance: { // 400 KB (up from default 250 KB) maxEntrypointSize: 400000 } } } module.exports = webpackConfig ================================================ FILE: auth0/01-whitelist.js ================================================ function (user, context, callback) { // Apply this rule only for Hereditas, and bypass it for other apps context.clientMetadata = context.clientMetadata || {}; if (!context.clientMetadata.hereditas) { return callback(null, user, context); } // List of authorized users const whitelist = /*%ALL_USERS%*/; // Access should only be granted to verified users. if (!user.email || !user.email_verified) { return callback(new UnauthorizedError('Access denied.')); } // Check if the user's email address is whitelisted const userHasAccess = whitelist.some((email) => email === user.email); if (!userHasAccess) { return callback(new UnauthorizedError('Access denied.')); } // Continue callback(null, user, context); } ================================================ FILE: auth0/02-notify.js ================================================ function (user, context, callback) { // Apply this rule only for Hereditas, and bypass it for other apps context.clientMetadata = context.clientMetadata || {}; if (!context.clientMetadata.hereditas) { return callback(null, user, context); } // Skip if there's no webhook if (!configuration || !configuration.WEBHOOK_URL || configuration.WEBHOOK_URL === '0') { return callback(null, user, context); } // List of owners const owners = /*%OWNERS%*/; // Trigger the webhook const role = (owners.some((email) => email === user.email)) ? 'owner' : 'user'; const body = { value1: `New Hereditas login on ${(new Date()).toUTCString()}. User: ${user.email} (role: ${role})`, value2: user.email, value3: role }; const fetch = require('node-fetch@2.6.0'); fetch(configuration.WEBHOOK_URL, { method: 'POST', body: JSON.stringify(body), headers: {'Content-Type': 'application/json'} }) // Ensure the response has a valid status code .then((response) => { if (response.ok) { return callback(null, user, context); } else { return Promise.reject('Invalid response status code'); } }) // Catch errors and fail (fail the login even if the notification fails to send) .catch((err) => { console.error(err); callback(new Error('Error sending the notification')); }); } ================================================ FILE: auth0/03-wait-logic.js ================================================ function (user, context, callback) { // Apply this rule only for Hereditas, and bypass it for other apps context.clientMetadata = context.clientMetadata || {}; if (!context.clientMetadata.hereditas) { return callback(null, user, context); } // List of owners const owners = /*%OWNERS%*/; // Get the Auth0 management client const ManagementClient = require('auth0@2.27.0').ManagementClient; const management = new ManagementClient({ domain: auth0.domain, clientId: configuration.AUTH0_CLIENT_ID, clientSecret: configuration.AUTH0_CLIENT_SECRET }); // Get metadata const requestTime = context.clientMetadata.requestTime ? parseInt(context.clientMetadata.requestTime, 10) : 0; const waitTime = parseInt(context.clientMetadata.waitTime, 10); // Check if the user is an owner const isOwner = owners.some((email) => email === user.email); if (isOwner) { // Enrich the JWT with the app token if (context.idToken) { context.idToken['https://hereditas.app'] = { role: 'owner', token: configuration.APP_TOKEN, requestTime: 0, waitTime: waitTime }; } // Reset the timer if it's running if (requestTime > 0) { const params = {client_id: context.clientID}; const data = {client_metadata: {requestTime: '0'}}; management.clients.update(params, data, (err, client) => { if (err) { console.log(err); callback(new Error('Error while updating client_metadata')); } else { // Continue callback(null, user, context); } }); } else { // Continue callback(null, user, context); } } else { const now = parseInt(Date.now() / 1000, 10); // For non-owners: first, check if the timer has been started already, and we've reached the wait time if (requestTime > 0) { // Enrich the JWT with the app token if (context.idToken) { // If the wait time has passed, add the token const token = ((requestTime + waitTime) < now) ? configuration.APP_TOKEN : null; // Enrich the JWT context.idToken['https://hereditas.app'] = { role: 'user', token: token, requestTime: requestTime, waitTime: waitTime }; } // Continue callback(null, user, context); } else { // Start the timer const params = {client_id: context.clientID}; const data = {client_metadata: {requestTime: now.toString()}}; management.clients.update(params, data, (err, client) => { if (err) { console.log(err); callback(new Error('Error while updating client_metadata')); } else { // Enrich the JWT with the app token if (context.idToken) { context.idToken['https://hereditas.app'] = { role: 'user', requestTime: now, waitTime: waitTime }; } // Continue callback(null, user, context); } }); } } } ================================================ FILE: bin/run ================================================ #!/usr/bin/env node require('@oclif/command') .run() .then(require('@oclif/command/flush')) .catch(require('@oclif/errors/handle')) ================================================ FILE: bin/run.cmd ================================================ @echo off node "%~dp0\run" %* ================================================ FILE: cli/commands/auth0/sync.js ================================================ 'use strict' const {Command} = require('@oclif/command') const Config = require('../../lib/Config') const Auth0Management = require('../../lib/Auth0Management') class Auth0SetupCommand extends Command { async run() { // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Initialize the management client const auth0Management = new Auth0Management(config) // First step: sync the app on Auth0 const clientId = await auth0Management.syncClient(config.get('auth0.hereditasClientId')) config.set('auth0.hereditasClientId', clientId) // Second step: create the rules const ruleIds = await auth0Management.syncRules(config.get('auth0.rules')) config.set('auth0.rules', ruleIds) // Third step: create rule settings await auth0Management.updateRulesConfigs() // Save config changes await config.save() this.log('Auth0 configuration updated successfully') } } // Command description Auth0SetupCommand.description = `sync the application and rules in Auth0 Synchronizes the status of the resources configured in Auth0: the client (application), the rules and the rule settings. ` module.exports = Auth0SetupCommand ================================================ FILE: cli/commands/build.js ================================================ 'use strict' const {Command} = require('@oclif/command') const Config = require('../lib/Config') const Builder = require('../lib/Builder') const {cli} = require('cli-ux') class BuildCommand extends Command { async run() { // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Make sure that we have an Auth0 client id const clientId = config.get('auth0.hereditasClientId') if (!clientId) { this.error('The Hereditas application hasn\'t been configured on Auth0 yet. Please run `hereditas auth0:sync` first') return this.exit(1) } // Check if we have a passphrase passed as environmental variable (useful for development only) let passphrase if (process.env.HEREDITAS_PASSPHRASE) { passphrase = process.env.HEREDITAS_PASSPHRASE this.warn('Passphrase set through the HEREDITAS_PASSPHRASE environmental variable; this should be used for development only') } else { // Ask for the user passphrase passphrase = await cli.prompt('User passphrase', {type: 'mask'}) } if (!passphrase || passphrase.length < 8) { this.error('Passphrase needs to be at least 8 characters long') return this.exit(1) } // Timer const startTime = Date.now() // Build the project const builder = new Builder(passphrase, config) await builder.build() // Done! const duration = (Date.now() - startTime) / 1000 if (!builder.hasErrors) { this.log(`Finished building project in ${config.get('distDir')} (took ${duration} seconds)`) } else { this.error(`Build failed (took ${duration} seconds)`) } } } // Command description BuildCommand.description = `build an Hereditas project Build an Hereditas project in the current working directory. ` module.exports = BuildCommand ================================================ FILE: cli/commands/init.js ================================================ 'use strict' const {Command, flags} = require('@oclif/command') const fs = require('fs') const util = require('util') const process = require('process') const path = require('path') const Config = require('../lib/Config') const {GenerateToken} = require('../lib/Crypto') class InitCommand extends Command { async run() { const {flags} = this.parse(InitCommand) // Check if the folder is empty const files = await util.promisify(fs.readdir)('.') if (files.length) { this.error(`Directory ${process.cwd()} isn't empty; aborting`) return this.exit(1) } // Get the relative paths to the folders const contentDir = path.relative('', flags.content) const distDir = path.relative('', flags.dist) // Create the directories const mkdirPromise = util.promisify(fs.mkdir) await mkdirPromise(contentDir) await mkdirPromise(distDir) // Generate an appToken const appToken = await GenerateToken(21) // Create configuration const config = new Config('hereditas.json') config.create({ distDir: distDir, contentDir: contentDir, auth0: { domain: flags.auth0Domain, managementClientId: flags.auth0ClientId, managementClientSecret: flags.auth0ClientSecret }, urls: flags.url, waitTime: 86400, appToken }) await config.save() // Create the welcome.md file const welcomeContent = `--- # This welcome file is displayed in the box's authentication page. # It can be used to provide information to visitors about what this Hereditas box is, and how it can be used. # Note that this file is NOT ENCRYPTED, and it's accessible to the entire world; do not write anything confidential in here. --- ## What is this? Someone (likely, a loved one) told you to come here in case they suddenly disappeared. This box contains important information about the digital life of the person that shared it with you, for example passwords, digital documents, photos, cryptocurrency wallets, etc. ## How do I use this? Sign in above using your existing account. You will then need to type the passphrase that you've been given. Unless you're the owner of this box, you won't immediately have access to its content, but instead you'll have to wait a certain amount of time. During that time, if the owner signs in here too, they will reset the timer and you will not get access to this box. ## About Hereditas [Hereditas](https://hereditas.app) is an open source project to generate "fully-trustless" digital legacy boxes. ` await util.promisify(fs.writeFile)(path.relative('', 'welcome.md'), welcomeContent) this.log('Project initialized') } } // Command description InitCommand.description = `initialize a new Hereditas project in the current working directory. Initialize a new Hereditas project in the current working directory, creating the folders for the content and the generated data, as well as the "hereditas.json" configuration file. The current working directory needs to be empty, or the command will raise an error. ` // Usage example InitCommand.usage = `init \\ --auth0Domain "yourdomain.auth0.com" \\ --auth0ClientId "..." \\ --auth0ClientSecret "..." \\ --url "https://my.testhereditas.app" ` // Command-line options InitCommand.flags = { content: flags.string({ char: 'i', description: 'path of the directory with the content', default: 'content' }), dist: flags.string({ char: 'o', description: 'path of the dist directory (where output is saved)', default: 'dist' }), auth0Domain: flags.string({ char: 'd', description: 'Auth0 domain/tenant (e.g. "myhereditas.auth0.com")', required: true }), auth0ClientId: flags.string({ char: 'c', description: 'Auth0 client ID for the management app', required: true }), auth0ClientSecret: flags.string({ char: 's', description: 'Auth0 client secret for the management app', required: true }), url: flags.string({ char: 'u', description: 'URL where the app is deployed to, used for OAuth callbacks (multiple values supported)', required: true, multiple: true }) } module.exports = InitCommand ================================================ FILE: cli/commands/pack.js ================================================ 'use strict' const {Command} = require('@oclif/command') const Config = require('../lib/Config') const util = require('util') const path = require('path') const fs = require('fs') const {CleanDirectory} = require('../lib/Utils') const execPromise = util.promisify(require('child_process').exec) class PackCommand extends Command { async run() { // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Make sure that http://localhost:8080 is allowed as url let urls = config.get('urls') if (!urls || urls.indexOf('http://localhost:8080') == -1) { this.error('Before you can pack a box, the URL `http://localhost:8080` must be allowed. Please run `hereditas url:add -u http://localhost:8080` (and then `hereditas auth0:sync`).') return this.exit(1) } // Check that we have go installed try { const {stdout} = await execPromise('go version') const match = stdout.match(/^go version go1\.([0-9]+)/) if (!match || !match[0] || !match[1]) { throw Error('Invalid go interpreter') } const goVersion = parseInt(match[1], 10) if (goVersion < 13) { throw Error('Go 1.13 or higher is required') } } catch (err) { this.error('Go 1.13 or higher must be installed for this command to work.') return this.exit(1) } // Ensure that the GOPATH and HOME are defined if (!process.env.GOPATH || !process.env.HOME) { this.error('Environmental variables GOPATH and HOME must be defined.') return this.exit(1) } // Check that we have packr2 installed try { const {stdout} = await execPromise('packr2 version') const match = stdout.match(/^v2/) if (!match || !match[0]) { throw Error('Invalid packr2 version') } } catch (err) { this.error('packr2 must be installed for this command to work.\nSee https://github.com/gobuffalo/packr/tree/master/v2') return this.exit(1) } // Check that the Hereditas box is built const distDir = config.get('distDir') if (!fs.existsSync(path.join(distDir, '_index'))) { this.error('This Hereditas box hasn\'t been built yet; please run `hereditas build` first.') return this.exit(1) } // Create a directory for the Go app // Or clean it if it exists const packPath = path.relative('', 'pack.tmp') if (fs.existsSync(packPath)) { await CleanDirectory(packPath) } else { fs.mkdirSync(packPath) } // Copy the Go app's files ['main.go', 'go.mod', 'go.sum'].forEach((file) => { fs.copyFileSync( path.resolve(__dirname, '../../pack/' + file), path.join(packPath, file) ) }) // Create a symlink to distDir inside the packPath fs.symlinkSync(path.join('..', distDir), path.join(packPath, 'dist'), 'dir') // Run packr2 await execPromise('packr2', { cwd: packPath }) // Build the Go app for all archs const archs = { 'linux-amd64': { GOOS: 'linux', GOARCH: 'amd64' }, 'linux-386': { GOOS: 'linux', GOARCH: '386' }, 'linux-arm64': { GOOS: 'linux', GOARCH: 'arm64' }, 'linux-armv7': { GOOS: 'linux', GOARCH: 'arm', GOARM: '7' }, 'macos': { GOOS: 'darwin', GOARCH: 'amd64' }, 'win64.exe': { GOOS: 'windows', GOARCH: 'amd64' }, 'win32.exe': { GOOS: 'windows', GOARCH: '386' } } for (const extension in archs) { if (!archs.hasOwnProperty(extension)) { continue } const file = 'hereditas-box-' + extension this.log('Building ' + file) // Environmental variables const env = Object.assign({ GOPATH: process.env.GOPATH, HOME: process.env.HOME, CGO_ENABLED: '0', GO111MODULE: 'on' }, archs[extension]) await execPromise('go build -o ' + path.join('..', '_bin', file), { cwd: packPath, env }) } // Delete the temporary folder await CleanDirectory(packPath) fs.rmdirSync(packPath) this.log('Done! Binaries are in the _bin folder') } } // Command description PackCommand.description = `pack a box into a self-contained binary After building a box with Hereditas, the \`hereditas pack\` command allows you to generate a self-contained binary (for Windows, macOS and Linux) that contains your Hereditas box and all of its contents. Depending on your use case, this single binary might be easier to distribute. Note that this command has some pre-requisites: - You need to have the Go compiler installed (version 1.13 or higher) - You need to have packr2 installed in your path (see https://github.com/gobuffalo/packr/tree/master/v2) - Your Hereditas box must be already built (run \`hereditas build\` beforehand) - The URL \`http://localhost:8080\` must be allowed for this box (run \`hereditas url:add -u http://localhost:8080\`) ` module.exports = PackCommand ================================================ FILE: cli/commands/regenerate-token.js ================================================ 'use strict' const {Command} = require('@oclif/command') const Config = require('../lib/Config') const {GenerateToken} = require('../lib/Crypto') class RegenerateTokenCommand extends Command { async run() { // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Generate a new appToken and save it const appToken = await GenerateToken(21) config.set('appToken', appToken) await config.save() this.log('New application token saved in the configuration file') // Notify users that they need to run the auth0:sync command this.log('Info: The new application token will be used for boxes you build from now on, using `hereditas build`; it will not impact existing boxes. Additionally, remember to run `hereditas auth0:sync` to update the application token on Auth0 after deploying the new box.') } } // Command description RegenerateTokenCommand.description = `regenerate the application token Update the "application token", which is part of the encryption key, in the hereditas.json config file, by generating a new random one. After running this command, you will need to build a new box with \`hereditas build\` and then synchronize the changes on Auth0 with \`hereditas auth0:sync\`. ` module.exports = RegenerateTokenCommand ================================================ FILE: cli/commands/url/add.js ================================================ 'use strict' const {Command, flags} = require('@oclif/command') const Config = require('../../lib/Config') class UrlAddCommand extends Command { async run() { const {flags} = this.parse(UrlAddCommand) // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Load the current list let urls = config.get('urls') if (!urls) { urls = [] } // Add all new URLs for (let i = 0; i < flags.url.length; i++) { if (urls.indexOf(flags.url[i]) != -1) { this.log(`URL ${flags.url[i]} is already present`) } else { // Add url urls.push(flags.url[i]) this.log(`Added URL ${flags.url[i]}`) } } // Save changes config.set('urls', urls) await config.save() this.log('URL list updated') // Notify users that they need to run the auth0:sync command this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`') } } // Command description UrlAddCommand.description = `add URLs where the box is deployed to, used for OAuth callbacks Add one or more URLs to the list of addresses where the Hereditas box is deployed to. This information is stored on Auth0 to whitelist URLs where users are redirected after a successful authentication. Note that the protocol (\`http://\` or \`https://\`) needs to match too. After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box). ` // Usage example UrlAddCommand.usage = `url:add \\ --url "https://my.testhereditas.app" ` // Command-line options UrlAddCommand.flags = { url: flags.string({ char: 'u', description: 'URL where the box is deployed to (multiple values supported)', required: true, multiple: true }) } module.exports = UrlAddCommand ================================================ FILE: cli/commands/url/list.js ================================================ 'use strict' const {Command} = require('@oclif/command') const Config = require('../../lib/Config') class UrlListCommand extends Command { async run() { // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Load all urls let urls = config.get('urls') if (!urls) { urls = [] } this.log(urls.join('\n')) } } // Command description UrlListCommand.description = `list URLs where the box is deployed to Shows the list of URLs where the Hereditas box is deployed to. This list is used by Auth0 to whitelist redirect URLs after users authenticate. ` module.exports = UrlListCommand ================================================ FILE: cli/commands/url/rm.js ================================================ 'use strict' const {Command, flags} = require('@oclif/command') const Config = require('../../lib/Config') class UrlRmCommand extends Command { async run() { const {flags} = this.parse(UrlRmCommand) // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Load the current list let urls = config.get('urls') if (!urls) { urls = [] } else { // Remove urls that match urls = urls.filter((el) => flags.url.indexOf(el) == -1) } if (!urls.length) { this.error('Cannot remove all URLs from the list') return this.exit(1) } // Save changes config.set('urls', urls) await config.save() this.log('URL list updated') // Notify users that they need to run the auth0:sync command this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`') } } // Command description UrlRmCommand.description = `removes URL(s) from the configuration These URLs are used by Auth0 to whitelist the pages users are redirected to after authenticating. After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box). ` // Usage example UrlRmCommand.usage = `url:rm \\ --url "https://my.testhereditas.app" ` // Command-line options UrlRmCommand.flags = { url: flags.string({ char: 'u', description: 'URL to remove (multiple values supported)', required: true, multiple: true }) } module.exports = UrlRmCommand ================================================ FILE: cli/commands/user/add.js ================================================ 'use strict' const {Command, flags} = require('@oclif/command') const Config = require('../../lib/Config') class UserAddCommand extends Command { async run() { const {flags} = this.parse(UserAddCommand) // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Check if the user is already in the configuration let users = config.get('users') if (!users) { users = [] } const added = users.some((el) => el.email == flags.email) if (added) { this.log(`User ${flags.email} is already authorized`) return } // Add user users.push({ email: flags.email, role: flags.role }) config.set('users', users) await config.save() this.log(`Added user ${flags.email} (role: ${flags.role})`) // Notify users that they need to run the auth0:sync command this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`') } } // Command description UserAddCommand.description = `add an authorized user to the box Whitelist email addresses to allow users to authenticate and access your Hereditas box. If you configure Auth0 to enable social logins (e.g. Google, Facebook and/or Microsoft accounts), users won't need to set up a new account or password, and they can authenticate with their existing social account as long as the email address matches what you've whitelisted. When you whitelist an email address, you can choose between the "user" role (the default) and the "owner" one. Someone with the "owner" role can access the data in this Hereditas box at any time (provided they have the "user passphrase" too), and when they authenticate, they reset any timer that might have been started by another person with the "user" role. After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box). ` // Usage example UserAddCommand.usage = `user:add \\ --email "someone@example.com" ` // Command-line options UserAddCommand.flags = { email: flags.string({ char: 'e', description: 'email address of the user to whitelist', required: true }), role: flags.string({ char: 'r', options: ['user', 'owner'], description: 'role: user or owner', default: 'user' }) } module.exports = UserAddCommand ================================================ FILE: cli/commands/user/list.js ================================================ 'use strict' const {Command, flags} = require('@oclif/command') const Config = require('../../lib/Config') class UserListCommand extends Command { async run() { const {flags} = this.parse(UserListCommand) // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Load all users let users = config.get('users') if (!users) { users = [] } const list = {owners: [], users: []} for (let i = 0; i < users.length; i++) { if (users[i].role == 'owner') { list.owners.push(users[i].email) } else { list.users.push(users[i].email) } } list.owners.sort() list.users.sort() // Show list if (!flags.role) { this.log(`\x1b[1mOwners:\x1b[0m\n ${list.owners.join('\n ')}`) this.log(`\x1b[1mUsers:\x1b[0m\n ${list.users.join('\n ')}`) } else { this.log(list[flags.role + 's'].join('\n')) } } } // Command description UserListCommand.description = `list users that are authorized to authenticate with this box Prints the list of authorized users (email adddresses) and their role. ` // Command-line options UserListCommand.flags = { role: flags.string({ char: 'r', options: ['', 'user', 'owner'], description: 'filter by role: user or owner (or none)', default: '' }) } module.exports = UserListCommand ================================================ FILE: cli/commands/user/rm.js ================================================ 'use strict' const {Command, flags} = require('@oclif/command') const Config = require('../../lib/Config') class UserRmCommand extends Command { async run() { const {flags} = this.parse(UserRmCommand) // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Load users and remove the requested one let users = config.get('users') if (!users) { users = [] } else { users = users.filter((el) => el.email != flags.email) } // Save config.set('users', users) await config.save() this.log(`Removed user ${flags.email}`) // Notify users that they need to run the auth0:sync command this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`') } } // Command description UserRmCommand.description = `remove an authorized user from this box Removes an email address from the list of those authorized to authenticate with Auth0 for this Hereditas box. After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box). ` // Usage example UserRmCommand.usage = `user:rm \\ --email "someone@example.com" ` // Command-line options UserRmCommand.flags = { email: flags.string({ char: 'e', description: 'email address of the user to remove from the whitelist', required: true }) } module.exports = UserRmCommand ================================================ FILE: cli/commands/wait-time/get.js ================================================ 'use strict' const {Command} = require('@oclif/command') const Config = require('../../lib/Config') class WaitTimeGetCommand extends Command { async run() { // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } this.log(config.get('waitTime') + 's') } } // Command description WaitTimeGetCommand.description = `get the current value for the wait time This command returns the current value for the wait time, in seconds. The wait time is the amount of time for normal users (that don't have the "owner" role) before they can unlock the Hereditas box. Auth0 will not provide users with the "application token" unless the wait time has passed since their first login, preventing them from having the information required to unlock the Hereditas box. If users with the "owner" role authenticate, the timer is automatically stopped. ` module.exports = WaitTimeGetCommand ================================================ FILE: cli/commands/wait-time/set.js ================================================ 'use strict' const {Command, flags} = require('@oclif/command') const Config = require('../../lib/Config') class WaitTimeSetCommand extends Command { async run() { const {flags} = this.parse(WaitTimeSetCommand) // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Get the new value const time = parseInt(flags.time || '0', 10) if (time < 1) { this.error('Wait time must be a number greater than zero') return this.exit(1) } // Save changes config.set('waitTime', time) await config.save() this.log('Wait time updated') // Notify users that they need to run the auth0:sync command this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`') } } // Command description WaitTimeSetCommand.description = `configure the wait time This command sets the wait time (in seconds) for this Hereditas box. The wait time is the amount of time for normal users (that don't have the "owner" role) before they can unlock the Hereditas box. Auth0 will not provide users with the "application token" unless the wait time has passed since their first login, preventing them from having the information required to unlock the Hereditas box. If users with the "owner" role authenticate, the timer is automatically stopped. After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box). ` // Usage example WaitTimeSetCommand.usage = `wait-time:set \\ --time 86400 ` // Command-line options WaitTimeSetCommand.flags = { time: flags.string({ char: 't', description: 'wait time, in seconds', required: true, }) } module.exports = WaitTimeSetCommand ================================================ FILE: cli/commands/webhook/get.js ================================================ 'use strict' const {Command} = require('@oclif/command') const Config = require('../../lib/Config') class WebhookGetCommand extends Command { async run() { // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } this.log(config.get('webhookUrl') || 'No webhook configured') } } // Command description WebhookGetCommand.description = `get the current value for the webhook URL Hereditas configures Auth0 to send a notification when someone successfully authenticates into this Hereditas box. The notification can be used as a warning that the timer has started. Notifications are sent by invoking a webhook, which can then trigger any action you desire. See the Hereditas documentation for examples and ideas on how to use this feature. If no webhook is set, Hereditas will not send you notifications on new logins. ` module.exports = WebhookGetCommand ================================================ FILE: cli/commands/webhook/set.js ================================================ 'use strict' const {Command, flags} = require('@oclif/command') const Config = require('../../lib/Config') class WebhookSetCommand extends Command { async run() { const {flags} = this.parse(WebhookSetCommand) // Read the config file const config = new Config('hereditas.json') try { await config.load() } catch (e) { this.error(`The current directory ${process.cwd()} doesn't contain a valid Hereditas project`) return this.exit(1) } // Save changes config.set('webhookUrl', (flags.url === 'none') ? undefined : flags.url) await config.save() this.log('Webhook URL updated') // Notify users that they need to run the auth0:sync command this.log('Info: The configuration has been updated locally; for changes to be effective, remember to run `hereditas auth0:sync`') } } // Command description WebhookSetCommand.description = `set the webhook URL used to notify of new logins Hereditas configures Auth0 to send a notification when someone successfully authenticates into this Hereditas box. The notification can be used as a warning that the timer has started. Notifications are sent by invoking a webhook, which can then trigger any action you desire. See the Hereditas documentation for examples and ideas on how to use this feature. You can disable notifications by setting \`--url none\` when invoking this command. After running this command, you will need to synchronize the changes on Auth0 with \`hereditas auth0:sync\` (it's not necessary to re-build or re-deploy the box). ` // Usage example WebhookSetCommand.usage = `webhook:set \\ --url "https://example.com/webhook/token/abc123XYZ" ` // Command-line options WebhookSetCommand.flags = { url: flags.string({ char: 'u', description: 'webhook URL; set to "none" to remove', required: true, }) } module.exports = WebhookSetCommand ================================================ FILE: cli/index.js ================================================ module.exports = require('@oclif/command') ================================================ FILE: cli/lib/Auth0Management.js ================================================ 'use strict' const fs = require('fs') const util = require('util') const path = require('path') const ManagementClient = require('auth0').ManagementClient /** * Configures Auth0 to work with Hereditas */ class Auth0Management { /** * Initializes the object * @param {Config} config - Config object */ constructor(config) { // Ensure that the configuration has Auth0 credentials const auth0Config = config.get('auth0') if (!auth0Config || !auth0Config.domain || !auth0Config.managementClientId || !auth0Config.managementClientSecret) { throw Error('Auth0 Management Client credentials are not present') } this._config = config this._management = new ManagementClient({ domain: auth0Config.domain, clientId: auth0Config.managementClientId, clientSecret: auth0Config.managementClientSecret }) } /** * Ensures that we have a client (application) on Auth0 whose configuration matches the desired one. If a client ID is passed, the method checks if the client exists and updates it; otherwise, it will create a new client. * * @param {string} [clientId] - Auth0 client (application) ID * @returns {string} Client ID of the application (either new or updated) */ async syncClient(clientId) { // Check if we already have a client and it exists if (clientId) { // Check if it exists; if it does, update the data let data try { data = await this.getClient(clientId) } catch (err) { // If the exception is because the client doesn't exist, all good; otherwise, re-throw it if (err.toString().match(/Not Found/i)) { data = null } else if (err.name && err.name == 'access_denied') { throw Error('Invalid Auth0 credentials') } else { throw err } } // If we have an existing client, update it if (data) { try { await this.updateClient(clientId) } catch (err) { if (err.name && err.name == 'access_denied') { throw Error('Invalid Auth0 credentials') } else { throw err } } } else { clientId = undefined } } // If client doesn't exist, create it if (!clientId) { try { clientId = await this.createClient() } catch (err) { if (err.name && err.name == 'access_denied') { throw Error('Invalid Auth0 credentials') } else { throw err } } } return clientId } /** * Updates a client (application) on Auth0 so the configuration matches the desired one. * * @param {string} clientId - Auth0 client (application) ID * @returns {string} Client ID of the updated application */ async updateClient(clientId) { const params = { client_id: clientId } const result = await this._management.clients.update(params, this._clientConfiguration()) if (result) { return result.client_id } } /** * Create the client (application) on Auth0. * * @returns {string} Client ID of the new application */ async createClient() { // Create the client const result = await this._management.clients.create(this._clientConfiguration()) if (result) { return result.client_id } } /** * Retrieve a client (application) from Auth0. * * @param {string} clientId - Client ID */ async getClient(clientId) { const data = await this._management.clients.get({client_id: clientId}) if (!data || data.client_id != clientId) { return null } return data } /** * Ensures that the rules Hereditas needs are present in Auth0, and re-creates them so they're on the last version of the configuration and code. * * @param {Array} [ruleIds] - List of rules already created by Hereditas (if any) * @returns {Array} New list of rules managed by Hereditas */ async syncRules(ruleIds) { // First, check if the rules still exist if (ruleIds && ruleIds.length) { const rules = await this.listRules() if (rules && Array.isArray(rules) && rules.length) { // Delete all rules from the array that still exist const promises = [] for (let i = 0; i < rules.length; i++) { const el = rules[i] if (!el || !el.id) { continue } if (ruleIds.indexOf(el.id) != -1) { promises.push(this._management.rules.delete({id: el.id})) } } // Await all requests in parallel await Promise.all(promises) } } // Lastly, re-create all rules and return the new IDs return this.createRules() } /** * List all rules * * @returns {Array} List of rules * @async */ listRules() { return this._management.rules.getAll() } /** * Create all rules required by Hereditas. * * @returns {Array} Array with the ID of the rules, in order * @async */ async createRules() { // Read all scripts const readFilePromise = util.promisify(fs.readFile) const scripts = await Promise.all([ readFilePromise(path.resolve(__dirname, '../../auth0/01-whitelist.js'), 'utf8'), readFilePromise(path.resolve(__dirname, '../../auth0/02-notify.js'), 'utf8'), readFilePromise(path.resolve(__dirname, '../../auth0/03-wait-logic.js'), 'utf8') ]) const names = [ 'Hereditas 01 - Whitelist email addresses', 'Hereditas 02 - Notify', 'Hereditas 03 - Wait logic' ] // Replacer function in scripts const users = this._config.get('users') || [] const replacer = (script) => { const vars = { '/*%ALL_USERS%*/': JSON.stringify(users.map((el) => el.email)), '/*%OWNERS%*/': JSON.stringify(users.filter((el) => el.role == 'owner').map((el) => el.email)) } return script.replace(/\/\*%([A-Za-z0-9_]+)%\*\//, (token) => { return vars[token] }) } // Create all rules, in order const promises = [] for (let i = 0; i < 3; i++) { promises.push(this._management.rules.create({ enabled: true, stage: 'login_success', order: i + 1, name: names[i], script: replacer(scripts[i]) })) } const results = await Promise.all(promises) // Return the IDs of the rules return results.map((el) => el.id) } /** * List all rules configurations (only the keys, not values) * * @returns {Array} Array with all the rules configurations * @async */ listRulesConfigs() { return this._management.rulesConfigs.getAll() } /** * Updates all rules configurations. This creates new configurations, and overwrites existing ones. * * @async */ async updateRulesConfigs() { const rulesConfigs = { APP_TOKEN: this._config.get('appToken'), AUTH0_CLIENT_ID: this._config.get('auth0.managementClientId'), AUTH0_CLIENT_SECRET: this._config.get('auth0.managementClientSecret'), WEBHOOK_URL: this._config.get('webhookUrl') || '0' } // Create all rules configurations const promises = [] for (const key in rulesConfigs) { const value = rulesConfigs[key] promises.push(this._management.rulesConfigs.set({key}, {value})) } await Promise.all(promises) } /** * Returns the configuration object for a client (application) on Auth0. * * @returns {Object} Configuration object for the client (application) on Auth0 */ _clientConfiguration() { return { name: 'Hereditas', is_first_party: true, oidc_conformant: true, cross_origin_auth: false, description: 'This application is managed by the Hereditas CLI. For information, see https://hereditas.app', logo_uri: '', sso: false, callbacks: this._config.get('urls'), allowed_logout_urls: [], allowed_clients: [], client_metadata: { requestTime: '0', waitTime: this._config.get('waitTime') + '', // Cast as string hereditas: '1' }, allowed_origins: [], jwt_configuration: { alg: 'RS256', lifetime_in_seconds: 1800 }, token_endpoint_auth_method: 'none', app_type: 'spa', grant_types: [ 'implicit' ] } } } module.exports = Auth0Management ================================================ FILE: cli/lib/Builder.js ================================================ 'use strict' const fs = require('fs') const crypto = require('crypto') const {Readable} = require('stream') const util = require('util') const Content = require('./Content') const {CleanDirectory} = require('./Utils') const path = require('path') const kw = require('./aes-kw') const argon2 = require('argon2-browser') // Webpack const webpack = util.promisify(require('webpack')) const webpackConfig = require('../../app/webpack.config') // Promisified fs.readdir, fs.stat and fs.unlink const readdirPromise = util.promisify(fs.readdir) const statPromise = util.promisify(fs.stat) // Promisified crypto.pbkdf2 and crypto.randomBytes const pbkdf2Promise = util.promisify(crypto.pbkdf2) const randomBytesPromise = util.promisify(crypto.randomBytes) /** * Object containing properties for a file in the content directory * * @typedef {Object} HereditasContentFile * @property {string} path - Path of the file (relative to the contentDir) * @property {number} size - File size in bytes * @property {string} dist - Random filename used in the dist folder * @property {string} tag - Authentication tag for AES-GCM * @property {string} processed - If the file has been pre-processed, this explains how (e.g. "markdown"); it's undefined otherwise * @property {"text"|"image"|"attach"} display - Configures how the file should be displayed */ /** * Builds a project */ class Builder { /** * Initializes the object * @param {string} passphrase - User passphrase * @param {Config} config - Config object */ constructor(passphrase, config) { // Store config in the object this._config = config this._passphrase = passphrase // Output this.keySalt = null this.indexTag = null this.hasErrors = false } /** * Performs a full build * * @async */ async build() { // Step 1: clean dist directory await CleanDirectory(this._config.get('distDir')) // Step 2: get the list of files let content = await this._scanContent() // Step 3: generate a salt for deriving the encryption key // This needs to be of 64 bytes, which is the length of a SHA-512 hash this.keySalt = await randomBytesPromise(64) // Step 4: derive the master key const masterKey = await this._deriveKey(this._passphrase + this._config.get('appToken'), this.keySalt) // Step 5: encrypt all files content = await this._encryptContent(masterKey, content) // Step 6: write an (encrypted) index file this.indexTag = await this._createIndex(masterKey, content) // Step 7: build the app with webpack const appParams = { distDir: this._config.get('distDir'), authIssuer: 'https://' + this._config.get('auth0.domain'), authClientId: this._config.get('auth0.hereditasClientId'), idTokenNamespace: 'https://hereditas.app', indexTag: this.indexTag, keySalt: this.keySalt, kdf: this._config.get('kdf'), pbkdf2Iterations: this._config.get('pbkdf2.iterations'), argon2Iterations: this._config.get('argon2.iterations'), argon2Memory: this._config.get('argon2.memory') } const webpackStats = await webpack(webpackConfig(appParams)) // Check if webpack compilation had errors if (webpackStats.hasErrors()) { const errors = webpackStats.toJson().errors // eslint-disable-next-line no-console console.error('\x1b[31m\x1b[1m' + 'WEBPACK ERRORS' + '\x1b[0m\n') for (const i in errors) { // eslint-disable-next-line no-console console.error('\x1b[31m' + errors[i] + '\x1b[0m\n') } this.hasErrors = true } if (webpackStats.hasWarnings()) { const warnings = webpackStats.toJson().warnings // eslint-disable-next-line no-console console.warn('\x1b[33m\x1b[1m' + 'WEBPACK WARNINGS' + '\x1b[0m\n') for (const i in warnings) { // eslint-disable-next-line no-console console.warn('\x1b[33m' + warnings[i] + '\x1b[0m\n') } } } /** * Derives a 256 bit key from the passphrase and the salt, using the preferred key derivation function. * The key can be used directly for symmetric encryption. * * @param {string} passphrase - Passphrase for the key * @param {Buffer} salt - Salt for the key * @returns {Promise} Promise that resolves to the buffer with the key * @async */ _deriveKey(passphrase, salt) { const kdf = this._config.get('kdf') if (kdf == 'pbkdf2') { // Using SHA-512, the result is a 512 bit key, so truncate it to 256 bit (32 bytes) return pbkdf2Promise( passphrase, salt, this._config.get('pbkdf2.iterations'), 32, 'sha512' ) } else if (kdf == 'argon2') { return Promise.resolve() .then(() => argon2.hash({ pass: passphrase, salt: salt, type: argon2.ArgonType.Argon2id, time: this._config.get('argon2.iterations'), mem: this._config.get('argon2.memory'), hashLen: 32, parallelism: 1 })) .then((res) => { return Buffer.from(res.hash) }) } else { throw Error('Invalid key derivation function requested') } } /** * Creates an index file and encrypts it on disk. * * @param {Buffer} masterKey - Master encryption key * @param {HereditasContentFile[]} content - List of content * @returns {Buffer} Authentication tag * @async */ async _createIndex(masterKey, content) { // Creat the index file, and convert it to a Readable Stream const indexData = JSON.stringify(content) const inStream = new Readable() inStream._read = () => {} // _read is required, but it's a no-op inStream.push(indexData, 'utf8') inStream.push(null) // End // Output stream const outStream = fs.createWriteStream(path.join(this._config.get('distDir'), '_index')) // Encrypt the index and write it, returning the tag return this._encryptStream(masterKey, inStream, outStream) } /** * Encrypts all the content * @param {Buffer} masterKey - Master encryption key * @param {HereditasContentFile[]} content - List of content * @returns {HereditasContentFile[]} - List of content with the dist and tag properties set * @async */ async _encryptContent(masterKey, content) { // Clone the content object const result = JSON.parse(JSON.stringify(content)) // Iterate through the content and encrypt each file for (const i in result) { // Generate the file name for the output file (a random hex string) const dist = (await randomBytesPromise(12)).toString('hex') // Create the Readable stream to the input, and Writable stream to the output const outStream = fs.createWriteStream(path.join(this._config.get('distDir'), dist)) // Pre-process the file const content = new Content(result[i], this._config) await content.process() result[i] = content.el // Encrypt the stream and get the tag const tagBuf = await this._encryptStream(masterKey, content.inStream, outStream) const tag = tagBuf.toString('base64') // Add the dist and tag properties to the result object result[i].dist = dist result[i].tag = tag } return result } /** * Encrypts a stream using aes-256-gcm * * @param {Buffer} masterKey - Master key; must be 256 bit long * @param {Stream} inStream - Readable stream with the data to encrypt * @param {Stream} outStream - Writable stream to pipe the data to * @returns {Buffer} Authentication tag * @async */ async _encryptStream(masterKey, inStream, outStream) { // Generate a key for this specific file const fileKey = await randomBytesPromise(32) // Generate an IV const fileIV = await randomBytesPromise(12) // Wrap the file's key with the master key, using AES-KW (RFC-3394) const wrappedKey = kw.encrypt(masterKey, fileKey) return new Promise((resolve, reject) => { // Write the wrapped key and IV to the outStream, at the beginning outStream.write(wrappedKey) outStream.write(fileIV) // Create the Cipher, which can be used as a stream transform too const cipher = crypto.createCipheriv('aes-256-gcm', fileKey, fileIV) // When the encryption is done, get the authentication tag cipher.on('end', () => { resolve(cipher.getAuthTag()) }) // In case of errors, throw inStream.on('error', reject) outStream.on('error', reject) // Pipe the input stream through the cipher and then to the output stream inStream.pipe(cipher).pipe(outStream) }) } /** * Recursively scans the content directory, listing files * @returns {HereditasContentFile[]} List of files * @async */ async _scanContent() { // Will contain the final list const result = [] // Recursive function that scans folders const scanFolder = async (folder) => { folder = folder || '' // Scan the list of files and folders, recursively const list = await readdirPromise(path.join(this._config.get('contentDir'), folder)) for (const e in list) { const el = folder + list[e] // Check if we need to include this path or ignore it if (!includePath(el)) { continue } // Check if it's a directory const stat = await statPromise(path.join(this._config.get('contentDir'), el)) if (!stat) { continue } // If it's a directory, scan it recursively if (stat.isDirectory()) { await scanFolder(el + path.sep) } else { // Add the file to the list result.push({ path: el, size: stat.size }) } } } // Get the list await scanFolder() return result } } // Returns true if a path should be included in the box // This ignores files such as operating system's metadata function includePath(str) { const base = path.basename(str) if ( // Linux base.endsWith('~') || base == '.directory' || // macOS base == '.DS_Store' || base == '.AppleDouble' || base == '.LSOverride' || base.startsWith('._') || // Windows base == 'Thumbs.db' || base == 'Thumbs.db:encryptable' || base == 'desktop.ini' || base == 'Desktop.ini' ) { return false } return true } module.exports = Builder ================================================ FILE: cli/lib/Config.js ================================================ 'use strict' const fs = require('fs') const util = require('util') const defaultsDeep = require('lodash.defaultsdeep') const cloneDeep = require('lodash.clonedeep') const SMHelper = require('smhelper') const ConfigVersion = 20190222 /** * Authorized users * * @typedef {object} HereditasUser * @property {string} email - Email address * @property {"user"|"owner"} role - Role: "user" or "owner" */ /** * Configuration dictionary for Hereditas * * @typedef {object} HereditasConfig * @property {number} version - Version of the configuration object * @property {string} contentDir - Folder containing the source content * @property {string} distDir - Folder where to place the compiled project * @property {boolean} processMarkdown - If true, enable processing of Markdown files into HTML * @property {object} auth0 - Auth0 configuration * @property {string} auth0.domain - Auth0 domain/tenant (e.g. "myhereditas.auth0.com") * @property {string} auth0.hereditasClientId - Auth0 app client ID for Hereditas * @property {string} auth0.managementClientId - Client ID for the Auth0 Management app * @property {string} auth0.managementClientSecret - Client Secret for the Auth0 Management app * @property {Array} rules - ID of the Auth0 rules created by the Hereditas CLI * @property {"pbkdf2"|"argon2"} kdf - Key derivation function to use: pbkdf2 or argon2 (default) * @property {object} pbkdf2 - Configuration for pbkdf2 * @property {string} pbkdf2.iterations - Number of iterations * @property {string} webhookUrl - URL of the webhook to trigger when a new user logs into Hereditas. * @property {Array} users - List of users * @property {string} appToken - Application token; when combined with the user passphrase, this allows deriving the encryption key * @property {number} waitTime - The amount of time, in seconds, to wait before Auth0 can return to users the app token * @property {Array} urls - list of URLs where your app will be deployed to, e.g. `https://hereditas.example.com`, or `https://myname.blob.core.windows.net/hereditas`, etc; this is used for OAuth redirects. */ /** * Helper class for managing Hereditas configuration */ class Config { /** * Initializes the object. * * @param {string} [filename="hereditas.json"] - Name of the file on disk */ constructor(filename) { if (!filename) { filename = 'hereditas.json' } this._filename = filename // userConfig is the data read from the config file. config is that, plus defaults this._userConfig = null this._config = {} } /** * Create a new userConfig object. * * Note that this doesn't save changes on disk, you must manually call `save()`. * * @param {HereditasConfig} initConfig - Initial configuration values */ create(initConfig) { this._userConfig = { version: ConfigVersion } defaultsDeep(this._userConfig, initConfig) // Update the config in memory this._config = {} this._defaults() } /** * Reads and parses a config file, validating it. * * @param {string} [filename="hereditas.json"] - Name of the config file to read; default is "hereditas.json" * @throws Throws an error if the config file doesn't exist or is not a valid Hereditas config */ async load() { // Read the file const configFile = await util.promisify(fs.readFile)(this._filename, 'utf8') if (!configFile) { throw Error('Cannot read config file') } // Parse JSON and ensure it's a valid format this._userConfig = JSON.parse(configFile) if (!this._validate()) { throw Error('Invalid config file') } // Apply defaults this._defaults() } /** * Returns value for key from configuration * * @param {string} key - Key of the object, in "dot notation" * @returns {*} Value of the configuration key requested (cloned) */ get(key) { let val // If key contains a dot, we are requesting a nested object if (key.indexOf('.') != -1) { val = SMHelper.getDescendantProperty(this._config, key) } else { val = this._config[key] } // Returns a clone of the object so it can't be modified return cloneDeep(val) } /** * Returns all config values (cloned). * * @returns {HereditasConfig} All configuration data */ all() { return cloneDeep(this._config) } /** * Updates the value of a user config. * * Note: this does NOT save the changes on disk; you must invoke `save()` for that. * * @param {string} key - Name of the key to update, using the "dot notation" * @param {*} value - New value */ set(key, value) { // Update the value and validate the config SMHelper.updatePropertyInObject(this._userConfig, key, value) if (!this._validate()) { throw Error('Invalid config data') } // Update the config in memory this._config = {} this._defaults() } /** * Save changes to user configuration to disk. * * @returns {Promise} Returns a promise that resolves when the changes have been committed to disk. * @async */ save() { return util.promisify(fs.writeFile)(this._filename, JSON.stringify(this._userConfig, null, 2)) } /** * Validates a config object, ensuring that all required keys are present. * * @returns {boolean} Returns true on valid configuration objects * @throws Throws an Error if the config file isn't valid */ _validate() { if (!this._userConfig || typeof this._userConfig != 'object' || !Object.keys(this._userConfig).length) { throw Error('Invalid config file') } if (!this._userConfig.version) { throw Error('Config file is missing required key version') } if (!this._userConfig.distDir) { throw Error('Config file is missing required key distDir') } if (!this._userConfig.contentDir) { throw Error('Config file is missing required key contentDir') } if (!this._userConfig.appToken) { throw Error('Config file is missing required key appToken') } if (!this._userConfig.auth0 || typeof this._userConfig.auth0 != 'object' || !Object.keys(this._userConfig.auth0).length) { throw Error('Config file is missing required key auth0') } if (!this._userConfig.auth0.domain) { throw Error('Config file is missing required key auth0.domain') } if (!this._userConfig.urls || !Array.isArray(this._userConfig.urls) || !this._userConfig.urls.length) { throw Error('Config file is missing required key urls') } return true } /** * Applies default parameters to the userConfig object, and stores that into the config object */ _defaults() { defaultsDeep(this._config, this._userConfig, { processMarkdown: true, kdf: 'argon2', pbkdf2: { iterations: 100000 }, argon2: { iterations: 2, memory: 64 * 1024 } }) } } module.exports = Config ================================================ FILE: cli/lib/Content.js ================================================ 'use strict' const fs = require('fs') const {Readable} = require('stream') const util = require('util') const path = require('path') // Marked.js const marked = util.promisify(require('marked')) // Promisified fs.readFile, fs.readdir, fs.stat and fs.unlink const readFilePromise = util.promisify(fs.readFile) // List of file extensions of images const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'] /** * Processes content */ class Content { /** * Constructor * * @param {HereditasContentFile} el - Content to process * @param {Config} config - Config object */ constructor(el, config) { this._config = config this._el = el this._inStream = null } /** * Object with the content information, potentially modified * * @returns {HereditasContentFile} Content information object */ get el() { return this._el } /** * Readable stream to the (optionally, pre-processed) content * * @returns {ReadableStream} Stream with content data */ get inStream() { return this._inStream } /** * Pre-processes the content in any way necessary, e.g. converting Markdown into HTML. */ async process() { if (this._el.path.match(/\.txt$/i)) { await this._processText() } else if (this._el.path.match(/\.(md|markdown)$/i)) { await this._processMarkdown() } else { await this._processBinary() } } /** * Processes images and other binary files */ async _processBinary() { // Just get a stream to the file on disk this._inStream = fs.createReadStream(path.join(this._config.get('contentDir'), this._el.path)) // Set the display as "image" for images, and "attach" for anything else const extension = this._el.path.split('.') .pop() .toLowerCase() this._el.display = (imageExtensions.indexOf(extension) < 0) ? 'attach' : 'image' } /** * Processes simple Text files */ async _processText() { // Get a stream to the file and display it as text this._inStream = fs.createReadStream(path.join(this._config.get('contentDir'), this._el.path)) this._el.display = 'text' } /** * Processes Markdown files, converting them to HTML */ async _processMarkdown() { // Check if we process Markdown into HTML if (this._config.get('processMarkdown')) { const markdown = await readFilePromise(path.join(this._config.get('contentDir'), this._el.path), 'utf8') const html = await marked(markdown) // Push the data into a stream this._inStream = new Readable() this._inStream._read = () => {} // _read is required, but it's a no-op this._inStream.push(html, 'utf8') this._inStream.push(null) // End // Mark the file as pre-processed this._el.processed = 'markdown' this._el.display = 'html' // TODO: Handle different encodings } else { // If not processing them, treat Markdown files as simple text await this._processText() } } } module.exports = Content ================================================ FILE: cli/lib/Crypto.js ================================================ 'use strict' const crypto = require('crypto') const util = require('util') /** * Generates a token with `length` random bytes, and returns it as a base64-encoded string. * * @param {number} length - Number of bytes to generate (before converting to base64) * @returns {string} Token represented as base64-encoded string */ async function GenerateToken(length) { if (!length || length < 0) { length = 20 } const bytes = await util.promisify(crypto.randomBytes)(length) return bytes.toString('base64') } module.exports = { GenerateToken } ================================================ FILE: cli/lib/Utils.js ================================================ const fs = require('fs') const util = require('util') const path = require('path') const readdirPromise = util.promisify(fs.readdir) const unlinkPromise = util.promisify(fs.unlink) const rmdirPromise = util.promisify(fs.rmdir) /** * Deletes all files in a directory, without removing the directory itself. * * @param {string} directory - Directory to clean * @async */ async function CleanDirectory(directory) { const files = await readdirPromise(directory) return Promise.all(files.map( (file) => { const target = path.join(directory, file) const stat = fs.lstatSync(target) if (stat.isDirectory()) { return CleanDirectory(target) .then(() => rmdirPromise(target)) } return unlinkPromise(target) } )) } module.exports = { CleanDirectory } ================================================ FILE: cli/lib/aes-kw.js ================================================ /** * This module is based on https://github.com/calvinmetcalf/aes-kw * * Copyright (C) Calvin Metcalf. Released under MIT license. */ const crypto = require('crypto') const xor = require('buffer-xor/inplace') const bufferEq = require('buffer-equal-constant-time') const IV = Buffer.from('A6A6A6A6A6A6A6A6', 'hex') const EMPTY_BUF = Buffer.alloc(0) function Encrypter(key, decipher) { if (decipher) { this.cipher = crypto.createDecipheriv(getCipherName(key), key, EMPTY_BUF) } else { this.cipher = crypto.createCipheriv(getCipherName(key), key, EMPTY_BUF) } this.cipher.setAutoPadding(false) } Encrypter.prototype.encrypt = function(iv, buf) { if (iv.length !== 8) { throw new Error('invalid iv length') } if (buf.length !== 8) { throw new Error('invalid data length') } this.cipher.update(iv) return this.cipher.update(buf) } Encrypter.prototype.done = function() { this.cipher.final() } function getCipherName(key) { switch (key.length) { case 16: return 'aes-128-ecb' case 24: return 'aes-192-ecb' case 32: return 'aes-256-ecb' } } function msb(b) { return b.slice(0, 8) } function lsb(b) { return b.slice(-8) } exports.encrypt = encrypt function encrypt(key, plaintext) { if (plaintext.length % 8) { throw new Error('must be 64 bit increment') } const enc = new Encrypter(key) let j = -1 let i, b const t = Buffer.alloc(8) let a = IV const n = plaintext.length / 8 const r = createR(plaintext) while (++j <= 5) { i = -1 while (++i < n) { b = enc.encrypt(a, r[i]) t.writeUInt32BE(0, 0) t.writeUInt32BE((n * j) + i + 1, 4) a = xor(msb(b), t) r[i] = lsb(b) } } enc.done() return Buffer.concat([a].concat(r)) } exports.decrypt = decrypt function decrypt(key, ciphertext) { if (ciphertext.length % 8) { throw new Error('must be 64 bit increment') } const enc = new Encrypter(key, true) let j = 6 let i, b const t = Buffer.alloc(8) const n = ciphertext.length / 8 const r = createR(ciphertext) let a = r[0] while (--j >= 0) { i = n while (--i) { t.writeUInt32BE(0, 0) t.writeUInt32BE(((n - 1)* j) + i, 4) a = xor(a, t) b = enc.encrypt(a, r[i]) a = msb(b) r[i] = lsb(b) } } enc.done() if (!bufferEq(a, IV)) { throw new Error('unable to decrypt') } return Buffer.concat(r.slice(1)) } function createR(buf) { const n = buf.length / 8 const out = new Array(n) let i = -1 while (++i < n) { out[i] = buf.slice(i * 8, (i + 1) * 8) } return out } ================================================ FILE: docs-source/.gitignore ================================================ # Generated files /content/cli/*.md !/content/cli/__template.md /content/menu/*.md !/content/menu/__template.md /dist # Created by https://www.gitignore.io/api/hugo # Edit at https://www.gitignore.io/?templates=hugo ### Hugo ### # gitginore template for Hugo projects # website: https://gohugo.io # generated files by hugo /public/ /resources/_gen/ # executable may be added to repository hugo.exe hugo.darwin hugo.linux # End of https://www.gitignore.io/api/hugo ================================================ FILE: docs-source/config.yaml ================================================ baseURL: "https://hereditas.app/" languageCode: en-us title: Hereditas # Ignore files ignoreFiles: - "\\.sh$" - "Makefile" - "Dockerfile" - "__template.md" # Enable all URLs to be relative, and make them end with ".html" relativeURLs: true canonifyURLs: false uglyurls: true # Book Theme is intended for documentation use, therefore it doesn't render taxonomy. # You can hide related warning with config below disableKinds: - taxonomy - taxonomyTerm - section # Goldmark markup: goldmark: renderer: unsafe: true # Syntax highlighting pygmentsCodeFences: true pygmentsStyle: "tango" # Google analytics #googleAnalytics: UA-72379106-2 # Privacy privacy: googleAnalytics: anonymizeIP: true youtube: privacyEnhanced: true # Theme theme: book # Theme params params: # (Optional, default true) Show or hide table of contents globally # You can also specify this parameter per page in front matter BookShowToC: true # (Optional, default none) Set leaf bundle to render as side menu # When not specified file structure and weights will be used BookMenuBundle: /menu # (Optional, default docs) Specify section of content to render as menu # You can also set value to "*" to render all sections to menu BookSection: docs # This value is duplicate of $link-color for making active link highlight in menu bundle mode # BookMenuBundleActiveLinkColor: \#004ed0 # Include JS scripts in pages. Disabled by default. # - Keep side menu on same scroll position during navigation BookEnableJS: false # Set source repository location. # Used for 'Last Modified' and 'Edit this page' links. #BookRepo: https://github.com/ItalyPaleAle/hereditas/docs # Enable "Edit this page" links for 'doc' page type. # Disabled by default. Uncomment to enable. Requires 'BookRepo' param. # Path must point to 'content' directory of repo. #BookEditPath: edit/master/exampleSite/content # Plausible Analytics PlausibleAnalytics: Domain: hereditas.app ================================================ FILE: docs-source/content/_index.md ================================================ --- title: What is Hereditas type: docs --- ![Hereditas logo](/images/hereditas-logo.png) # What is Hereditas **What happens to your digital life after you're gone?** Hereditas, which means *inheritance* in Latin, is a static website generator that builds **fully-trustless digital legacy boxes**, where you can store information for your relatives to access in case of your sudden death or disappearance. For example, you could use this to pass information such as passwords, cryptographic keys, cryptocurrency wallets, sensitive documents, etc. {{< youtube lZEKgB5dzQ4 >}} > Note: the video above was recorded with Hereditas 0.1. The design of the interface has been improved and made nicer in 0.2. ## Why we built Hereditas Check out the announcement [**blog post**](https://withblue.ink/2019/03/18/what-happens-to-your-digital-life-after-youre-gone-introducing-hereditas.html?utm_source=web&utm_campaign=hereditas-docs) to understand more about why we built Hereditas and why you need it too. ## Design We've designed Hereditas with three principles in mind. ### Fully trustless – really With Hereditas, you don't need to trust any person or provider. **No other person or company has standing access to your data.** As the owner of an Hereditas box, you can nominate some authorized users by whitelisting their email address and giving them a *user passphrase*. To prevent authorized users from having standing access to your data, however, once they log into your Hereditas box for the first time, they need to wait for a few hours or days before they can unlock the box. This gives you, the owner of the box, enough time to stop the timer, by simply logging into the same Hereditas box. For example, if you set the waiting time to 24 hours (the default), when a relative of yours tries to log in the timer starts and Hereditas sends you a notification right away. If you've not disappeared, you can log into the same Hereditas box within 24 hours and stop the timer. Without any action from you, after the delay has passed all your relatives would be able to unlock your Hereditas box by logging in again and typing the *user passphrase*. Hereditas generates digital legacy boxes that are encrypted bundles within static HTML5 applications. The encryption key is split between what you give your users and what's stored inside the authorization provider, so no company or provider has standing access to your data. ### Simple for your loved ones We designed Hereditas so it's simple to use for your loved ones, when they need to access your digital legacy box, even if they are not tech wizards. **A web browser is all they need.** As the owner of the Hereditas box, you will provide them with the URL where they can find your box, and the *user passphrase* they need to use to unlock it. You will also whitelist their email address so they can log in with their existing accounts (e.g. Google, Facebook, Microsoft…) – no need to create new accounts for them and have new passwords around. ### No costly and/or time-consuming maintenance You don't want to rely on a solution that you'll have to keep paying and/or patching for the rest of your life (and in this case, we mean that literally). **Hereditas outputs a static HTML5 app that you can host anywhere you'd like**, for free or almost. ## Open source We made Hereditas fully open source so you can study how the app works down to every detail. We wrote the app in JavaScript, and we use Node.js for the CLI and HTML5 for the static web app. **The source code is available on GitHub at [ItalyPaleAle/hereditas](https://github.com/ItalyPaleAle/hereditas)** under GNU General Public License (GPL) version 3.0 (see [LICENSE](https://github.com/ItalyPaleAle/hereditas/tree/master/LICENSE.md)). We happily accept contributions! Feel free to submit a Pull Request to fix bugs or add new features. Equally important, you can contribute by improving this [documentation](https://github.com/ItalyPaleAle/hereditas/tree/master/docs-source) you're reading. If you believe you've found a security issue that could impact other people, please [report it confidentially](https://www.npmjs.com/advisories/report?package=hereditas). ## Get started Ready? Get started with Hereditas now! }}">Quickstart Video Or: }}">Get started documentation ================================================ FILE: docs-source/content/advanced/auth0-manual-configuration.md ================================================ --- title: Auth0 manual configuration type: docs --- # Auth0 manual configuration Hereditas uses Auth0 to authenticate users and to provide the *application token*, which is part of the string used to derive the encryption key. This document explains the configuration that the Hereditas CLI performs when you execute the [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) command. > **Important:** this page is primarily primarily meant as reference. We recommend letting the Hereditas CLI manage the Auth0 configuration with the [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) command rather than changing settings manually. ## Differences with the "API Access" application In the [Auth0 setup]({{< relref "/guides/auth0-setup.md" >}}) article we guided you through the creation of an **API Access** app ("Machine to Machine") and how to get the credentials, which are used by the Hereditas CLI to configure Hereditas on Auth0, including setting up the rules, and also by the Hereditas rules on Auth0 to set timers. This document focuses on the main "Hereditas" application on Auth0, which is what users will authenticate with. ## Hereditas application On Auth0, create an application of type **Single Page Application**. You can name it any way you want, but `Hereditas` is probably a good name. Once the app is created, take note of the **Domain** and the **Client ID**. We will not need the Client Secret. ### Application configuration Ensure that the application is configured with: - **Application type**: Should be "Single Page Application" - **Allowed callback URLs**: List of URLs (one per line) where your box is deployed to - **JWT Expiration**: Recommended to set it to a value that make sense for you, for example 1800 seconds (30 mins) Under **Advanced Settings**, then **OAuth**: - **JsonWebToken Signature Algorithm**: Should be "RS256" - **OIDC Conformant**: Should be enabled In the **Grant types** tab: - **Grants**: choose only "Implicit" ### Application Metadata The application needs to be configured with the following "Application Metadata" (called `client_metadata` in the Auth0 APIs): - **`hereditas`**: this is required and must be set to `1`. - **`requestTime`**: set this value to `0`. When users that are not owners sign in, the application rules automatically update this value with the current time (as UNIX timestamp). - **`waitTime`**: the amount of time, in seconds, to wait before Auth0 can return to users (non-owners) the app token. Set this value to whatever makes sense for you; `86400` (1 day) is often a good amount of time. ## Rules The `auth0` folder in the repository contains the rules that need to be configured in Auth0. Note that the order below is very important! - **Hereditas 01 - Whitelist email addresses (`01-whitelist.js`)**: This rule configures which users are allowed to authenticate, by whitelisting their email address. - **Hereditas 02 - Notify (`02-notify.js`)**: This rule sends a notification on all successful logins via a webhook. - **Hereditas 03 - Wait logic (`03-wait-logic.js`)**: This rule implements the "wait logic". If a non-owner users signs in, the rule starts the timer (by setting the current timestamp in the `waitTime` application metadata). After the wait is over, this same rule adds the app token to the claim. If an owner signs in, the timer is reset (and the app token is added to the claim regardless). The scripts above contain some tokens that need to be replaced with the list of email addresses of all users or just owners. - **`/*%OWNERS%*/`** This token needs to be replaced with the JSON-encoded array of the email addresses of users who are owners. - **`/*%ALL_USERS%*/`** This token needs to be replaced with the JSON-encoded array of the email addresses of all users. For example: ````js const whitelist = /*%ALL_USERS%*/; const owners = /*%OWNERS%*/; // Become const whitelist = ["me@example.com", "someone@example.com"]; const owners = ["me@example.com"]; ```` In the rules page, add the following settings. You will need some credentials from the "API Access" app, which is the "Machine to Machine" app created in the getting started guide. - **`APP_TOKEN`**: the application token part of the encryption key. - **`AUTH0_CLIENT_ID`**: Set this to the Client ID of the API Access app. - **`AUTH0_CLIENT_SECRET`**: Set this to the Client Secret of the API Access app. - **`WEBHOOK_URL`**: URL of the webhook invoked after a successful authentication (see the [Login notifications]({{< relref "/guides/login-notifications.md" >}})). ================================================ FILE: docs-source/content/advanced/building-self-contained-binaries.md ================================================ --- title: Building self-contained binaries type: docs --- # Building self-contained binaries Starting with Hereditas 0.2, in addition to generating a set of files to be served via HTTP, you can also build a self-contained binary that can be distributed as an app without further dependencies. This binary launches a local web server, and it contains and serves all of your Hereditas box, including the (encrypted) files. The [`hereditas pack`]({{< relref "/cli/pack.md" >}}) command, run inside your Hereditas working directory, automatically builds binaries for Windows (32-bit and 64-bit), macOS, and Linux (amd64, i386, arm64, armv7). ## Requirements There are a few requirements before you can run the `hereditas pack` command (the CLI will check them for you too): 1. You need to have the Go compiler installed in your laptop, at least version 1.13. 1. You need to have packr2 installed in your laptop and available in your `PATH`; you can fetch it with `go get -u github.com/gobuffalo/packr/v2/packr2` or you can get a [pre-compiled binary](https://github.com/gobuffalo/packr/releases). 1. The URL `http://localhost:8080` must be allowed for your Hereditas box. You can do that with `hereditas url:add -u http://localhost:8080` (and then `hereditas auth0:sync`). 1. You must have already built your Hereditas box. That is, you must have run the `hereditas build` command. ## Build the binaries Run the [`hereditas pack`]({{< relref "/cli/pack.md" >}}) command to automatically build the binaries; this can take a couple of minutes. ```sh hereditas pack ``` The binaries will be placed in the `_bin` folder: ```sh ~/hereditas $ ls _bin hereditas-box-linux-386 hereditas-box-linux-amd64 hereditas-box-linux-arm64 hereditas-box-linux-armv7 hereditas-box-macos hereditas-box-win32.exe hereditas-box-win64.exe ``` Pick the right binary for your system(s) and distribute them in any way you see fit. > **macOS and Gatekeeper:** Hereditas does not sign the macOS binary, as that requires a developer certificate from Apple. The app you compile will run in your Mac without issues, but if you distribute it to other people, Gatekeeper might refuse to run it as it's unsigned. Read more about Gatekeeper in the [Apple support site](https://support.apple.com/en-us/HT202491). ## Running the self-contained app Most users will be able to open your Hereditas box by double-clicking on the binary. The app runs in the command line, and it should automatically open a terminal if you launch it through your operating system's shell. The app launches a web server listening on `127.0.0.1` and on port `8080`. It will then automatically open the user's default web browser (if possible) with the URL `http://localhost:8080`. ================================================ FILE: docs-source/content/advanced/configuration-file.md ================================================ --- title: Configuration file type: docs --- # Configuration file Each Hereditas working folder contains a JSON configuration file called `hereditas.json`. This file is automatically generated when running the [`hereditas init`]({{< relref "/cli/init.md" >}}) command, and it's modified by the app itself when you run certain commands. ## Contents The structure of the document is similar to the following: ````json { "version": 20190222, "contentDir": "content", "distDir": "dist", "appToken": "...", "waitTime": 86400, "users": [ { "email": "me@example.com", "role": "owner" }, { "email": "someone@example.com", "role": "user" } ], "auth0": { "domain": "myhereditas.auth0.com", "managementClientId": "...", "managementClientSecret": "...", "hereditasClientId": "...", "rules": [ '...', '...', '...' ] }, "urls": [ "https://my.testhereditas.app", "https://another.testhereditas.app" ], "webhookUrl": "https://example.com/webhook/token/abc123XYZ", "kdf": "argon2", "argon2": { "memory": 65536 }, "pbkdf2": { "iterations": 100000 }, "processMarkdown": true } ```` ## Configuration options ### Hereditas working folder options These options configure the way the Hereditas current working folder is set up. - **`version`** (int): This represents the version of the configuration file. At present moment, this is `20190222`. - **`contentDir`** (string): Name of the directory, inside the Hereditas working folder, containing the documents and files to include in the Hereditas box. The content of this folder is not encrypted, as the CLI will do that automatically.
The default value is `content`. This value can be set during box initialization with the `--content` option for the [`hereditas init`]({{< relref "/cli/init.md" >}}) command. - **`distDir`** (string): Name of the directory, inside the Hereditas working folder, where the CLI will put the generated web app and the encrypted content.
The default value is `dist`. This value can be set during box initialization with the `--dist` option for the [`hereditas init`]({{< relref "/cli/init.md" >}}) command. ### Access control basics These options are used to configure the way Hereditas protects access to your box. - **`appToken`** (string): The *application token*, which together with the *user passphrased* is used to derive the encryption and decryption key. The application token is stored in the Hereditas configuration file and synced with Auth0. This is an important secret to protect, although by itself (without the *user passphrase*, which isn't stored anywhere) it isn't sufficient to encrypt or decrypt your data.
This value is generated automatically when a new Hereditas project is initialized (using [`hereditas init`]({{< relref "/cli/init.md" >}})), and can be re-generated with [`hereditas regenerate-token`]({{< relref "/cli/regenerate-token.md" >}}). Note that changing this value will require re-building and re-deploying your box. - **`waitTime`** (string): Amount of time users need to wait before they can unlock an Hereditas box, in seconds. The default value is `86400`, or 24 hours.
This value can be also set with [`hereditas wait-time:set`]({{< relref "/cli/wait-time_set.md" >}}) - **`users`** (array of objects): Array of users that are whitelisted to use the app. Users can authenticate with Auth0 using any social account they support, e.g. Google, Facebook, Microsoft, etc; as long as the email address matches what's whitelisted in this configuration option. For more information, please refer to the documentation on [Managing users]({{< relref "/guides/managing-users.md" >}}). This value is an array of objects with the structure: - **`users.$.email`** (string): email address of the user - **`users.$.role`** (string): either `user` or `owner` ### Auth0 settings and credentials The **`auth0`** object contains settings and credentials for communicating with the Auth0 APIs. For more information, please refer to the [Auth0 Setup]({{< relref "/guides/auth0-setup.md" >}}) article. - **`auth0.domain`** (string): domain on Auth0, e.g. `myhereditas.auth0.com`. This is created when you sign up for Auth0, and it's globally unique.
This is set with the `--auth0Domain` option for the [`hereditas init`]({{< relref "/cli/init.md" >}}) command. - **`auth0.managementClientId`** (string): Client Id (ie. public key) for the "API Access" application on Auth0, a "Machine-to-Machine" application used by the Hereditas CLI and the rules to interact with the Auth0 APIs.
This is set with the `--auth0ClientId` option for the `hereditas init` command. - **`auth0.managementClientSecret`** (string): Client Secret (ie. private key) for the "API Access" application on Auth0.
This is set with the `--auth0ClientSecret` option for the `hereditas init` command. - **`auth0.hereditasClientId`** (string): Client Id (ie. public key) for the "Hereditas" application on Auth0. This value is generated automatically by the Auth0 CLI, when running the [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) command. In most cases, you should not edit this Client Id manually. - **`auth0.rules`** (array of strings): These strings represent the Ids of the rules that the Hereditas CLI creates on Auth0. These are returned automatically by the Auth0 CLI, when running the [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) command. In most cases, you should not edit this array manually. ### Deployment and webhook URLs The Hereditas configuration file stores two separate kinds of URLs: - **`urls`** (array of strings): list of URLs where the Hereditas application is deployed to. These URLs are synced with Auth0, which uses them to whitelist callback URLs after users authenticate successfully. You should list every URL where your app might be reachable at. Note that the protocol needs to match too, so `http://example.com` and `https://example.com` are separate URLs. For more information, see the [Auth0 Setup]({{< relref "/guides/auth0-setup.md" >}}) and [Deploying the box]({{< relref "/guides/deploy-box.md" >}}) articles.
You need to provide (at least) one URL with the `--url` flag when running [`hereditas init`]({{< relref "/cli/init.md" >}}). These URLs can also be changed with CLI commands, such as [`url:add`]({{< relref "/cli/url_add.md" >}}) and [`url:rm`]({{< relref "/cli/url_rm.md" >}}). - **`webhookUrl`** (string): URL of the webhook that is invoked to notify owners of a successful authentication. Please refer to the [Login notifications]({{< relref "/guides/login-notifications.md" >}}) article for more information.
This value can also be set with the [`webhook:set`]({{< relref "/cli/webhook_set.md" >}}) command. ### Advanced options These options are set by default by Hereditas. You shouldn't change these options unless you have a good reason for that, and you're confident that you know what you're doing. - **`kdf`** (string): Key derivation function to use. Supported values are `argon2` for Argon2id (default), and `pbkdf2` for PBKDF2. - **`argon2`** (object): Parameters for deriving a key with Argon2. - **`argon2.iterations`** (int): Iterations used by Argon2 (in Argon2id mode). Default value is 2. - **`argon2.memory`** (int): Memory used by Argon2 (in Argon2id mode), in bytes. Default value is 65536 (64KB) - **`pbkdf2`** (object): Parameters for deriving a key with PBKDF2. - **`pbkdf2.iterations`** (int): Number of iterations to use for PBKDF2. Default value is 100000 (1E+05) - **`processMarkdown`** (boolean): Switch to enable/disable the conversion of Markdown documents into HTML, when building the box. Default is true (enabled). ================================================ FILE: docs-source/content/advanced/index-file.md ================================================ --- title: Index file type: docs --- # Index file Each Hereditas box contains an encrypted file named `_index`. ## Encryption details The index file is encrypted, just like all other files, with AES-256-GCM. The encryption key is a unique, random sequence of 32 bytes (256 bits), which is wrapped with the master key and then stored (wrapped) at the beginning of the file. Additionally, the 12-byte IV is randomly generated too and stored in the file's data right after the wrapped key. As a result of the usage of GCM, which is an authenticated cipher, the encryption step outputs an authentication tag too. The index file's authentication tag is stored inside the JavaScript file in cleartext and it's used to certify that the index file's content are authentic. ## Contents In celartext, the index file is a JSON document listing all files inside the Hereditas box. For example: ````json [ { "path": "hello.md", "dist": "043bd2a8986b5ed805737ab8", "size": 248, "display": "html", "tag": "VczD/yHW3XtcH2nNyt9Q4w==", "processed": "markdown" }, { "path": "photo.jpg", "dist": "122c1a87b03db8793eb90d53", "size": 10181034, "display": "image", "tag": "Zsh42WN+iy05M6CaXtlhPA==" }, { "path": "folder/passwords.pdf", "dist": "715f14d0479b455ed481af9f", "size": 60600, "display": "attach", "tag": "CjurYwY6KeeTrmJsKxdR1A==" } ] ```` The JSON document is an array of objects each representing a file: - `path`: The original path of the file in the content folder - `dist`: Name of the encrypted file - `size`: The size of the original file, in bytes - `display`: Instructs the Hereditas web app on how to display the file. The generator determines this based on the file extension. Accepted values are: - `html`: Display the content as HTML fragment inside the page (for converted Markdown files) - `text`: Display the content as pre-formatted text, in a `
` HTML block (for text files)
    - `image`: Display the image inline (for images)
    - `attach`: Prompts to download the file
- `tag`: The authentication tag for the encrypted file, as returned by the GCM cipher
- `processed`: Contains information on how the file was pre-processed. If not present, it means the file wasn't pre-processed. Possible values:
    - `markdown`: The Markdown file was converted to HTML


================================================
FILE: docs-source/content/cli/__template.md
================================================
---
title: {{{commandName}}}
type: docs
---

# hereditas {{{commandName}}}

{{{shortDescription}}}

## Description

{{{longDescription}}}

{{#usage}}
## Example usage

````sh
{{{usage}}}
````
{{/usage}}

{{#hasFlags}}
## Flags
| Flag | Type | Required | Default Value | Description |
|---|---|---|---|---|
{{#flags}}
|{{{name}}} | {{{type}}} | {{required}} | {{{defaultValue}}} | {{description}} |
{{/flags}}
{{/hasFlags}}


================================================
FILE: docs-source/content/guides/auth0-setup.md
================================================
---
title: Auth0 setup
type: docs
---

# Auth0 setup

[Auth0](https://auth0.com/) is an authentication provider built to be flexible, safe and reliable. It offers a generous free tier that is more than enough for any user of Hereditas.

On Auth0, users can authenticate using their existing social logins, including Google, Facebook, Microsoft accounts; the list of supported providers is [fairly long](https://auth0.com/docs/identityproviders). This is very convenient because it lets your users sign in with existing credentials, so you don't need to create new accounts (and passwords) for them. It also offers increased security, as providers like Microsoft, Google, Facebook (and Auth0 itself) have a powerful infrastructure to prevent and detect malicious logins (often using AI trained on millions of authentications by their users every day), and they support Multi-Factor Authentication.

> **Why do we need Auth0?**
>
> Hereditas is a static website generator. In order to reduce the need for future maintenance and keeping operating costs down to zero (or almost), Hereditas outputs a static HTML5 web app with no server-side code at all. HTML5 apps nowadays are extremely powerful and we are able to do advanced cryptographic operations within a web browser.
>
> However, in order to implement the wait timer (ensuring that users need to wait a certain amount of time after their first login to unlock your box), we needed to store data in a centralized repository. Using Auth0 and splitting the encryption key between the *user passphrase* given to your users, and the *application token* stored inside the authentication provider, lets us precisely do that, while still maintaining the promise of a fully trustless platform. For more information, check out the [Security model]({{< relref "/introduction/security-model.md" >}}) article.

## Sign up for Auth0

On the [Auth0 website](https://auth0.com/), sign up and create a new (free) account.

After creating an account, you should automatically be redirected to the [Auth0 management portal](https://manage.auth0.com/), where you can create a new domain. Choose a name (must be universally unique) and a region, then continue the process until you've created your account and domain.

![Auth0 management portal: new domain creation](/images/auth0-setup-create-domain.png)

## Create the "API Access" application

Once you are inside the Auth0 management portal, click on the button to create a new application.

Throughout this documentation, we'll name this new application "API Access", even though you can choose whichever name you prefer. Choose type "Machine to Machine App", then create the app.

![Auth0 management portal: create a new Machine to Machine application](/images/auth0-setup-create-application.png)

In the next step, you need to grant this application access to the Auth0 APIs. From the dropdown menu, select "Auth0 Management API". Then, select **all and only** the following scopes:

- read:clients
- update:clients
- create:clients
- read:rules
- update:rules
- delete:rules
- create:rules
- update:rules_configs

![Auth0 management portal: enable Auth0 Management APIs](/images/auth0-setup-create-application-api.png)

Lastly, from the Settings tab, take note of the following values, which we'll need to pass to the Hereditas CLI:

- Domain
- Client ID
- Client Secret

![Auth0 management portal: obtain credentials for API Access app](/images/auth0-setup-credentials.png)

## Configure connections

One of the main benefits of using Auth0 is that it integrates with third-party identity providers such as Google, Microsoft, Facebook. This lets you skip creating new accounts for your users, so they can sign in with their existing credentials. Not only there's one less password for them to remember, but it's also safer: the external providers can support Multi-Factor Authentication, and can use advanced tools (often AI-based) to better detect hacked accounts.

### Disable Username and Password authentication

By default, Auth0 offers users the possibility to create a new account specific to your app. You might want to disable that and allow social logins only. (While we recommend doing this, it's entirely optional)

In the Auth0 Management management portal, on the menu on the left side navigate to **Connections**, then **Database**.

In the row for the "Username-Password-Authentication" database, click on "Settings".

![Auth0 management portal: database](/images/auth0-setup-database.png)

Scroll to the bottom of the page and click on the big, red button to remove the connection.

![Auth0 management portal: remove the database](/images/auth0-setup-database-remove.png)

### Configure social logins

In the Auth0 management portal, this time navigate to the **Connections** and then **Social** page.

Here, you can configure all the social login providers, including Google, Facebook, and Microsoft.

![Auth0 management portal: social logins](/images/auth0-setup-social-logins.png)

Each provider has a different procedure for setting the connection up, and you can follow the Auth0 documentation for instructions.

You can enable any provider you want, and your users will be able to use anyone of them. Because Hereditas whitelists users based on their email addresses, it doesn't matter what provider they use to authenticate, as long as the email address matches.

**Important:** we need providers to return users' email addresses. When you configure a new social provider, make sure that it supports sharing of users' email addresses (not all of them do, e.g. Twitter), and that the **_email_ scope is enabled** when not included in the basic info.

## Next step: Login notifications

In the next step, we'll configure a webhook to send notifications when users sign into your Hereditas box.

}}">Login notifications


================================================
FILE: docs-source/content/guides/build-static-web-app.md
================================================
---
title: Build the static web app
type: docs
---

# Build the static web app

In the previous step we created an Hereditas box, and now we're finally ready to build the static web app.

## Build the web app

You're finally ready to build the static web app, using the [`hereditas build`]({{< relref "/cli/build.md" >}}) command:

````sh
hereditas build
````

This will ask you to **type the _user passphrase_**, which needs to be at least 8 characters long. You can choose any passphrase you'd like, but a good practice is to use a bunch of words in your native language. If you're interested in the subject, check out [XKCD 936](https://www.explainxkcd.com/wiki/index.php/936:_Password_Strength).

Once the command is done, you'll see your generated files in the `dist` folder:

````text
~/hereditas $ ls content
hello.md
photo.webp
subfolder
text.txt
tulips.jpg

~/hereditas $ hereditas build
User passphrase: ***********
Finished building project in dist (took 3.895 seconds)

~/hereditas $ ls dist
1.1.9a2b99a07d39e25b4b7f.js
24ec53f0e99728db2f471caf
35bc79f07f20c003532724bf
430c96dc23ccca5eb4227508
_index
d0160be2ee0f1479367b325c
d9f2eb6a0f34382c36d2a116
hereditas.9a2b99a07d39e25b4b7f.css
hereditas.9a2b99a07d39e25b4b7f.js
index.html
robots.txt
````

> The Hereditas CLI uses [webpack](https://webpack.js.org/) behind the scenes to generate your static app. By default, the JavaScript code is bundled and minified. If you're looking at modifying the Hereditas source and want to skip the minification (for much faster builds) and include a sourcemap, you can call `NODE_ENV=development hereditas build` instead.

## Next step: Managing users

We're almost there. Before you can actually deploy your box (or test it locally), we need to configure the list of users who can unlock it.

}}">Managing users


================================================
FILE: docs-source/content/guides/create-box.md
================================================
---
title: Create the box
type: docs
---

# Create the box

After gathering all the content you want to encrypt, setting up our "API Access" application on Auth0, and configuring a webhook endpoints to send notifications, we can now create a box on our laptop. This will be our "working directory".

## Initialize a working directory

Create a **new, empty folder** on your laptop. Open a terminal inside that folder, then run:

````sh
hereditas init \
   --auth0Domain "yourdomain.auth0.com" \
   --auth0ClientId "..." \
   --auth0ClientSecret "..." \
   --url "http://localhost:5000"
````

You'll need to pass some options to the command above:

- `--auth0Domain` is your domain on Auth0, created in the previous step
- Set `--auth0ClientId` and `--auth0ClientSecret` to the Client Id and Client Secret for the "API Access" app you just created in Auth0
- `--url` is the URL where the app will be deployed to. We'll be testing locally before deploying the app, so for now you might just want to keep this to `http://localhost:5000`. We can always change this later, without having to re-build the Hereditas box.

After running the command, you'll see that your folder now contains three objects:

````text
~/hereditas $ ls
content
dist
hereditas.json
welcome.md
````

- The `content` folder is where you store the data you wish to encrypt
- The `dist` folder will contain the generated web app
- The `hereditas.json` file contains the configuration for the Hereditas box
- The `welcome.md` file contains a welcome message that is displayed in the login page; this file is not encrypted.

> In most cases you will not need to manually edit the `hereditas.json` configuration file, as you can use the Hereditas CLI to change the most common options. However, you can find the full reference for the configuration file in the [Configuration file]({{< relref "/advanced/configuration-file.md" >}}) article.

## Content

Place all the content you want to encrypt in the `content` folder. You can store any kind of file in this folder and sub-folders. The [Get started]({{< relref "/guides/get-started.md" >}}#step-zero-gather-all-content) article has some suggestions on what kind of content to store.

Markdown documents are automatically converted to HTML chunks, so that's a great way to include information. However, at present Hereditas web apps do not support hyperlinks, images or videos in Markdown or HTML files linking to other content within the box.

### Welcome file

As mentioned above, Hereditas generates a `welcome.md` file and pre-populates it with some default content.

The welcome file is displayed in the authentication page, and you can use it to provide some information about what your Hereditas box is, and how it can be used.

Note that the welcome file is **not encrypted**, so do not store any confidential information in there!

## Set the webhook URL

We need to set the URL of the webhook we created in the previous step. We can use [`hereditas webhook:set`]({{< relref "/cli/webhook_set.md" >}}) for that, replacing the URL below with yours:

````sh
hereditas webhook:set --url "https://maker.ifttt.com/trigger/hereditas_auth/with/key/123abc456def"
````

## Synchronize changes on Auth0

At this point, let's create the Hereditas application and rules on Auth0, which will also give us the required Client Id.

The Hereditas CLI has a built-in command [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) that manages the application, configuration and rules inside Auth0, in a fully-automated way. So, syncing the changes is as simple as running:

````sh
hereditas auth0:sync
````

The command above will create the application and the rules on Auth0, and make sure that everything is configured correctly. As we'll see in the next steps, you will need to re-run that command after making certain configuration changes.

## Next step: Build the static web app

We're finally ready to use the Hereditas CLI to build our static app! Follow the instructions in the next article for how:

}}">Build the static web app



================================================
FILE: docs-source/content/guides/deploy-box.md
================================================
---
title: Deploy the box
type: docs
---

# Deploy the box

In this last step, we're finally ready to deploy the static web app!

In the [Build the static web app]({{< relref "/guides/build-static-web-app.md" >}}) article you used the Hereditas CLI to generate the box, which is a static, HTML5 web application. The generated files are in the `dist` folder. It's now time to take those files and make them accessible to others.

## Sync changes with Auth0

In case you haven't done it already in the previous step, run the [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) command to ensure that the Hereditas application and rules are propertly configured on Auth0 (it's safe to run this command as often as you'd like).

````sh
hereditas auth0:sync
````

## Testing locally

Before deploying your app, you can test it running on your laptop with a local server. There are multiple options to run a local server; a simple one is:

````sh
npx serve dist -n
````

This will serve all files in the `dist` directory at the URL `http://localhost:5000`, which you can open with any web browser.

Keep in mind that the URL and port must be whitelisted in the Hereditas app and Auth0. In the previous step, we did whitelist `http://localhost:5000` when running [`hereditas init`]({{< relref "/cli/init.md" >}}), so we're good for now. If you use a local server listening on another port, however, you'll have to allow that URL too – see the [managing deployment URLs](#managing-deployment-urls) section below.

## Choosing where to host your box

Your box is just a static HTML5 web app, with HTML, JavaScript and CSS files, plus a bunch of encrypted documents. You can deploy it on any service capable of serving HTML5 apps via HTTP(S).

Because all of your data is encrypted, Hereditas boxes are designed to be deployed on publicly-accessible endpoints too, safely.

Good solutions include [Azure Blob Storage](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website), or [AWS S3](https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html). Any provider that supports static website hosting should work; this service is often free, or very inexpensive.

While possible, we do not recommend deploying Hereditas on a VPS (*what would happen if your credit card got canceled and your services stopped?*), nor inside a server in your home (*would your relatives know how to access it from within your LAN? what if your landlord disconnected your servers, would people know how to rebuild your network?*). Ultimately, however, it's up to you, your specific situation, and to the trust you put in the technical skills of your loved ones.

## Managing deployment URLs

After you've decided where to deploy your app to, you need to whitelist the URL where it will be reachable at. This is necessary because after a successful authentication, Auth0 will redirect users only to URLs you specifically whitelist, for security reasons.

You can manage the list of allowed URLs using the Hereditas CLI, with the commands:

- [`hereditas url:add`]({{< relref "/cli/url_add.md" >}})
- [`hereditas url:list`]({{< relref "/cli/url_list.md" >}})
- [`hereditas url:rm`]({{< relref "/cli/url_rm.md" >}})

For example, let's whitelist `https://myhereditas.example.com` and remove the localhost one we added earlier:

````sh
hereditas url:add --url "https://myhereditas.example.com"
hereditas url:rm --url "http://localhost:5000"

# Verify the list
hereditas url:list
````

After making changes to the list of URLs, sync them with Auth0 so they become effective (you don't need to re-build the Hereditas box, however):

````sh
hereditas auth0:sync
````

## Examples

Here's an example on how to deploy your box to Azure Storage.

### Deploy to Azure Storage

Azure Storage is an object storage provider that offers static website hosting too. You pay for how much data you store, at a rate that starts at less than $0.02 per GB per month.

In order to deploy to Azure Storage, you'll need:

- An Azure account. You can get it [for free](https://azure.com/free) if you don't have one already.
- The Azure CLI installed on your laptop. Installation instructions are in the [official documentation](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest).

Start by logging into Azure and creating an Azure Storage Account:

````sh
# Log into azure
# After running this, follow the instructions to log in via a web browser
az login

# Create a Resource Group and a Storage Account
# The storage account name must be universally unique
# For a list of locations available, type:
# az account list-locations --query "[].{Region:name}" --out table
AZURE_STORAGE_ACCOUNT="myhereditas"
AZURE_RESOURCE_GROUP="Hereditas"
AZURE_LOCATION="eastus2"
az group create \
    --name "$AZURE_RESOURCE_GROUP" \
    --location "$AZURE_LOCATION"
az storage account create \
    --name "$AZURE_STORAGE_ACCOUNT" \
    --resource-group "$AZURE_RESOURCE_GROUP" \
    --location "$AZURE_LOCATION" \
    --sku Standard_LRS \
    --kind StorageV2
````

At this point, enable static website hosting for your Storage Account, and retrieve the URL with:

````sh
az storage blob service-properties update \
    --account-name "$AZURE_STORAGE_ACCOUNT" \
    --static-website \
    --404-document "404.html" \
    --index-document "index.html"
az storage account show \
    --name "$AZURE_STORAGE_ACCOUNT" \
    --resource-group "$AZURE_RESOURCE_GROUP" \
    --query "primaryEndpoints.web" \
    --output tsv
# Result will be something similar to:
# https://myhereditas.z20.web.core.windows.net/
````

We can now upload all files from the `dist` folder into the Storage Account, in the `$web` container:

````sh
az storage blob upload-batch \
    --source dist \
    --destination "\$web" \
    --account-name "$AZURE_STORAGE_ACCOUNT"
````

In the last step, we need to whitelist the website's URL with Auth0:

````sh
# Replace the URL with yours
hereditas url:add --url "https://myhereditas.z20.web.core.windows.net"
hereditas auth0:sync
````

Done! You can now go to `https://myhereditas.z20.web.core.windows.net` and use your Hereditas box.

![Hereditas deployed to Azure Storage](/images/deploy-box-azure-done.png)

## Share the information with your relatives

At this point, you have all the information you need to give to your relatives, for usage in case you disappear. Send them a letter, or an email, or anything else that works for you.

Make sure to include:

1. An explanation of what this Hereditas box is, and what information they can find in there.
2. The URL they need to type
3. The *user passphrase*
4. The name of the account they need to use to sign in (the email address)

This is all and only the information they need to use Hereditas.


================================================
FILE: docs-source/content/guides/get-started.md
================================================
---
title: Get started
type: docs
---

# Get started

## Prerequisites

In order to use Hereditas, you will need [Node.js](https://nodejs.org/en/download/) 10 or higher installed on your laptop.

You will also need three services; we'll guide you through the creation of those in the next steps.

1. A (free) [Auth0](https://auth0.com/) account. This is used by Hereditas to ensure that only authorized users can access your data, and only after a certain amount of time after the first request. (But don't worry: Auth0 and their developers have no way to access your data)
2. A webhook that you can use to send you notifications when users log into your Hereditas box, so you know when the unlock timer starts and it gives you a chance to stop it. There are multiple options for that, including [IFTTT](https://ifttt.com/) (free), or more advanced solutions like Microsoft Flow, Azure Functions, AWS Lambda, etc.
3. A place where to host static HTML5 apps (HTML, JavaScript, CSS files, plus your encrypted content) serving it over HTTP(S).

## Install the Hereditas CLI

You can install the Hereditas CLI on your machine from NPM, by running:

````sh
npm install --global hereditas

# Verify it's installed with
hereditas --version
````

> Note: If you prefer not to install the CLI as a global package, you can always invoke it using NPX, for example with `npx hereditas --version`. However, each command invocation will take significantly longer as NPX needs to restore all dependencies.

## Step zero: gather all content

This is the least technical of all the steps, but by far the most important one.

As the owner of an Hereditas box, you start by assembling all the content you want to include in your digital legacy box. For example, text/Markdown documents, images, and other files.

Things you might want to include:

* The passwords to access your laptop and your phone/tablet/watch/etc.
* The recovery key for your password manager, for example iCloud Keychain, 1Password, LastPass, KeePass, etc.
* How to access your private photos on an encrypted drive or cloud storage.
* Useful encryption keys, inclduing keys for your cryptocurrency wallets.
* Or, just a nice letter.

This step is very personal, and Hereditas gives you total flexibility to decide what to include in your box.

While it would technically be possible, we recommend that you don't store large amount of data, or data that changes frequently, inside an Hereditas box. In fact, every time you change any information, you'd have to encrypt and publish again the entire box, which can be very time-consuming, and could lead to your box containing outdated information.

For example, rather than including gigabytes of photos, we recommend that you store them in a safe place (encrypted drive, cloud storage, etc) and use your Hereditas box to explain how to retrieve them. Similarly, instead of including every single password (which can change frequently), just put the recovery key of your password manager.

## Next step: Auth0 setup

After you've installed the Hereditas CLI and gathered all the content, you're ready to go to the next step and configure a new Auth0 account.

}}">Auth0 setup


================================================
FILE: docs-source/content/guides/login-notifications.md
================================================
---
title: Login notifications
type: docs
---

# Login notifications

As the owner of an Hereditas box, you'll want to be notified when someone signs into your box, to potentially block unauthorized attempts. For example, you can choose to receive a text message, or an email, etc.

Hereditas uses [webhooks](https://codeburst.io/what-are-webhooks-b04ec2bf9ca2) for this, which are just POST requests to an external HTTPS endpoint (make sure you use HTTPS, and not HTTP!).

## Notification webhook

Hereditas sends a webhook to the URL you provide, as a POST request with the following JSON body:

````json
{
    "value1": "Full notification, e.g. 'New Hereditas login on Fri, 08 Mar 2019 12:01:10 GMT. User: user@example.com (role: user)'",
    "value2": "email address of user, e.g. user@example.com",
    "value3": "role, either owner or user"
}
````

You can point the webhook to whatever service you'd like to use. The next sections will show some common examples.

### Using IFTTT

[IFTTT](https://ifttt.com/), or "IF This Then That", is a free service that lets you "connect" multiple APIs and actions.

After enabling the [webhook service](https://ifttt.com/maker_webhooks), you'll get a private key. The URL you need to use is:

````text
https://maker.ifttt.com/trigger/{event}/with/key/{key}
````

Replace `{event}` with an event name (e.g. `hereditas_auth`) and `{key}` with your IFTTT Webhook key (so messages are sent to yourself). For example:

````text
https://maker.ifttt.com/trigger/hereditas_auth/with/key/123abc456def
````

Note down your webhook URL, as we'll need it soon.

You can then configure your IFTTT applet to perform any action as a consequence of this. For example, you could send yourself an email, a message on Telegram, or a notification on Slack (or turn the lights red in your home, etc!).

If you send yourself a message, you can use `{{value1}}` as a pre-made mesasge, or you can write whatever body you prefer. As example:

````text
{{Value2}} (role: {{Value3}}) just logged into your Hereditas box at {{OccurredAt}}!
````

## Next step: Create the box

We now have all the information we need to create the Hereditas box in our laptop and start putting content in there.

}}">Create the box


================================================
FILE: docs-source/content/guides/managing-users.md
================================================
---
title: Managing users
type: docs
---

# Managing users

You can use the Hereditas CLI to add or remove authorized users and owners.

## Roles

Hereditas users can have one of two roles:

- **user**: This is the normal user role. When someone with a *user* role signs into your Hereditas box, they are not immediately able to unlock it. Instead, a successful authentication of someone with a *user* role will start the timer, and after a certain delay (e.g. 24 hours, or as you configured it), users can sign in again and this time they'll be able to unlock the box (as long as they know the *user passphrase*).
- **owner**: When an *owner* successfully signs in, two things happen. First, they are always returned the *application token* by Auth0, so they can unlock the Hereditas box at any time (as long as they know the *user passphrase*). Second, an *owner* logging in always stops any running timer. This prevents those with a *user* role from accessing your data when you don't want them to.

## Add or remove authorized users

You can easily manage users with the Hereditas CLI.

To **add users**, use the [`hereditas user:add`]({{< relref "/cli/user_add.md" >}}) command.

````sh
hereditas user:add --email "someone@example.com"
````

By default, all users are given the role *user*. To add an *owner*, use the `--role owner` option:

````sh
hereditas user:add --email "owner@example.com" --role owner
````

You can **list users** who have access to your Hereditas box, and their roles, with the [`hereditas user:list`]({{< relref "/cli/user_list.md" >}}) command.

````sh
hereditas user:list
````

Lastly, you can **remove users** with the [`hereditas user:rm`]({{< relref "/cli/user_rm.md" >}}) command.

````sh
hereditas user:rm --email "someone@example.com"
````

## Synchronize changes on Auth0

The commands above save the changes in the local `hereditas.json` configuration file only.

In order for changes like adding/removing users (and others including changing the wait time, the webhook URL, or re-generating the application token) to be effective, you need to synchronize them with Auth0.

We can use again the [`hereditas auth0:sync`]({{< relref "/cli/auth0_sync.md" >}}) command, which will synchronize all changes in Auth0, updating our rules and application configuration, in a fully-automated way.

````sh
hereditas auth0:sync
````

## Next step: Deploy the box

We're almost there! Ready to test the box locally and then deploy it, so your users can access it when needed.

}}">Deploy the box


================================================
FILE: docs-source/content/introduction/quickstart-video.md
================================================
---
title: Quickstart video
type: docs
---

# Quickstart video

This quickstart video shows you how to get an Hereditas box configured, built and deployed to the cloud, in just a few minutes.

{{< youtube iGgza7AK7ow >}}

> Note: the quickstart video was recorded with Hereditas 0.1. All instructions remain the same for Hereditas 0.2, but the user interface looks better now.

## Get started documentation

For more information, check out our Get Started documentation.

}}">Get started with Hereditas


================================================
FILE: docs-source/content/introduction/security-model.md
================================================
---
title: Security model
type: docs
---

# Security model

This document explains in technical details how Hereditas ensures that your data is protected.

## Data encryption

Hereditas, as a static site generator, encrypts all of your sensitive data with AES-256 in [Galois/Counter Mode (CGM)](https://en.wikipedia.org/wiki/Galois/Counter_Mode), a symmetric cryptographic algorithm that is industry-standard for encrypting data. GCM is an authenticated encryption algorithm, designed to provide both data authenticity and confidentiality.

Each file is encrypted with a unique, random 256-bit key (32 bytes), which is wrapped with a master key (read more below). The wrapped key (40 bytes-long after wrapping) is stored at the beginning of the file.

Additionally, each file is encrypted with a unique, random IV of 12 bytes, which is stored in clear text at the beginning of each file, right after the wrapped key.

Encrypted files are given random names so attackers cannot gather information about the name and type of each file.

## Index file

Each Hereditas box also contains an `_index` file, which is encrypted just like each data file. The file's key is 256-bit, unique and randomly-generated, and it's wrapped with the master key. The wrapped key and the random, unique 12-byte IV are stored at the beginning of the file.

When in cleartext, the index file is a JSON document that contains the original file name, the id of the object stored in the Hereditas box, the authentication tag (as returned by AES-GCM), and a few more details. For more information on the index file, see the [Index file]({{< relref "/advanced/index-file.md" >}}) article in the advanced section.

## Master key

Each file inside the box is encrypted with a unique key that is wrapped with a master key. You can read the next section for details on the key wrapping algorithm used.

The master key is itself derived from the *user passphrase* and the *application token*.

The **user passphrase** is set while running [`hereditas build`]({{< relref "/cli/build.md" >}}). Users need to type it in the Hereditas app before they can unlock the box. The owner can choose any passphrase they want, as long as it's longer than 8 characters. This is not stored anywhere, but you should communicate it (in a safe way) to your loved ones.

The **application token** is unique to each Hereditas box and stored in the `hereditas.json` configuration file. By default, Hereditas generates it when the [`hereditas init`]({{< relref "/cli/init.md" >}}) command is executed, and the token can be re-generated with [`hereditas regenerate-token`]({{< relref "/cli/regenerate-token.md" >}}). The CLI creates the application token by getting 21 random bytes with Node.js [`crypto.randomBytes()`](https://nodejs.org/api/crypto.html#crypto_crypto_randombytes_size_callback), then encoding them as base64. The application token is stored inside Auth0 as a ["rule configuration"](https://auth0.com/docs/rules/guides/configuration), and it's passed to the web app in the JWT token for users that are logged in when they're ready to unlock the box (see below).

The **master key** is derived from the concatenated string (user passphrase + application token) using a key derivation function. Hereditas supports two strong, industry-standard key derivation functions:

- [Argon2](https://en.wikipedia.org/wiki/Argon2) is the default since version 0.2, and it uses the Argon2id variant. Argon2id can use a configurable amount of memory, which can be set in the `hereditas.json` config file; the default is 64 MB.
- [PBKDF2](https://en.wikipedia.org/wiki/PBKDF2), which is based on SHA-512 and supports a configurable number of iterations. The default is 100,000 iterations, and it can be configured with the `hereditas.json` config file.

Both key derivation functions generate a 256-bit key.

Argon2 is the default because it is known to provide better resistance against GPU-based brute force attacks. However, while support for PBKDF2 is available natively in browser thanks to the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API), Argon2 uses an [external module](https://github.com/antelle/argon2-browser) based on WebAssembly.

Hereditas uses a salt with the key derivation function (both Argon2 and PBKDF2) that is re-generated on each new build of the box (ie. each time you run [`hereditas build`]({{< relref "/cli/build.md" >}})), and it's stored inside the app's JavaScript file in cleartext.

## Key wrapping

After deriving the master key, Hereditas can use that to wrap and un-wrap the key used to encrypt each file.

As mentioned above, each file's key is a unique, randomly-generated sequence of 32 bytes (256 bits), suitable for AES-256-GCM. This key is wrapped and then stored at the beginning of each file.

File keys are wrapped using the AES-KW algorithm, as defined in RFC 3394. The master key is used as wrapping key.

## Unlocking the box

In order to unlock the box, users need to complete two steps:

1. Authenticate with Auth0. This lets authorized users get the *application token* if appropriate.
2. Type the *user passphrase*.

When users open the Hereditas web app, they are redirected to Auth0 to authenticate themselves.

- Only users whose email address is explicitly whitelisted (using [`hereditas user:add`]({{< relref "/cli/user_add.md" >}})) are allowed to log in. Users can authenticate with any social profile they want (anything supported by Auth0, eg. Google, Facebook, Microsoft accounts, etc), as long as the email address returned by the provider is included in the whitelist.
- Users can have two roles: *owner* and *user*.
- When an **owner** authenticates, Auth0 includes the *application token* in the JWT token every time. Owners who also know the *user passphrase* can unlock their Hereditas boxes any time they want.
- When a normal **user** (ie. non-owner) authenticates the first time, Auth0 sets the time of the login in the Client Application setting, but does not return the *application token*.
- After a configurable amount of time, e.g. 24 hours, users (non-owners) can authenticate again, and this time Auth0 will include the *application token* in the JWT token. The wait time can be configured with [`hereditas wait-time:set`]({{< relref "/cli/wait-time_set.md" >}}). Users can then unlock the Hereditas box if they also know the *user passphrase*.
- If an *owner* authenticates at any time, Auth0 resets any active timer, preventing other users to unlock the Hereditas box when the owner is still around.

## Trustless

The model above is what allows Hereditas to be fully trustless:

1. Users who are in possession of the *user passphrase* cannot unlock Hereditas boxes without the *application token*, even if they have full access to the encrypted files.
2. Auth0 stores only the *application token* and has no knowledge of the *user passphrase*. So, a malicious actor who managed to extract the *application token* from Auth0 would not be able to unlock the Hereditas box.
3. Users need to wait a certain amount of time before they're allowed to unlock Hereditas boxes, and owners can stop the timer by logging in themselves. This guarantees that ill-intentioned users won't be able to unlock Hereditas boxes until you're around.


================================================
FILE: docs-source/content/menu/__template.md
================================================
---
headless: true
---
{{! Set Mustache delimeters to ASP-style tags (this is a Mustache comment) }}
{{=<% %>=}}

* **Introduction**
  * [What is Hereditas]({{< relref "/" >}})
  * [Quickstart video]({{< relref "/introduction/quickstart-video.md" >}})
  * [Security model]({{< relref "/introduction/security-model.md" >}})
* **Guides**
  * [Get started]({{< relref "/guides/get-started.md" >}})
  * [Auth0 setup]({{< relref "/guides/auth0-setup.md" >}})
  * [Login notifications]({{< relref "/guides/login-notifications.md" >}})
  * [Create the box]({{< relref "/guides/create-box.md" >}})
  * [Build the static web app]({{< relref "/guides/build-static-web-app.md" >}})
  * [Managing users]({{< relref "/guides/managing-users.md" >}})
  * [Deploy the box]({{< relref "/guides/deploy-box.md" >}})
* **CLI Reference**
<%# index %>
  * [<% name %>]({{< relref "/cli/<% path %>" >}})
<%/ index %>
* **Advanced topics**
  * [Building self-contained binaries]({{< relref "/advanced/building-self-contained-binaries.md" >}})
  * [Auth0 manual configuration]({{< relref "/advanced/auth0-manual-configuration.md" >}})
  * [Configuration file]({{< relref "/advanced/configuration-file.md" >}})
  * [Index file]({{< relref "/advanced/index-file.md" >}})
* **Other**
  * [GitHub project page](https://github.com/ItalyPaleAle/hereditas)


================================================
FILE: docs-source/generate-cli-docs.js
================================================
'use strict'

const Mustache = require('mustache')
const fs = require('fs')
const util = require('util')

// Promisified functions
const readFilePromise = util.promisify(fs.readFile)
const readdirPromise = util.promisify(fs.readdir)
const statPromise = util.promisify(fs.stat)
const writeFilePromise = util.promisify(fs.writeFile)

// Scan a directory recursively and get the file names
const scanFolder = async (base, folder, result) => {
    result = result || []
    folder = folder || ''

    // Scan the list of files and folders, recursively
    const list = await readdirPromise(base + folder)
    for (const e in list) {
        const el = base + folder + list[e]

        // Check if it's a directory
        const stat = await statPromise(el)
        if (!stat) {
            continue
        }

        // If it's a directory, scan it recursively
        if (stat.isDirectory()) {
            await scanFolder(base, folder + list[e] + '/', result)
        }
        // Get only JavaScript files
        else if (el.substr(-3) === '.js') {
            // Add the file to the list
            result.push(folder + list[e])
        }
    }

    return result
}

const commandsPath = __dirname + '/../cli/commands/'
const docTemplateFile = __dirname + '/content/cli/__template.md'
const menuTemplateFile = __dirname + '/content/menu/__template.md'
const docDestinationPath = __dirname + '/content/cli/'
const menuDestinationPath = __dirname + '/content/menu/index.md'

// Main entrypoint
;(async function generateCliDocs() {
    // Load the templates
    const docTemplate = await readFilePromise(docTemplateFile, 'utf8')
    Mustache.parse(docTemplate)

    // Load the list of CLI commands
    const commands = await scanFolder(commandsPath)

    // Generate the documentation file for all commands
    const promises = commands.map(async (file) => {
        // Command name is derived from the file name
        const commandName = file.replace('/', ':').slice(0, -3)

        // Import the class
        const command = require(commandsPath + file)

        // Description: first line is the short one, and second line is the long one
        const description = command.description.trim()
        const [shortDescription, ...parts] = description.split('\n')
        const longDescription = parts.join('\n').trim()

        // Usage
        const usage = (command.usage) ?
            'hereditas ' + command.usage.trim() :
            'hereditas ' + commandName

        // Flags
        const flags = []
        if (command.flags) {
            // Iterate through the flags
            for (const key in command.flags) {
                if (!command.flags.hasOwnProperty(key) || !command.flags[key]) {
                    continue
                }
                const flag = command.flags[key]

                if (flag.type !== 'option') {
                    // eslint-disable-next-line no-console
                    console.warn('Skipping flag with type != "option"')
                    continue
                }

                // Required
                const required = flag.required ? '✓' : ''

                // Flag name and character
                let name = ['--' + key]
                if (flag.char) {
                    name.unshift('-' + flag.char)
                }
                name = '`' + name.join('`
`') + '`' // Type (including options) const type = (flag.options) ? '`"' + flag.options.join('"`, `"') + '"`' : 'string' const defaultValue = (flag.default) ? '`"' + flag.default + '"`' : 'none' // Add the flag flags.push({ name, description: flag.description, required, defaultValue, type }) } } // Build the documentation file const params = { commandName, shortDescription, longDescription, usage, hasFlags: !!flags.length, flags } const rendered = Mustache.render(docTemplate, params) const outfileName = commandName.replace(':', '_') + '.md' await writeFilePromise(docDestinationPath + outfileName, rendered) // Return the name of the command and the file, which will be used for the index return { name: commandName, path: outfileName } }) const index = await Promise.all(promises) // Use the index to build the menu const menuTemplate = await readFilePromise(menuTemplateFile, 'utf8') const menuRendered = Mustache.render(menuTemplate, { index }) await writeFilePromise(menuDestinationPath, menuRendered) })() ================================================ FILE: docs-source/sync-assets.sh ================================================ #!/bin/sh set -eu # "azcopy" command, defaults to searching for it in the PATH : "${AZCOPYCMD:=$(which azcopy)}" echo "Using azcopy in $AZCOPYCMD" # Ensure azcopy is installed "$AZCOPYCMD" --version # Check required env vars: $ASSETS, $CONTAINER, $AZURE_STORAGE_ACCOUNT if [ -z "$ASSETS" ]; then echo "\$ASSETS is empty" exit 1 fi if [ -z "$CONTAINER" ]; then echo "\$CONTAINER is empty" exit 1 fi if [ -z "$AZURE_STORAGE_ACCOUNT" ]; then echo "\$AZURE_STORAGE_ACCOUNT is empty" exit 1 fi echo "Syncing with: https://${AZURE_STORAGE_ACCOUNT}.blob.core.windows.net/${CONTAINER}" # Check that all folders specified in $ASSETS exist for asset in $ASSETS; do # Ensure the asset exists and it's a folder if [ ! -d "$asset" ]; then echo "$asset doesn't exist or it's not a folder" exit 2 fi done # Sync all folders for asset in $ASSETS; do # Sync the folder "$AZCOPYCMD" sync "$asset" https://${AZURE_STORAGE_ACCOUNT}.blob.core.windows.net/${CONTAINER}/${asset} --recursive --delete-destination=true done ================================================ FILE: docs-source/themes/book/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Alex Shpak Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: docs-source/themes/book/README.md ================================================ # Hugo Book Theme [![Build Status](https://travis-ci.org/alex-shpak/hugo-book.svg?branch=master)](https://travis-ci.org/alex-shpak/hugo-book) ### [Hugo](https://gohugo.io) documentation theme as simple as plain book ![Screenshot](https://github.com/alex-shpak/hugo-book/blob/master/images/screenshot.png) ## Features * Clean simple design * Mobile friendly * Customizable * Designed to not interfere with other layouts * Zero initial configuration ## Requirements * Hugo 0.43 or higher * Hugo extended version, read more [here](https://gohugo.io/news/0.43-relnotes/) ## Installation Navigate to your hugo website root and run: ``` git submodule add https://github.com/alex-shpak/hugo-book themes/book ``` Then run hugo (or set `theme: book` in configuration file) ``` hugo server --theme book ``` ## Menu ### File tree menu (default) By default theme will render pages from `content/docs` section as menu in a tree structure. You can set `title` and `weight` in front matter of pages to adjust order and titles in menu. ### Leaf bundle menu You can also use leaf bundle and content of it's `index.md` as menu. Given you have this file structure ``` ├── content │ ├── docs │ │ ├── page-one.md │ │ └── page-two.md │ └── posts │ ├── post-one.md │ └── post-two.md ``` Create file `content/docs/menu/index.md` with content ```md --- headless: true --- - [Book Example](/docs/) - [Page One](/docs/page-one) - [Page Two](/docs/page-two) - [Blog](/posts) ``` And Enable it by settings `BookMenuBundle: /docs/menu` in Site configuration - [Example menu](https://github.com/alex-shpak/hugo-book/blob/master/exampleSite/content/menu/index.md) - [Example config file](https://github.com/alex-shpak/hugo-book/blob/master/exampleSite/config.yml) - [Leaf bundles](https://gohugo.io/content-management/page-bundles/) ## Blog Simple blog supported for section `posts` ## Configuration ### Site Configuration There are few configuration options you can add to your `config.yml|json|toml` file ```yaml # (Optional) Set this to true if you use captial letters in file names disablePathToLower: true # (Optional) Set this to true to enable 'Last Modified by' date and git author # information on 'doc' type pages. enableGitInfo: true # (Warnings) Theme is intended for documentation use, there for it doesn't render taxonomy. # You can hide related warning with config below disableKinds: ["taxonomy", "taxonomyTerm"] params: # (Optional, default true) Show or hide table of contents globally # You can also specify this parameter per page in front matter BookShowToC: true # (Optional, default none) Set leaf bundle to render as side menu # When not specified file structure and weights will be used BookMenuBundle: /menu # (Optional, default docs) Specify section of content to render as menu # You can also set value to "*" to render all sections to menu BookSection: docs # This value is duplicate of $link-color for making active link highlight in menu bundle mode # BookMenuBundleActiveLinkColor: \#004ed0 # Include JS scripts in pages. Disabled by default. # - Keep side menu on same scroll position during navigation BookEnableJS: true # Set source repository location. # Used for 'Last Modified' and 'Edit this page' links. BookRepo: https://github.com/alex-shpak/hugo-book # Enable "Edit this page" links for 'doc' page type. # Disabled by default. Uncomment to enable. Requires 'BookRepo' param. # Path must point to 'content' directory of repo. BookEditPath: edit/master/exampleSite/content ``` ### Page Configuration You can specify additional params per page in front matter ```yaml --- # Set type to 'docs' if you want to render page outside of configured section or if you render section other than 'docs' type: docs # Set page weight to re-arrange items in file-tree menu (if BookMenuBundle not set) weight: 10 # (Optional) Set to mark page as flat section in file-tree menu (if BookMenuBundle not set) bookFlatSection: true # (Optional) Set to hide table of contents, overrides global value bookShowToC: false --- ``` ### Partials There are few empty partials you can override in `layouts/partials/` | Partial | Placement | | -- | -- | | `layouts/partials/docs/inject/head.html` | Before closing `` tag | | `layouts/partials/docs/inject/body.html` | Before closing `` tag | | `layouts/partials/docs/inject/menu-before.html` | At the beginning of `